diff --git a/.bazelrc b/.bazelrc new file mode 100644 index 000000000..8724ec1da --- /dev/null +++ b/.bazelrc @@ -0,0 +1,20 @@ +common --enable_bzlmod +# Use built-in 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 + + +# Hide Java 8 deprecation warnings. +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 + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 67062e439..59a173b91 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -6,12 +6,6 @@ labels: '' --- ---- -name: Feature request -about: Suggest an idea for this project - ---- - **Feature request checklist** - [ ] There are no issues that match the desired change diff --git a/.github/workflows/cross_artifact_dependencies_check.sh b/.github/workflows/cross_artifact_dependencies_check.sh new file mode 100755 index 000000000..0802a299a --- /dev/null +++ b/.github/workflows/cross_artifact_dependencies_check.sh @@ -0,0 +1,85 @@ +#!/bin/bash +# 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. + +set -o pipefail + +TARGETS=( + "//publish:cel" + "//publish:cel_common" + "//publish:cel_compiler" + "//publish:cel_runtime" + "//publish:cel_protobuf" + "//publish:cel_v1alpha1" +) + +echo "------------------------------------------------" +echo "Checking for duplicates..." +echo "------------------------------------------------" + +JDK8_FLAGS="--java_language_version=8 --java_runtime_version=8" +bazel build $JDK8_FLAGS "${TARGETS[@]}" || { echo "Bazel build failed"; exit 1; } + +( + for target in "${TARGETS[@]}"; do + # Locate the jar + jar_path=$(bazel cquery "$target" --output=files 2>/dev/null | grep '\-project.jar$') + + if [[ -z "$jar_path" ]]; then + echo "Error: Could not find -project.jar for target $target" >&2 + exit 1 + fi + + # Fix relative paths if running from a subdir. + if [[ ! -f "$jar_path" ]]; then + if [[ -f "../../$jar_path" ]]; then + jar_path="../../$jar_path" + else + echo "Error: File not found at $jar_path" >&2 + exit 1 + fi + fi + + echo "Inspecting: $target" >&2 + + # Extract classes and append the target name to the end of the line + # Format: dev/cel/expr/Expr.class //publish:cel_compiler + jar tf "$jar_path" | grep "\.class$" | awk -v tgt="$target" '{print $0, tgt}' + done +) | awk ' + # $1 is the Class Name, $2 is the Target Name + seen[$1] { + print "❌ DUPLICATE FOUND: " $1 + print " Present in: " seen[$1] + print " And in: " $2 + dupe=1 + next + } + { seen[$1] = $2 } + + END { if (dupe) exit 2 } +' + +EXIT_CODE=$? + +if [ $EXIT_CODE -eq 0 ]; then + echo "✅ Success: No duplicate classes found." +elif [ $EXIT_CODE -eq 2 ]; then + echo "⛔ Failure: Duplicate classes detected." +else + echo "💥 Error: An unexpected error occurred (e.g., missing jar files). Exit Code: $EXIT_CODE" +fi + +exit $EXIT_CODE + diff --git a/.github/workflows/unwanted_deps.sh b/.github/workflows/unwanted_deps.sh new file mode 100755 index 000000000..c483f9730 --- /dev/null +++ b/.github/workflows/unwanted_deps.sh @@ -0,0 +1,49 @@ +#!/bin/bash +# 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. + +# Script ran as part of Github CEL-Java CI to verify that the runtime jar does not contain unwanted dependencies. + +function checkUnwantedDeps { + target="$1" + unwanted_dep="$2" + + query="bazel query 'deps(${target})' --notool_deps --noimplicit_deps --output graph" + deps=$(eval $query) + + if echo "$deps" | grep "$unwanted_dep" > /dev/null; then + echo -e "$target contains unwanted dependency: $unwanted_dep!\n" + echo "$(echo "$deps" | grep "$unwanted_dep")" + exit 1 + fi +} + +# Do not include generated CEL protos in the jar +checkUnwantedDeps '//publish:cel_runtime' '@cel_spec' + +# cel_runtime does not support protolite +checkUnwantedDeps '//publish:cel_runtime' 'protobuf_java_util' +checkUnwantedDeps '//publish:cel' 'protobuf_java_util' + +# cel_runtime shouldn't depend on antlr +checkUnwantedDeps '//publish:cel_runtime' '@maven//:org_antlr_antlr4_runtime' + +# cel_runtime shouldn't depend on the protobuf_lite runtime +checkUnwantedDeps '//publish:cel_runtime' '@maven_android//:com_google_protobuf_protobuf_javalite' +checkUnwantedDeps '//publish:cel' '@maven_android//:com_google_protobuf_protobuf_javalite' + +# 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' +exit 0 diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 16ebd2a5f..94effd33b 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -15,6 +15,36 @@ concurrency: cancel-in-progress: true jobs: + Bazel-Build-Java8: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event." + - run: echo "🐧 Job is running on a ${{ runner.os }} server!" + - run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}." + - name: Check out repository code + uses: actions/checkout@v6 + - name: Setup Bazel + uses: bazel-contrib/setup-bazel@0.18.0 + with: + # Avoid downloading Bazel every time. + bazelisk-cache: true + # Store build cache per workflow. + disk-cache: ${{ github.workflow }} + # 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: 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 @@ -23,14 +53,77 @@ jobs: - run: echo "🐧 Job is running on a ${{ runner.os }} server!" - run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}." - name: Check out repository code - uses: actions/checkout@v3 - - name: Mount Bazel Cache - uses: actions/cache@v3 + uses: actions/checkout@v6 + - name: Setup Bazel + uses: bazel-contrib/setup-bazel@0.18.0 with: - path: "/home/runner/.cache/bazel" - key: bazelisk + # Avoid downloading Bazel every time. + bazelisk-cache: true + # Store build cache per workflow. + disk-cache: ${{ github.workflow }} + # 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: Bazel Test - run: bazelisk test ... --deleted_packages=codelab/src/test/codelab --test_output=errors # Exclude codelab exercises as they are intentionally made to fail + # Exclude codelab exercises as they are intentionally made to fail + # Exclude maven conformance tests. They are only executed when there's version change. + run: bazelisk test ... --deleted_packages=//codelab/src/test/codelab --test_output=errors --test_tag_filters=-conformance_maven --build_tag_filters=-conformance_maven - run: echo "🍏 This job's status is ${{ job.status }}." + + # -- Start of Maven Conformance Tests (Ran only when there's version changes) -- + Maven-Conformance: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event." + - run: echo "🐧 Job is running on a ${{ runner.os }} server!" + - run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}." + - name: Check out repository code + uses: actions/checkout@v6 + - name: Get changed files + id: changed_file + uses: tj-actions/changed-files@v47 + with: + files: publish/cel_version.bzl + - name: Setup Bazel + if: steps.changed_file.outputs.any_changed == 'true' + uses: bazel-contrib/setup-bazel@0.18.0 + with: + # Avoid downloading Bazel every time. + bazelisk-cache: true + # Store build cache per workflow. + disk-cache: ${{ github.workflow }} + # Share repository cache between workflows. + repository-cache: true + # Never write to the cache, strictly read-only + cache-save: false + - name: Verify Version Consistency + if: steps.changed_file.outputs.any_changed == 'true' + run: | + CEL_VERSION=$(grep 'CEL_VERSION =' publish/cel_version.bzl | cut -d '"' -f 2) + + MODULE_VERSION=$(grep 'CEL_VERSION =' MODULE.bazel | cut -d '"' -f 2) + + if [ -z "$CEL_VERSION" ] || [ -z "$MODULE_VERSION" ]; then + echo "❌ Error: Could not extract one or both version strings." + exit 1 + fi + + echo "Version in publish/cel_version.bzl: ${CEL_VERSION}" + echo "Version in MODULE.bazel: ${MODULE_VERSION}" + + if [ "$CEL_VERSION" != "$MODULE_VERSION" ]; then + echo "❌ Error: Version mismatch between files!" + exit 1 + fi + + echo "✅ Versions match." + - 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}." + env: + JOB_STATUS: ${{ job.status }} diff --git a/.gitignore b/.gitignore index 753e69672..3a2e0015d 100644 --- a/.gitignore +++ b/.gitignore @@ -6,8 +6,6 @@ bazel-cel-java bazel-out bazel-testlogs -MODULE.bazel* - # IntelliJ IDEA .idea *.iml @@ -30,3 +28,8 @@ target # Temporary output dir for artifacts mvn-artifacts + +*.swp +*.lock +.eclipse +.vscode diff --git a/BUILD.bazel b/BUILD.bazel index 3f9a36090..024908625 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -15,6 +15,20 @@ # Includes package-wide build definitions for maven imported java targets # that needs to be defined separately. +load( + "@bazel_tools//tools/jdk:default_java_toolchain.bzl", + "BASE_JDK9_JVM_OPTS", + "DEFAULT_JAVACOPTS", + "DEFAULT_TOOLCHAIN_CONFIGURATION", + "default_java_toolchain", +) +load( + "@rules_java//java:defs.bzl", + "java_binary", + "java_library", + "java_package_configuration", + "java_plugin", +) load("@rules_license//rules:license.bzl", "license") licenses(["notice"]) # Apache License 2.0 @@ -66,7 +80,6 @@ java_library( ], neverlink = 1, exports = [ - "@maven//:com_google_auto_value_auto_value", "@maven//:com_google_auto_value_auto_value_annotations", ], ) @@ -82,25 +95,16 @@ java_library( ], ) -load( - "@bazel_tools//tools/jdk:default_java_toolchain.bzl", - "BASE_JDK9_JVM_OPTS", - "DEFAULT_JAVACOPTS", - "DEFAULT_TOOLCHAIN_CONFIGURATION", - "default_java_toolchain", -) - default_java_toolchain( name = "repository_default_toolchain", configuration = DEFAULT_TOOLCHAIN_CONFIGURATION, - java_runtime = "@bazel_tools//tools/jdk:remote_jdk11", javacopts = DEFAULT_JAVACOPTS, jvm_opts = BASE_JDK9_JVM_OPTS, package_configuration = [ ":error_prone", ], - source_version = "8", - target_version = "8", + source_version = "11", + target_version = "11", ) # This associates a set of javac flags with a set of packages @@ -189,3 +193,14 @@ java_binary( main_class = "org.antlr.v4.Tool", runtime_deps = ["@antlr4_jar//jar"], ) + +# These two package groups are to allow proper bidrectional sync with g3 +package_group( + name = "internal", + packages = ["//..."], +) + +package_group( + name = "android_allow_list", + packages = ["//..."], +) diff --git a/MODULE.bazel b/MODULE.bazel new file mode 100644 index 000000000..fcaf041ba --- /dev/null +++ b/MODULE.bazel @@ -0,0 +1,140 @@ +# 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. + +module( + name = "cel_java", +) + +bazel_dep(name = "bazel_skylib", version = "1.9.0") +bazel_dep(name = "rules_jvm_external", version = "6.10") +bazel_dep(name = "protobuf", version = "33.4", repo_name = "com_google_protobuf") # see https://github.com/bazelbuild/rules_android/issues/373 +bazel_dep(name = "googleapis", version = "0.0.0-20260223-edfe7983", repo_name = "com_google_googleapis") +bazel_dep(name = "rules_pkg", version = "1.2.0") +bazel_dep(name = "rules_license", version = "1.0.0") +bazel_dep(name = "rules_proto", version = "7.1.0") +bazel_dep(name = "rules_java", version = "9.3.0") +bazel_dep(name = "rules_android", version = "0.7.1") +bazel_dep(name = "rules_shell", version = "0.6.1") +bazel_dep(name = "googleapis-java", version = "1.0.0") +bazel_dep(name = "cel-spec", version = "0.25.1", repo_name = "cel_spec") +bazel_dep(name = "rules_go", version = "0.50.1") + +# Required by cel-spec to satisfy gazelle transitive dependency +go_sdk = use_extension("@rules_go//go:extensions.bzl", "go_sdk") +go_sdk.download(version = "1.23.0") + +switched_rules = use_extension("@com_google_googleapis//:extensions.bzl", "switched_rules") +switched_rules.use_languages(java = True) +use_repo(switched_rules, "com_google_googleapis_imports") + +maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven") + +GUAVA_VERSION = "33.5.0" + +TRUTH_VERSION = "1.4.4" + +PROTOBUF_JAVA_VERSION = "4.33.5" + +CEL_VERSION = "0.13.1" + +# Compile only artifacts +[ + maven.artifact( + artifact = artifact, + group = group, + neverlink = True, + version = version, + ) + 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", + ]] +] + +# Test only artifacts +[ + maven.artifact( + testonly = True, + artifact = artifact, + group = group, + version = version, + ) + 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", + "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, + "com.google.truth.extensions:truth-liteproto-extension:" + TRUTH_VERSION, + "com.google.truth:truth:" + TRUTH_VERSION, + ]] +] + +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.guava:guava:" + GUAVA_VERSION + "-jre", + "com.google.protobuf:protobuf-java:" + PROTOBUF_JAVA_VERSION, + "com.google.protobuf:protobuf-java-util:" + PROTOBUF_JAVA_VERSION, + "com.google.re2j:re2j:1.8", + "info.picocli:picocli:4.7.7", + "org.antlr:antlr4-runtime:4.13.2", + "org.freemarker:freemarker:2.3.34", + "org.jspecify:jspecify:1.0.0", + "org.threeten:threeten-extra:1.8.0", + "org.yaml:snakeyaml:2.5", + ], + repositories = [ + "https://maven.google.com", + "https://repo1.maven.org/maven2", + ], +) +maven.install( + name = "maven_android", + # keep sorted + artifacts = [ + "com.google.guava:guava:" + GUAVA_VERSION + "-android", + "com.google.protobuf:protobuf-javalite:" + PROTOBUF_JAVA_VERSION, + ], + repositories = [ + "https://maven.google.com", + "https://repo1.maven.org/maven2", + ], +) + +# Conformance test only + +maven.install( + name = "maven_conformance", + artifacts = [ + "dev.cel:cel:" + CEL_VERSION, + "dev.cel:compiler:" + CEL_VERSION, + "dev.cel:runtime:" + CEL_VERSION, + ], + repositories = [ + "https://maven.google.com", + "https://repo1.maven.org/maven2", + "https://central.sonatype.com/repository/maven-snapshots/", + ], +) +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 c317e960e..e2424a85c 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.3.0 + 0.13.1 ``` **Gradle** ```gradle -implementation 'dev.cel:cel:0.3.0' +implementation 'dev.cel:cel:0.13.1' ``` Then run this example: @@ -264,7 +264,7 @@ robust to evaluation against dynamic data types such as JSON inputs. In the following truth-table, the symbols `` and `` represent error or unknown values, with the `?` indicating that the branch is not taken due to -short-circuiting. When the result is `` this means that the both args are +short-circuiting. When the result is `` this means that both the args are possibly relevant to the result. | Expression | Result | @@ -376,16 +376,14 @@ Java 8 or newer is required. Released under the [Apache License](LICENSE). -Disclaimer: This is not an official Google product. - -[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/WORKSPACE b/WORKSPACE deleted file mode 100644 index f7b21a719..000000000 --- a/WORKSPACE +++ /dev/null @@ -1,172 +0,0 @@ -# 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. - -workspace(name = "cel_java") - -register_toolchains("//:repository_default_toolchain_definition") - -load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive", "http_jar") - -http_archive( - name = "bazel_skylib", - sha256 = "74d544d96f4a5bb630d465ca8bbcfe231e3594e5aae57e1edbf17a6eb3ca2506", - urls = [ - "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.3.0/bazel-skylib-1.3.0.tar.gz", - "https://github.com/bazelbuild/bazel-skylib/releases/download/1.3.0/bazel-skylib-1.3.0.tar.gz", - ], -) - -load("@bazel_skylib//:workspace.bzl", "bazel_skylib_workspace") - -bazel_skylib_workspace() - -RULES_JVM_EXTERNAL_TAG = "aa44247b3913da0da606e9c522313b6a9396a571" - -RULES_JVM_EXTERNAL_SHA = "87378580865af690a78230e04eba1cd6d9c60d0db303ea129dc717705d711d9c" - -# rules_jvm_external as of 12/11/2023 -http_archive( - name = "rules_jvm_external", - sha256 = RULES_JVM_EXTERNAL_SHA, - strip_prefix = "rules_jvm_external-%s" % RULES_JVM_EXTERNAL_TAG, - url = "https://github.com/bazelbuild/rules_jvm_external/archive/%s.zip" % RULES_JVM_EXTERNAL_TAG, -) - -load("@rules_jvm_external//:defs.bzl", "maven_install") -load("@rules_jvm_external//:repositories.bzl", "rules_jvm_external_deps") - -rules_jvm_external_deps() - -load("@rules_jvm_external//:setup.bzl", "rules_jvm_external_setup") - -rules_jvm_external_setup() - -ANTLR4_VERSION = "4.11.1" - -# Important: there can only be one maven_install rule. Add new maven deps here. -maven_install( - # keep sorted - artifacts = [ - "com.google.api.grpc:proto-google-common-protos:2.27.0", - "com.google.auto.value:auto-value:1.10.4", - "com.google.auto.value:auto-value-annotations:1.10.4", - "com.google.code.findbugs:annotations:3.0.1", - "com.google.errorprone:error_prone_annotations:2.23.0", - "com.google.guava:guava:33.0.0-jre", - "com.google.guava:guava-testlib:33.0.0-jre", - "com.google.protobuf:protobuf-java:3.24.4", - "com.google.protobuf:protobuf-java-util:3.24.4", - "com.google.re2j:re2j:1.7", - "com.google.testparameterinjector:test-parameter-injector:1.15", - "com.google.truth.extensions:truth-java8-extension:1.4.0", - "com.google.truth.extensions:truth-proto-extension:1.4.0", - "com.google.truth:truth:1.4.0", - "org.antlr:antlr4-runtime:" + ANTLR4_VERSION, - "org.jspecify:jspecify:0.2.0", - "org.threeten:threeten-extra:1.7.2", - ], - repositories = [ - "https://maven.google.com", - "https://repo1.maven.org/maven2", - ], -) - -http_archive( - name = "com_google_protobuf", - sha256 = "b1d6dd2cbb5d87e17af41cadb720322ce7e13af826268707bd8db47e5654770b", - strip_prefix = "protobuf-21.11", - urls = ["https://github.com/protocolbuffers/protobuf/archive/v21.11.tar.gz"], -) - -# Required by com_google_protobuf -load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps") - -protobuf_deps() - -# googleapis as of 12/08/2022 -http_archive( - name = "com_google_googleapis", - sha256 = "8503282213779a3c230251218c924f385f457a053b4f82ff95d068f71815e558", - strip_prefix = "googleapis-d73a41615b101c34c58b3534c2cc7ee1d89cccb0", - urls = [ - "https://github.com/googleapis/googleapis/archive/d73a41615b101c34c58b3534c2cc7ee1d89cccb0.tar.gz", - ], -) - -load("@com_google_googleapis//:repository_rules.bzl", "switched_rules_by_language") - -switched_rules_by_language( - name = "com_google_googleapis_imports", - java = True, -) - -# Required by googleapis -http_archive( - name = "rules_pkg", - sha256 = "8a298e832762eda1830597d64fe7db58178aa84cd5926d76d5b744d6558941c2", - urls = [ - "https://github.com/bazelbuild/rules_pkg/releases/download/0.7.0/rules_pkg-0.7.0.tar.gz", - ], -) - -load("@rules_pkg//:deps.bzl", "rules_pkg_dependencies") - -rules_pkg_dependencies() - -BAZEL_COMMON_TAG = "aaa4d801588f7744c6f4428e4f133f26b8518f42" - -BAZEL_COMMON_SHA = "1f85abb0043f3589b9bf13a80319dc48a5f01a052c68bab3c08015a56d92ab7f" - -http_archive( - name = "bazel_common", - sha256 = BAZEL_COMMON_SHA, - strip_prefix = "bazel-common-%s" % BAZEL_COMMON_TAG, - url = "https://github.com/google/bazel-common/archive/%s.tar.gz" % BAZEL_COMMON_TAG, -) - -# cel-spec api/expr canonical protos -http_archive( - name = "cel_spec", - sha256 = "3579c97b13548714f9059ef6f30c5264d439efef4b438e76e7180709efd93a6b", - strip_prefix = "cel-spec-0.14.0", - urls = [ - "https://github.com/google/cel-spec/archive/refs/tags/v0.14.0.tar.gz", - ], -) - -# required by cel_spec -http_archive( - name = "io_bazel_rules_go", - sha256 = "19ef30b21eae581177e0028f6f4b1f54c66467017be33d211ab6fc81da01ea4d", - urls = [ - "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.38.0/rules_go-v0.38.0.zip", - "https://github.com/bazelbuild/rules_go/releases/download/v0.38.0/rules_go-v0.38.0.zip", - ], -) - -http_jar( - name = "antlr4_jar", - sha256 = "62975e192b4af2622b72b5f0131553ee3cbce97f76dc2a41632dcc55e25473e1", - urls = ["https://www.antlr.org/download/antlr-" + ANTLR4_VERSION + "-complete.jar"], -) - -# Load license rules. -http_archive( - name = "rules_license", - sha256 = "6157e1e68378532d0241ecd15d3c45f6e5cfd98fc10846045509fb2a7cc9e381", - urls = [ - "https://github.com/bazelbuild/rules_license/releases/download/0.0.4/rules_license-0.0.4.tar.gz", - "https://mirror.bazel.build/github.com/bazelbuild/rules_license/releases/download/0.0.4/rules_license-0.0.4.tar.gz", - ], -) diff --git a/bundle/BUILD.bazel b/bundle/BUILD.bazel index e562cff00..1eaf0bec8 100644 --- a/bundle/BUILD.bazel +++ b/bundle/BUILD.bazel @@ -1,3 +1,5 @@ +load("@rules_java//java:defs.bzl", "java_library") + package( default_applicable_licenses = ["//:license"], default_visibility = ["//visibility:public"], @@ -5,5 +7,35 @@ 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( + name = "environment", + exports = ["//bundle/src/main/java/dev/cel/bundle:environment"], +) + +java_library( + name = "environment_exception", + exports = ["//bundle/src/main/java/dev/cel/bundle:environment_exception"], +) + +java_library( + name = "environment_yaml_parser", + exports = ["//bundle/src/main/java/dev/cel/bundle:environment_yaml_parser"], +) + +java_library( + name = "environment_exporter", + exports = ["//bundle/src/main/java/dev/cel/bundle:environment_exporter"], +) + +java_library( + name = "cel_impl", + testonly = 1, + visibility = ["//:internal"], + exports = ["//bundle/src/main/java/dev/cel/bundle:cel_impl"], ) diff --git a/bundle/src/main/java/dev/cel/bundle/BUILD.bazel b/bundle/src/main/java/dev/cel/bundle/BUILD.bazel index c2c7d3f10..716442849 100644 --- a/bundle/src/main/java/dev/cel/bundle/BUILD.bazel +++ b/bundle/src/main/java/dev/cel/bundle/BUILD.bazel @@ -1,3 +1,5 @@ +load("@rules_java//java:defs.bzl", "java_library") + package( default_applicable_licenses = ["//:license"], default_visibility = [ @@ -9,8 +11,6 @@ package( CEL_SOURCES = [ "Cel.java", "CelBuilder.java", - "CelFactory.java", - "CelImpl.java", ] java_library( @@ -19,28 +19,179 @@ 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", + ], +) + +java_library( + name = "cel_impl", + srcs = ["CelImpl.java"], + tags = [ + ], + deps = [ + ":cel", "//checker:checker_builder", - "//checker:checker_legacy_environment", "//checker:proto_type_mask", - "//common", + "//checker:standard_decl", + "//checker:type_provider_legacy", + "//common:cel_ast", + "//common:cel_source", "//common:compiler_common", + "//common:container", "//common:options", "//common/internal:env_visitor", "//common/internal:file_descriptor_converter", - "//common/types:cel_types", + "//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", - "@cel_spec//proto/cel/expr:expr_java_proto", - "@maven//:com_google_code_findbugs_annotations", + "//runtime:function_binding", + "//runtime:runtime_planner_impl", + "//runtime:standard_functions", + "@cel_spec//proto/cel/expr:checked_java_proto", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", ], ) + +java_library( + name = "environment", + srcs = [ + "CelEnvironment.java", + ], + tags = [ + ], + deps = [ + ":environment_exception", + ":required_fields_checker", + "//:auto_value", + "//bundle:cel", + "//checker:proto_type_mask", + "//checker:standard_decl", + "//common:compiler_common", + "//common:container", + "//common:options", + "//common:source", + "//common/types", + "//common/types:type_providers", + "//compiler:compiler_builder", + "//extensions", + "//parser:macro", + "//runtime", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "environment_exception", + srcs = [ + "CelEnvironmentException.java", + ], + tags = [ + ], + deps = ["//common:cel_exception"], +) + +java_library( + name = "environment_yaml_parser", + srcs = [ + "CelEnvironmentYamlParser.java", + "CelEnvironmentYamlSerializer.java", + ], + tags = [ + ], + deps = [ + ":environment", + ":environment_exception", + "//common:compiler_common", + "//common:container", + "//common/formats:file_source", + "//common/formats:parser_context", + "//common/formats:yaml_helper", + "//common/formats:yaml_parser_context_impl", + "//common/internal", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven//:org_jspecify_jspecify", + "@maven//:org_yaml_snakeyaml", + ], +) + +java_library( + name = "environment_exporter", + srcs = [ + "CelEnvironmentExporter.java", + ], + tags = [ + ], + deps = [ + ":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:type_providers", + "//compiler:compiler_builder", + "//extensions", + "//extensions:extension_library", + "//parser:macro", + "@cel_spec//proto/cel/expr:checked_java_proto", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "required_fields_checker", + srcs = [ + "RequiredFieldsChecker.java", + ], + visibility = ["//visibility:private"], + deps = [ + "//:auto_value", + "@maven//:com_google_guava_guava", + ], +) diff --git a/bundle/src/main/java/dev/cel/bundle/CelBuilder.java b/bundle/src/main/java/dev/cel/bundle/CelBuilder.java index eb54c2ff6..f603b479f 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelBuilder.java +++ b/bundle/src/main/java/dev/cel/bundle/CelBuilder.java @@ -22,8 +22,10 @@ import com.google.protobuf.Descriptors.FileDescriptor; import com.google.protobuf.ExtensionRegistry; import com.google.protobuf.Message; +import dev.cel.checker.CelStandardDeclarations; import dev.cel.checker.ProtoTypeMask; import dev.cel.checker.TypeProvider; +import dev.cel.common.CelContainer; import dev.cel.common.CelFunctionDecl; import dev.cel.common.CelOptions; import dev.cel.common.CelVarDecl; @@ -33,8 +35,9 @@ import dev.cel.compiler.CelCompilerLibrary; import dev.cel.parser.CelMacro; import dev.cel.parser.CelStandardMacro; -import dev.cel.runtime.CelRuntime; +import dev.cel.runtime.CelFunctionBinding; import dev.cel.runtime.CelRuntimeLibrary; +import dev.cel.runtime.CelStandardFunctions; import java.util.function.Function; /** Interface for building an instance of Cel. */ @@ -80,12 +83,15 @@ public interface CelBuilder { @CanIgnoreReturnValue CelBuilder addMacros(Iterable macros); + /** Retrieves the currently configured {@link CelContainer} in the builder. */ + CelContainer container(); + /** - * Set the {@code container} name to use as the namespace for resolving CEL expression variables - * and functions. + * Set the {@link CelContainer} to use as the namespace for resolving CEL expression variables and + * functions. */ @CanIgnoreReturnValue - CelBuilder setContainer(String container); + CelBuilder setContainer(CelContainer container); /** Add a variable declaration with a given {@code name} and {@link Type}. */ @CanIgnoreReturnValue @@ -144,20 +150,28 @@ public interface CelBuilder { CelBuilder addProtoTypeMasks(Iterable typeMasks); /** - * Add one or more {@link CelRuntime.CelFunctionBinding} objects to the CEL runtime. + * Add one or more {@link CelFunctionBinding} objects to the CEL runtime. * *

Functions with duplicate overload ids will be replaced in favor of the new overload. */ @CanIgnoreReturnValue - CelBuilder addFunctionBindings(CelRuntime.CelFunctionBinding... bindings); + CelBuilder addFunctionBindings(CelFunctionBinding... bindings); /** - * Bind a collection of {@link CelRuntime.CelFunctionBinding} objects to the runtime. + * Bind a collection of {@link CelFunctionBinding} objects to the runtime. * *

Functions with duplicate overload ids will be replaced in favor of the new overload. */ @CanIgnoreReturnValue - CelBuilder addFunctionBindings(Iterable bindings); + 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 @@ -295,6 +309,24 @@ public interface CelBuilder { @CanIgnoreReturnValue CelBuilder addRuntimeLibraries(Iterable libraries); + /** + * 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. + */ + @CanIgnoreReturnValue + CelBuilder setStandardDeclarations(CelStandardDeclarations standardDeclarations); + + /** + * Override the standard functions for the runtime. This can be used to subset the standard + * environment to only expose the desired function overloads to the runtime. + * + *

{@link #setStandardEnvironmentEnabled(boolean)} must be set to false for this to take + * effect. + */ + @CanIgnoreReturnValue + CelBuilder setStandardFunctions(CelStandardFunctions standardFunctions); + /** * Sets a proto ExtensionRegistry to assist with unpacking Any messages containing a proto2 extension field. diff --git a/bundle/src/main/java/dev/cel/bundle/CelEnvironment.java b/bundle/src/main/java/dev/cel/bundle/CelEnvironment.java new file mode 100644 index 000000000..ccbaef61b --- /dev/null +++ b/bundle/src/main/java/dev/cel/bundle/CelEnvironment.java @@ -0,0 +1,1112 @@ +// 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.bundle; + +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.ImmutableSet.toImmutableSet; + +import com.google.auto.value.AutoValue; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import com.google.errorprone.annotations.CheckReturnValue; +import dev.cel.bundle.CelEnvironment.LibrarySubset.FunctionSelector; +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; +import dev.cel.common.CelOverloadDecl; +import dev.cel.common.CelVarDecl; +import dev.cel.common.Source; +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.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 + * object is amenable to being serialized into YAML, textproto or other formats as needed. + */ +@AutoValue +public abstract class CelEnvironment { + + @VisibleForTesting + static final ImmutableMap CEL_EXTENSION_CONFIG_MAP = + ImmutableMap.of( + "bindings", CanonicalCelExtension.BINDINGS, + "encoders", CanonicalCelExtension.ENCODERS, + "lists", CanonicalCelExtension.LISTS, + "math", CanonicalCelExtension.MATH, + "optional", CanonicalCelExtension.OPTIONAL, + "protos", CanonicalCelExtension.PROTOS, + "regex", CanonicalCelExtension.REGEX, + "sets", CanonicalCelExtension.SETS, + "strings", CanonicalCelExtension.STRINGS, + "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); + + 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(); + + /** Name of the environment. */ + public abstract String name(); + + /** 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 + * content). + */ + public abstract String description(); + + /** Converts this {@code CelEnvironment} object into a builder. */ + public abstract Builder toBuilder(); + + /** + * Canonical extensions to enable in the environment, such as Optional, String and Math + * extensions. + */ + public abstract ImmutableSet extensions(); + + /** New variable declarations to add in the compilation environment. */ + public abstract ImmutableSet variables(); + + /** New function declarations to add in the compilation environment. */ + public abstract ImmutableSet functions(); + + /** 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 { + + public abstract ImmutableSet.Builder extensionsBuilder(); + + // For testing only, to empty out the source. + abstract Builder setSource(Optional source); + + public abstract Builder setSource(Source source); + + public abstract Builder setName(String name); + + public abstract Builder setDescription(String description); + + public abstract Builder setContainer(CelContainer container); + + @CanIgnoreReturnValue + public Builder setContainer(String container) { + return setContainer(CelContainer.ofName(container)); + } + + @CanIgnoreReturnValue + public Builder addExtensions(ExtensionConfig... extensions) { + checkNotNull(extensions); + return addExtensions(Arrays.asList(extensions)); + } + + @CanIgnoreReturnValue + public Builder addExtensions(Iterable extensions) { + checkNotNull(extensions); + this.extensionsBuilder().addAll(extensions); + return this; + } + + @CanIgnoreReturnValue + public Builder setVariables(VariableDecl... variables) { + return setVariables(ImmutableSet.copyOf(variables)); + } + + public abstract Builder setVariables(ImmutableSet variables); + + @CanIgnoreReturnValue + public Builder setFunctions(FunctionDecl... functions) { + return setFunctions(ImmutableSet.copyOf(functions)); + } + + public abstract Builder setFunctions(ImmutableSet 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 + public final CelEnvironment build() { + CelEnvironment env = autoBuild(); + LibrarySubset librarySubset = env.standardLibrarySubset().orElse(null); + if (librarySubset != null) { + if (!librarySubset.includedMacros().isEmpty() + && !librarySubset.excludedMacros().isEmpty()) { + throw new IllegalArgumentException( + "Invalid subset: cannot both include and exclude macros"); + } + if (!librarySubset.includedFunctions().isEmpty() + && !librarySubset.excludedFunctions().isEmpty()) { + throw new IllegalArgumentException( + "Invalid subset: cannot both include and exclude functions"); + } + } + return env; + } + } + + /** Creates a new builder to construct a {@link CelEnvironment} instance. */ + public static Builder newBuilder() { + return new AutoValue_CelEnvironment.Builder() + .setName("") + .setDescription("") + .setVariables(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() + .setOptions(celOptions) + .setTypeProvider(celTypeProvider) + .addVarDeclarations( + variables().stream() + .map(v -> v.toCelVarDecl(celTypeProvider)) + .collect(toImmutableList())) + .addFunctionDeclarations( + functions().stream() + .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); + } + } + + /** 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); + + CelRuntime celRuntime = extendRuntime(cel, celOptions); + + 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 + for (ExtensionConfig extensionConfig : extensions()) { + CanonicalCelExtension extension = getExtensionOrThrow(extensionConfig.name()); + if (extension.compilerExtensionProvider() != null) { + CelCompilerLibrary celCompilerLibrary = + extension + .compilerExtensionProvider() + .getCelCompilerLibrary(celOptions, extensionConfig.version()); + celCompilerBuilder.addLibraries(celCompilerLibrary); + } + } + } + + 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()); + if (extension.runtimeExtensionProvider() != null) { + CelRuntimeLibrary celRuntimeLibrary = + extension + .runtimeExtensionProvider() + .getCelRuntimeLibrary(celOptions, extensionConfig.version()); + celRuntimeBuilder.addLibraries(celRuntimeLibrary); + } + } + return celRuntimeBuilder.build(); + } + + private void applyStandardLibrarySubset(CelCompilerBuilder compilerBuilder) { + if (!standardLibrarySubset().isPresent()) { + return; + } + + LibrarySubset librarySubset = standardLibrarySubset().get(); + if (librarySubset.disabled()) { + compilerBuilder.setStandardEnvironmentEnabled(false); + return; + } + + if (librarySubset.macrosDisabled()) { + compilerBuilder.setStandardMacros(ImmutableList.of()); + } else if (!librarySubset.includedMacros().isEmpty()) { + compilerBuilder.setStandardMacros( + librarySubset.includedMacros().stream() + .flatMap(name -> getStandardMacrosOrThrow(name).stream()) + .collect(toImmutableSet())); + } else if (!librarySubset.excludedMacros().isEmpty()) { + ImmutableSet set = + librarySubset.excludedMacros().stream() + .flatMap(name -> getStandardMacrosOrThrow(name).stream()) + .collect(toImmutableSet()); + compilerBuilder.setStandardMacros( + CelStandardMacro.STANDARD_MACROS.stream() + .filter(macro -> !set.contains(macro)) + .collect(toImmutableSet())); + } + + if (!librarySubset.includedFunctions().isEmpty()) { + ImmutableSet includedFunctions = librarySubset.includedFunctions(); + compilerBuilder + .setStandardEnvironmentEnabled(false) + .setStandardDeclarations( + CelStandardDeclarations.newBuilder() + .filterFunctions( + (function, overload) -> + FunctionSelector.matchesAny(function, overload, includedFunctions)) + .build()); + } else if (!librarySubset.excludedFunctions().isEmpty()) { + ImmutableSet excludedFunctions = librarySubset.excludedFunctions(); + compilerBuilder + .setStandardEnvironmentEnabled(false) + .setStandardDeclarations( + CelStandardDeclarations.newBuilder() + .filterFunctions( + (function, overload) -> + !FunctionSelector.matchesAny(function, overload, excludedFunctions)) + .build()); + } + } + + private static ImmutableSet getStandardMacrosOrThrow(String macroName) { + ImmutableSet.Builder builder = ImmutableSet.builder(); + for (CelStandardMacro macro : CelStandardMacro.STANDARD_MACROS) { + if (macro.getFunction().equals(macroName)) { + builder.add(macro); + } + } + ImmutableSet macros = builder.build(); + if (macros.isEmpty()) { + throw new IllegalArgumentException("unrecognized standard macro `" + macroName + "'"); + } + return macros; + } + + private static CanonicalCelExtension getExtensionOrThrow(String extensionName) { + CanonicalCelExtension extension = CEL_EXTENSION_CONFIG_MAP.get(extensionName); + if (extension == null) { + throw new IllegalArgumentException("Unrecognized extension: " + 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 { + + /** Fully qualified variable name. */ + public abstract String name(); + + /** 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 { + + public abstract Optional name(); + + public abstract Optional type(); + + public abstract VariableDecl.Builder setName(String name); + + public abstract VariableDecl.Builder setType(TypeDecl typeDecl); + + public abstract VariableDecl.Builder setDescription(String name); + + @Override + public ImmutableList requiredFields() { + return ImmutableList.of( + RequiredField.of("name", this::name), RequiredField.of("type", this::type)); + } + + /** Builds a new instance of {@link VariableDecl}. */ + public abstract VariableDecl build(); + } + + public static VariableDecl.Builder newBuilder() { + return new AutoValue_CelEnvironment_VariableDecl.Builder(); + } + + /** Creates a new builder to construct a {@link VariableDecl} instance. */ + public static VariableDecl create(String name, TypeDecl type) { + return newBuilder().setName(name).setType(type).build(); + } + + /** Converts this policy variable declaration into a {@link CelVarDecl}. */ + public CelVarDecl toCelVarDecl(CelTypeProvider celTypeProvider) { + return CelVarDecl.newVarDeclaration(name(), type().toCelType(celTypeProvider)); + } + } + + /** Represents a policy function declaration. */ + @AutoValue + public abstract static class FunctionDecl { + + public abstract String name(); + + public abstract Optional description(); + + public abstract ImmutableSet overloads(); + + /** Builder for {@link FunctionDecl}. */ + @AutoValue.Builder + public abstract static class Builder implements RequiredFieldsChecker { + + public abstract Optional name(); + + public abstract Optional> overloads(); + + public abstract FunctionDecl.Builder setName(String name); + + public abstract FunctionDecl.Builder setDescription(String description); + + public abstract FunctionDecl.Builder setOverloads(ImmutableSet overloads); + + @Override + public ImmutableList requiredFields() { + return ImmutableList.of( + RequiredField.of("name", this::name), RequiredField.of("overloads", this::overloads)); + } + + /** Builds a new instance of {@link FunctionDecl}. */ + public abstract FunctionDecl build(); + } + + /** Creates a new builder to construct a {@link FunctionDecl} instance. */ + public static FunctionDecl.Builder newBuilder() { + return new AutoValue_CelEnvironment_FunctionDecl.Builder(); + } + + /** Creates a new {@link FunctionDecl} with the provided function name and its overloads. */ + public static FunctionDecl create(String name, ImmutableSet overloads) { + return newBuilder().setName(name).setOverloads(overloads).build(); + } + + /** Converts this policy function declaration into a {@link CelFunctionDecl}. */ + public CelFunctionDecl toCelFunctionDecl(CelTypeProvider celTypeProvider) { + return CelFunctionDecl.newFunctionDeclaration( + name(), + overloads().stream() + .map(o -> o.toCelOverloadDecl(celTypeProvider)) + .collect(toImmutableList())); + } + } + + /** Represents an overload declaration on a policy function. */ + @AutoValue + public abstract static class OverloadDecl { + + /** + * A unique overload ID. Required. This should follow the typical naming convention used in CEL + * (e.g: targetType_func_argType1_argType...) + */ + public abstract String id(); + + /** Target of the function overload if it's a receiver style (example: foo in `foo.f(...)`) */ + public abstract Optional target(); + + /** 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(); + + /** Builder for {@link OverloadDecl}. */ + @AutoValue.Builder + public abstract static class Builder implements RequiredFieldsChecker { + + public abstract Optional id(); + + public abstract Optional returnType(); + + public abstract OverloadDecl.Builder setId(String overloadId); + + public abstract OverloadDecl.Builder setTarget(TypeDecl target); + + // 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)); + return this; + } + + @CanIgnoreReturnValue + public OverloadDecl.Builder addArguments(TypeDecl... args) { + return addArguments(Arrays.asList(args)); + } + + public abstract OverloadDecl.Builder setReturnType(TypeDecl returnType); + + @Override + public ImmutableList requiredFields() { + return ImmutableList.of( + RequiredField.of("id", this::id), RequiredField.of("return", this::returnType)); + } + + /** Builds a new instance of {@link OverloadDecl}. */ + @CheckReturnValue + public abstract OverloadDecl build(); + } + + /** Creates a new builder to construct a {@link OverloadDecl} instance. */ + public static OverloadDecl.Builder newBuilder() { + return new AutoValue_CelEnvironment_OverloadDecl.Builder().setArguments(ImmutableList.of()); + } + + /** Converts this policy function overload into a {@link CelOverloadDecl}. */ + public CelOverloadDecl toCelOverloadDecl(CelTypeProvider celTypeProvider) { + CelOverloadDecl.Builder builder = + CelOverloadDecl.newBuilder() + .setIsInstanceFunction(false) + .setOverloadId(id()) + .setResultType(returnType().toCelType(celTypeProvider)); + + target() + .ifPresent( + t -> + builder + .setIsInstanceFunction(true) + .addParameterTypes(t.toCelType(celTypeProvider))); + + for (TypeDecl type : arguments()) { + builder.addParameterTypes(type.toCelType(celTypeProvider)); + } + + return builder.build(); + } + } + + /** + * Represents an abstract type declaration used to declare functions and variables in a policy. + */ + @AutoValue + public abstract static class TypeDecl { + + public abstract String name(); + + public abstract ImmutableList params(); + + public abstract boolean isTypeParam(); + + /** Builder for {@link TypeDecl}. */ + @AutoValue.Builder + public abstract static class Builder implements RequiredFieldsChecker { + + public abstract Optional name(); + + public abstract TypeDecl.Builder setName(String name); + + // This should stay package-private to encourage add/set methods to be used instead. + abstract ImmutableList.Builder paramsBuilder(); + + public abstract TypeDecl.Builder setParams(ImmutableList typeDecls); + + @CanIgnoreReturnValue + public TypeDecl.Builder addParams(TypeDecl... params) { + return addParams(Arrays.asList(params)); + } + + @CanIgnoreReturnValue + public TypeDecl.Builder addParams(Iterable params) { + this.paramsBuilder().addAll(checkNotNull(params)); + return this; + } + + public abstract TypeDecl.Builder setIsTypeParam(boolean isTypeParam); + + @Override + public ImmutableList requiredFields() { + return ImmutableList.of(RequiredField.of("type_name", this::name)); + } + + @CheckReturnValue + public abstract TypeDecl build(); + } + + /** Creates a new {@link TypeDecl} with the provided name. */ + public static TypeDecl create(String name) { + return newBuilder().setName(name).build(); + } + + public static TypeDecl.Builder newBuilder() { + return new AutoValue_CelEnvironment_TypeDecl.Builder().setIsTypeParam(false); + } + + /** Converts this type declaration into a {@link CelType}. */ + public CelType toCelType(CelTypeProvider celTypeProvider) { + switch (name()) { + case "list": + if (params().size() != 1) { + throw new IllegalArgumentException( + "List type has unexpected param count: " + params().size()); + } + + CelType elementType = params().get(0).toCelType(celTypeProvider); + return ListType.create(elementType); + case "map": + if (params().size() != 2) { + throw new IllegalArgumentException( + "Map type has unexpected param count: " + params().size()); + } + + 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; + } + + if (name().equals(OptionalType.NAME)) { + checkState( + params().size() == 1, + "Optional type must have exactly 1 parameter. Found %s", + params().size()); + return OptionalType.create(params().get(0).toCelType(celTypeProvider)); + } + + return celTypeProvider + .findType(name()) + .orElseThrow(() -> new IllegalArgumentException("Undefined type name: " + name())); + } + } + } + + /** 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. + */ + @AutoValue + public abstract static class ExtensionConfig { + + /** Name of the extension (ex: bindings, optional, math, etc).". */ + public abstract String name(); + + /** + * Version of the extension. Presently, this field is ignored as CEL-Java extensions are not + * versioned. + */ + public abstract int version(); + + /** Builder for {@link ExtensionConfig}. */ + @AutoValue.Builder + public abstract static class Builder implements RequiredFieldsChecker { + + public abstract Optional name(); + + public abstract Optional version(); + + public abstract ExtensionConfig.Builder setName(String name); + + public abstract ExtensionConfig.Builder setVersion(int version); + + @Override + public ImmutableList requiredFields() { + return ImmutableList.of(RequiredField.of("name", this::name)); + } + + /** Builds a new instance of {@link ExtensionConfig}. */ + public abstract ExtensionConfig build(); + } + + /** Creates a new builder to construct a {@link ExtensionConfig} instance. */ + public static ExtensionConfig.Builder newBuilder() { + return new AutoValue_CelEnvironment_ExtensionConfig.Builder().setVersion(0); + } + + /** Create a new extension config with the specified name and version set to 0. */ + public static ExtensionConfig of(String name) { + return of(name, 0); + } + + /** Create a new extension config with the specified name and version. */ + public static ExtensionConfig of(String name, int version) { + return newBuilder().setName(name).setVersion(version).build(); + } + + /** Create a new extension config with the specified name and the latest version. */ + public static ExtensionConfig latest(String name) { + return of(name, Integer.MAX_VALUE); + } + } + + @AutoValue + abstract static class Alias { + abstract String alias(); + + abstract String qualifiedName(); + + static Builder newBuilder() { + return new AutoValue_CelEnvironment_Alias.Builder(); + } + + @AutoValue.Builder + abstract static class Builder implements RequiredFieldsChecker { + + abstract Optional alias(); + + abstract Optional qualifiedName(); + + abstract Builder setAlias(String alias); + + abstract Builder setQualifiedName(String qualifiedName); + + abstract Alias build(); + + @Override + public ImmutableList requiredFields() { + return ImmutableList.of( + RequiredField.of("alias", this::alias), + RequiredField.of("qualified_name", this::qualifiedName)); + } + } + } + + @VisibleForTesting + enum CanonicalCelExtension { + BINDINGS((options, version) -> CelExtensions.bindings()), + PROTOS((options, version) -> CelExtensions.protos()), + ENCODERS( + (options, version) -> CelExtensions.encoders(options), + (options, version) -> CelExtensions.encoders(options)), + MATH( + (options, version) -> CelExtensions.math(options, version), + (options, version) -> CelExtensions.math(options, version)), + OPTIONAL( + (options, version) -> CelExtensions.optional(version), + (options, version) -> CelExtensions.optional(version)), + STRINGS( + (options, version) -> CelExtensions.strings(), + (options, version) -> CelExtensions.strings()), + 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(), + (options, version) -> CelExtensions.comprehensions()); + + @SuppressWarnings("ImmutableEnumChecker") + private final CompilerExtensionProvider compilerExtensionProvider; + + @SuppressWarnings("ImmutableEnumChecker") + private final RuntimeExtensionProvider runtimeExtensionProvider; + + interface CompilerExtensionProvider { + CelCompilerLibrary getCelCompilerLibrary(CelOptions options, int version); + } + + interface RuntimeExtensionProvider { + CelRuntimeLibrary getCelRuntimeLibrary(CelOptions options, int version); + } + + CompilerExtensionProvider compilerExtensionProvider() { + return compilerExtensionProvider; + } + + RuntimeExtensionProvider runtimeExtensionProvider() { + return runtimeExtensionProvider; + } + + CanonicalCelExtension(CompilerExtensionProvider compilerExtensionProvider) { + this.compilerExtensionProvider = compilerExtensionProvider; + this.runtimeExtensionProvider = null; // Not all extensions augment the runtime. + } + + CanonicalCelExtension( + CompilerExtensionProvider compilerExtensionProvider, + RuntimeExtensionProvider runtimeExtensionProvider) { + this.compilerExtensionProvider = compilerExtensionProvider; + this.runtimeExtensionProvider = runtimeExtensionProvider; + } + } + + /** + * LibrarySubset indicates a subset of the macros and function supported by a subsettable library. + */ + @AutoValue + public abstract static class LibrarySubset { + + /** + * Disabled indicates whether the library has been disabled, typically only used for + * default-enabled libraries like stdlib. + */ + public abstract boolean disabled(); + + /** DisableMacros disables macros for the given library. */ + public abstract boolean macrosDisabled(); + + /** IncludeMacros specifies a set of macro function names to include in the subset. */ + public abstract ImmutableSet includedMacros(); + + /** + * ExcludeMacros specifies a set of macro function names to exclude from the subset. + * + *

Note: if IncludedMacros is non-empty, then ExcludedMacros is ignored. + */ + public abstract ImmutableSet excludedMacros(); + + /** + * IncludeFunctions specifies a set of functions to include in the subset. + * + *

Note: the overloads specified in the subset need only specify their ID. + * + *

Note: if IncludedFunctions is non-empty, then ExcludedFunctions is ignored. + */ + public abstract ImmutableSet includedFunctions(); + + /** + * ExcludeFunctions specifies the set of functions to exclude from the subset. + * + *

Note: the overloads specified in the subset need only specify their ID. + */ + public abstract ImmutableSet excludedFunctions(); + + public static Builder newBuilder() { + return new AutoValue_CelEnvironment_LibrarySubset.Builder() + .setMacrosDisabled(false) + .setIncludedMacros(ImmutableSet.of()) + .setExcludedMacros(ImmutableSet.of()) + .setIncludedFunctions(ImmutableSet.of()) + .setExcludedFunctions(ImmutableSet.of()); + } + + /** Builder for {@link LibrarySubset}. */ + @AutoValue.Builder + public abstract static class Builder { + + public abstract Builder setDisabled(boolean disabled); + + public abstract Builder setMacrosDisabled(boolean disabled); + + public abstract Builder setIncludedMacros(ImmutableSet includedMacros); + + public abstract Builder setExcludedMacros(ImmutableSet excludedMacros); + + public abstract Builder setIncludedFunctions( + ImmutableSet includedFunctions); + + public abstract Builder setExcludedFunctions( + ImmutableSet excludedFunctions); + + @CheckReturnValue + public abstract LibrarySubset build(); + } + + /** + * Represents a function selector, which can be used to configure included/excluded library + * functions. + */ + @AutoValue + public abstract static class FunctionSelector { + + public abstract String name(); + + public abstract ImmutableSet overloads(); + + /** Builder for {@link FunctionSelector}. */ + @AutoValue.Builder + public abstract static class Builder implements RequiredFieldsChecker { + + public abstract Optional name(); + + public abstract Builder setName(String name); + + public abstract Builder setOverloads(ImmutableSet overloads); + + @Override + public ImmutableList requiredFields() { + return ImmutableList.of(RequiredField.of("name", this::name)); + } + + /** Builds a new instance of {@link FunctionSelector}. */ + public abstract FunctionSelector build(); + } + + /** Creates a new builder to construct a {@link FunctionSelector} instance. */ + public static FunctionSelector.Builder newBuilder() { + return new AutoValue_CelEnvironment_LibrarySubset_FunctionSelector.Builder() + .setOverloads(ImmutableSet.of()); + } + + public static FunctionSelector create(String name, ImmutableSet overloads) { + return newBuilder() + .setName(name) + .setOverloads( + overloads.stream() + .map(id -> OverloadSelector.newBuilder().setId(id).build()) + .collect(toImmutableSet())) + .build(); + } + + private static boolean matchesAny( + StandardFunction function, + StandardOverload overload, + ImmutableSet selectors) { + String functionName = function.functionDecl().name(); + for (FunctionSelector functionSelector : selectors) { + if (!functionSelector.name().equals(functionName)) { + continue; + } + + if (functionSelector.overloads().isEmpty()) { + return true; + } + + String overloadId = overload.celOverloadDecl().overloadId(); + for (OverloadSelector overloadSelector : functionSelector.overloads()) { + if (overloadSelector.id().equals(overloadId)) { + return true; + } + } + } + return false; + } + } + + /** Represents an overload selector on a function selector. */ + @AutoValue + public abstract static class OverloadSelector { + + /** An overload ID. Required. Follows the same format as {@link OverloadDecl#id()} */ + public abstract String id(); + + /** Builder for {@link OverloadSelector}. */ + @AutoValue.Builder + public abstract static class Builder implements RequiredFieldsChecker { + + public abstract Optional id(); + + public abstract Builder setId(String overloadId); + + @Override + public ImmutableList requiredFields() { + return ImmutableList.of(RequiredField.of("id", this::id)); + } + + /** Builds a new instance of {@link OverloadSelector}. */ + @CheckReturnValue + public abstract OverloadSelector build(); + } + + /** Creates a new builder to construct a {@link OverloadSelector} instance. */ + public static OverloadSelector.Builder newBuilder() { + return new AutoValue_CelEnvironment_LibrarySubset_OverloadSelector.Builder(); + } + } + } + + @FunctionalInterface + private interface BooleanOptionConsumer { + void accept(CelOptions.Builder options, boolean value); + } +} diff --git a/bundle/src/main/java/dev/cel/bundle/CelEnvironmentException.java b/bundle/src/main/java/dev/cel/bundle/CelEnvironmentException.java new file mode 100644 index 000000000..58bb5cdb9 --- /dev/null +++ b/bundle/src/main/java/dev/cel/bundle/CelEnvironmentException.java @@ -0,0 +1,29 @@ +// 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.bundle; + +import dev.cel.common.CelException; + +/** Checked exception thrown when a CEL environment is misconfigured. */ +public final class CelEnvironmentException extends CelException { + + CelEnvironmentException(String message) { + super(message); + } + + CelEnvironmentException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/bundle/src/main/java/dev/cel/bundle/CelEnvironmentExporter.java b/bundle/src/main/java/dev/cel/bundle/CelEnvironmentExporter.java new file mode 100644 index 000000000..d233fd36f --- /dev/null +++ b/bundle/src/main/java/dev/cel/bundle/CelEnvironmentExporter.java @@ -0,0 +1,520 @@ +// 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.bundle; + +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.collect.ImmutableSet.toImmutableSet; +import static java.util.Arrays.stream; + +import dev.cel.expr.Decl; +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; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import dev.cel.bundle.CelEnvironment.ExtensionConfig; +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; +import dev.cel.common.CelOptions; +import dev.cel.common.CelOverloadDecl; +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.compiler.CelCompiler; +import dev.cel.extensions.CelExtensionLibrary; +import dev.cel.extensions.CelExtensions; +import dev.cel.parser.CelMacro; +import dev.cel.parser.CelStandardMacro; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * {@code CelEnvironmentExporter} can be used to export the configuration of a {@link Cel} instance + * as a {@link CelEnvironment} object. + * + *

This exporter captures details such as: + * + *

    + *
  • Standard library subset: Functions and their overloads that are either included or + * excluded. + *
  • Extension libraries: Names and versions of the extension libraries in use. + *
  • Custom declarations: Functions and variables not part of standard or extension libraries. + *
+ * + *

The exporter provides options to control the behavior of the export process, such as the + * maximum number of excluded standard functions and overloads before switching to an inclusion + * strategy. + */ +@AutoValue +public abstract class CelEnvironmentExporter { + + /** + * Maximum number of excluded standard functions and macros before switching to a format that + * enumerates all included functions and macros. The default is 5. + * + *

This setting is primarily for stylistic preferences and the intended use of the resulting + * YAML file. + * + *

For example, if you want almost all the standard library with only a few exceptions (e.g., + * to ban a specific function), you would favor an exclusion-based approach by setting an + * appropriate threshold. + * + *

If you want full control over what is available to the CEL runtime, where no function is + * included unless fully vetted, you would favor an inclusion-based approach by setting the + * threshold to 0. This may result in a more verbose YAML file. + */ + abstract int maxExcludedStandardFunctions(); + + /** + * Maximum number of excluded standard function overloads before switching to a exhaustive + * enumeration of included overloads. The default is 15. + */ + abstract int maxExcludedStandardFunctionOverloads(); + + abstract ImmutableSet> + extensionLibraries(); + + /** Builder for {@link CelEnvironmentExporter}. */ + @AutoValue.Builder + public abstract static class Builder { + + public abstract Builder setMaxExcludedStandardFunctions(int count); + + public abstract Builder setMaxExcludedStandardFunctionOverloads(int count); + + abstract ImmutableSet.Builder> + extensionLibrariesBuilder(); + + @CanIgnoreReturnValue + public Builder addStandardExtensions(CelOptions options) { + addExtensionLibraries( + CelExtensions.getExtensionLibrary("bindings", options), + CelExtensions.getExtensionLibrary("encoders", options), + CelExtensions.getExtensionLibrary("lists", options), + CelExtensions.getExtensionLibrary("math", options), + CelExtensions.getExtensionLibrary("protos", options), + CelExtensions.getExtensionLibrary("regex", options), + CelExtensions.getExtensionLibrary("sets", options), + CelExtensions.getExtensionLibrary("strings", options)); + // TODO: add support for remaining standard extensions + return this; + } + + @CanIgnoreReturnValue + public Builder addExtensionLibraries( + CelExtensionLibrary... libraries) { + extensionLibrariesBuilder().add(libraries); + return this; + } + + abstract CelEnvironmentExporter autoBuild(); + + public CelEnvironmentExporter build() { + return autoBuild(); + } + } + + /** Creates a new builder to construct a {@link CelEnvironmentExporter} instance. */ + public static CelEnvironmentExporter.Builder newBuilder() { + return new AutoValue_CelEnvironmentExporter.Builder() + .setMaxExcludedStandardFunctions(5) + .setMaxExcludedStandardFunctionOverloads(15); + } + + /** + * Exports a {@link CelEnvironment} that describes the configuration of the given {@link Cel} + * 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(Cel cel) { + 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. + // + // Once we have the inventory, we attempt to identify sources of these declarations as + // standard library and extensions. The identified subsets will be removed from the inventory. + // + // 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); + addStandardLibrarySubsetAndRemoveFromInventory(envBuilder, inventory); + addCustomDecls(envBuilder, inventory); + 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())); + } + 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, CelCompiler cel) { + Preconditions.checkArgument(cel instanceof EnvVisitable); + ((EnvVisitable) cel) + .accept( + new EnvVisitor() { + @Override + public void visitDecl(String name, List decls) { + for (Decl decl : decls) { + if (decl.hasFunction()) { + FunctionDecl function = decl.getFunction(); + for (Overload overload : function.getOverloadsList()) { + inventory.add( + NamedOverload.create( + decl.getName(), CelOverloadDecl.overloadToCelOverload(overload))); + } + } else if (decl.hasIdent()) { + inventory.add( + CelVarDecl.newVarDeclaration( + decl.getName(), + CelProtoTypes.typeToCelType(decl.getIdent().getType()))); + } + } + } + + @Override + public void visitMacro(CelMacro macro) { + inventory.add(macro); + } + }); + } + + /** + * Iterates through the available extension libraries, checks if they are included in the + * inventory, and adds them to the environment builder. Only the highest version of a library is + * added to the builder. If the extension is identified, all corresponding items are removed from + * the inventory. + */ + private void addExtensionConfigsAndRemoveFromInventory( + CelEnvironment.Builder envBuilder, Set inventory) { + ArrayList featureSets = new ArrayList<>(); + + for (CelExtensionLibrary extensionLibrary : + extensionLibraries()) { + for (CelExtensionLibrary.FeatureSet featureSet : extensionLibrary.versions()) { + featureSets.add(NamedFeatureSet.create(extensionLibrary.name(), featureSet)); + } + } + + featureSets.sort( + Comparator.comparing(NamedFeatureSet::name) + .thenComparing(nfs -> nfs.featureSet().version()) + .reversed()); + + Set includedExtensions = new HashSet<>(); + for (NamedFeatureSet lib : featureSets) { + if (includedExtensions.contains(lib.name())) { + // We only need to infer the highest version library, so we can skip lower versions + continue; + } + + if (checkIfExtensionIsIncludedAndRemoveFromInventory(inventory, lib.featureSet())) { + envBuilder.addExtensions(ExtensionConfig.of(lib.name(), lib.featureSet().version())); + includedExtensions.add(lib.name()); + } + } + } + + private boolean checkIfExtensionIsIncludedAndRemoveFromInventory( + Set inventory, CelExtensionLibrary.FeatureSet featureSet) { + ImmutableSet functions = featureSet.functions(); + ArrayList includedFeatures = new ArrayList<>(functions.size()); + for (CelFunctionDecl function : functions) { + for (CelOverloadDecl overload : function.overloads()) { + NamedOverload feature = NamedOverload.create(function.name(), overload); + if (!inventory.contains(feature)) { + return false; + } + includedFeatures.add(feature); + } + } + + ImmutableSet macros = featureSet.macros(); + for (CelMacro macro : macros) { + if (!inventory.contains(macro)) { + return false; + } + includedFeatures.add(macro); + } + + // TODO - Add checks for variables. + + inventory.removeAll(includedFeatures); + return true; + } + + private void addStandardLibrarySubsetAndRemoveFromInventory( + CelEnvironment.Builder envBuilder, Set inventory) { + // Claim standard identifiers for the standard library + for (StandardIdentifier value : StandardIdentifier.values()) { + inventory.remove( + CelVarDecl.newVarDeclaration(value.identDecl().name(), value.identDecl().type())); + } + + Set excludedFunctions = new HashSet<>(); + Set includedFunctions = new HashSet<>(); + ListMultimap excludedOverloads = ArrayListMultimap.create(); + ListMultimap includedOverloads = ArrayListMultimap.create(); + + stream(StandardFunction.values()) + .map(StandardFunction::functionDecl) + .forEach( + decl -> { + String functionName = decl.name(); + boolean anyOverloadIncluded = false; + boolean allOverloadsIncluded = true; + for (CelOverloadDecl overload : decl.overloads()) { + NamedOverload item = NamedOverload.create(functionName, overload); + if (inventory.remove(item)) { + anyOverloadIncluded = true; + includedOverloads.put(functionName, overload.overloadId()); + } else { + allOverloadsIncluded = false; + excludedOverloads.put(functionName, overload.overloadId()); + } + } + if (!anyOverloadIncluded) { + excludedFunctions.add(functionName); + } + if (allOverloadsIncluded) { + includedFunctions.add(functionName); + } + }); + + Set excludedMacros = new HashSet<>(); + Set includedMacros = new HashSet<>(); + stream(CelStandardMacro.values()) + .map(celStandardMacro -> celStandardMacro.getDefinition()) + .forEach( + macro -> { + if (inventory.remove(macro)) { + includedMacros.add(macro.getFunction()); + } else { + excludedMacros.add(macro.getFunction()); + } + }); + + LibrarySubset.Builder subsetBuilder = LibrarySubset.newBuilder().setDisabled(false); + if (excludedFunctions.size() + excludedMacros.size() <= maxExcludedStandardFunctions() + && excludedOverloads.size() <= maxExcludedStandardFunctionOverloads()) { + subsetBuilder + .setExcludedFunctions(buildFunctionSelectors(excludedFunctions, excludedOverloads)) + .setExcludedMacros(ImmutableSet.copyOf(excludedMacros)); + } else { + subsetBuilder + .setIncludedFunctions(buildFunctionSelectors(includedFunctions, includedOverloads)) + .setIncludedMacros(ImmutableSet.copyOf(includedMacros)); + } + + envBuilder.setStandardLibrarySubset(subsetBuilder.build()); + } + + private ImmutableSet buildFunctionSelectors( + Set functions, ListMultimap functionToOverloadsMap) { + ImmutableSet.Builder functionSelectors = ImmutableSet.builder(); + for (String excludedFunction : functions) { + functionSelectors.add(FunctionSelector.create(excludedFunction, ImmutableSet.of())); + } + + for (String functionName : functionToOverloadsMap.keySet()) { + if (functions.contains(functionName)) { + continue; + } + functionSelectors.add( + FunctionSelector.create( + functionName, ImmutableSet.copyOf(functionToOverloadsMap.get(functionName)))); + } + return functionSelectors.build(); + } + + private void addCustomDecls(CelEnvironment.Builder envBuilder, Set inventory) { + // Group "orphaned" function overloads and vars by their names + ListMultimap extraOverloads = ArrayListMultimap.create(); + Map extraVars = new HashMap<>(); + for (Object item : inventory) { + if (item instanceof NamedOverload) { + extraOverloads.put( + ((NamedOverload) item).functionName(), ((NamedOverload) item).overload()); + } else if (item instanceof CelVarDecl) { + extraVars.put(((CelVarDecl) item).name(), ((CelVarDecl) item).type()); + } + } + + if (!extraOverloads.isEmpty()) { + ImmutableSet.Builder functionDeclBuilder = + ImmutableSet.builder(); + for (String functionName : extraOverloads.keySet()) { + functionDeclBuilder.add( + CelEnvironment.FunctionDecl.create( + functionName, + extraOverloads.get(functionName).stream() + .map(this::toCelEnvOverloadDecl) + .collect(toImmutableSet()))); + } + envBuilder.setFunctions(functionDeclBuilder.build()); + } + + if (!extraVars.isEmpty()) { + ImmutableSet.Builder varDeclBuilder = ImmutableSet.builder(); + for (String ident : extraVars.keySet()) { + varDeclBuilder.add( + CelEnvironment.VariableDecl.create(ident, toCelEnvTypeDecl(extraVars.get(ident)))); + } + envBuilder.setVariables(varDeclBuilder.build()); + } + } + + private CelEnvironment.OverloadDecl toCelEnvOverloadDecl(CelOverloadDecl overload) { + OverloadDecl.Builder builder = + OverloadDecl.newBuilder() + .setId(overload.overloadId()) + .setReturnType(toCelEnvTypeDecl(overload.resultType())); + + if (overload.isInstanceFunction()) { + builder + .setTarget(toCelEnvTypeDecl(overload.parameterTypes().get(0))) + .setArguments( + overload.parameterTypes().stream() + .skip(1) + .map(this::toCelEnvTypeDecl) + .collect(toImmutableList())); + } else { + builder.setArguments( + overload.parameterTypes().stream() + .map(this::toCelEnvTypeDecl) + .collect(toImmutableList())); + } + return builder.build(); + } + + private CelEnvironment.TypeDecl toCelEnvTypeDecl(CelType 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. */ + @AutoValue + abstract static class NamedOverload { + abstract String functionName(); + + abstract CelOverloadDecl overload(); + + static NamedOverload create(String functionName, CelOverloadDecl overload) { + return new AutoValue_CelEnvironmentExporter_NamedOverload(functionName, overload); + } + } + + /** + * Wrapper for CelExtensionLibrary.FeatureSet, associating it with the corresponding library name. + */ + @AutoValue + abstract static class NamedFeatureSet { + abstract String name(); + + abstract CelExtensionLibrary.FeatureSet featureSet(); + + static NamedFeatureSet create(String name, CelExtensionLibrary.FeatureSet featureSet) { + return new AutoValue_CelEnvironmentExporter_NamedFeatureSet(name, featureSet); + } + } +} diff --git a/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlParser.java b/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlParser.java new file mode 100644 index 000000000..14f1c93d8 --- /dev/null +++ b/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlParser.java @@ -0,0 +1,948 @@ +// 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.bundle; + +import static dev.cel.common.formats.YamlHelper.ERROR; +import static dev.cel.common.formats.YamlHelper.assertRequiredFields; +import static dev.cel.common.formats.YamlHelper.assertYamlType; +import static dev.cel.common.formats.YamlHelper.newBoolean; +import static dev.cel.common.formats.YamlHelper.newInteger; +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; +import dev.cel.bundle.CelEnvironment.LibrarySubset.FunctionSelector; +import dev.cel.bundle.CelEnvironment.LibrarySubset.OverloadSelector; +import dev.cel.bundle.CelEnvironment.OverloadDecl; +import dev.cel.bundle.CelEnvironment.TypeDecl; +import dev.cel.bundle.CelEnvironment.VariableDecl; +import dev.cel.common.CelContainer; +import dev.cel.common.CelIssue; +import dev.cel.common.formats.CelFileSource; +import dev.cel.common.formats.ParserContext; +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; +import org.yaml.snakeyaml.nodes.Node; +import org.yaml.snakeyaml.nodes.NodeTuple; +import org.yaml.snakeyaml.nodes.ScalarNode; +import org.yaml.snakeyaml.nodes.SequenceNode; +import org.yaml.snakeyaml.nodes.Tag; + +/** + * CelEnvironmentYamlParser intakes a YAML document that describes the structure of a CEL + * environment, parses it then creates a {@link CelEnvironment}. + */ +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 VariableDecl ERROR_VARIABLE_DECL = + VariableDecl.create(ERROR, ERROR_TYPE_DECL); + private static final FunctionDecl ERROR_FUNCTION_DECL = + FunctionDecl.create(ERROR, ImmutableSet.of()); + private static final ExtensionConfig ERROR_EXTENSION_DECL = ExtensionConfig.of(ERROR); + private static final FunctionSelector ERROR_FUNCTION_SELECTOR = + FunctionSelector.create(ERROR, ImmutableSet.of()); + private static final Alias ERROR_ALIAS = + Alias.newBuilder().setAlias(ERROR).setQualifiedName(ERROR).build(); + + /** Generates a new instance of {@code CelEnvironmentYamlParser}. */ + public static CelEnvironmentYamlParser newInstance() { + return new CelEnvironmentYamlParser(); + } + + /** Parsers the input {@code environmentYamlSource} and returns a {@link CelEnvironment}. */ + public CelEnvironment parse(String environmentYamlSource) throws CelEnvironmentException { + return parse(environmentYamlSource, ""); + } + + /** + * Parses the input {@code environmentYamlSource} and returns a {@link CelEnvironment}. + * + *

The {@code description} may be used to help tailor error messages for the location where the + * {@code environmentYamlSource} originates, e.g. a file name or form UI element. + */ + public CelEnvironment parse(String environmentYamlSource, String description) + throws CelEnvironmentException { + CelEnvironmentYamlParser.ParserImpl parser = new CelEnvironmentYamlParser.ParserImpl(); + + return parser.parseYaml(environmentYamlSource, description); + } + + private CelContainer parseContainer(ParserContext ctx, Node node) { + long valueId = ctx.collectMetadata(node); + // Syntax variant 1: "container: `str`" + if (validateYamlType(node, YamlNodeType.STRING, YamlNodeType.TEXT)) { + return CelContainer.ofName(newString(ctx, node)); + } + + // Syntax variant 2: + // container + // name: str + // abbreviations: + // - a1 + // - a2 + // aliases: + // - alias: a1 + // qualified_name: q1 + // - alias: a2 + // qualified_name: q2 + if (!assertYamlType(ctx, valueId, node, YamlNodeType.MAP)) { + return CelContainer.ofName(ERROR); + } + + CelContainer.Builder builder = CelContainer.newBuilder(); + MappingNode variableMap = (MappingNode) node; + for (NodeTuple nodeTuple : variableMap.getValue()) { + Node keyNode = nodeTuple.getKeyNode(); + long keyId = ctx.collectMetadata(keyNode); + Node valueNode = nodeTuple.getValueNode(); + String keyName = ((ScalarNode) keyNode).getValue(); + switch (keyName) { + case "name": + builder.setName(newString(ctx, valueNode)); + break; + case "aliases": + ImmutableSet aliases = parseAliases(ctx, valueNode); + for (Alias alias : aliases) { + builder.addAlias(alias.alias(), alias.qualifiedName()); + } + break; + case "abbreviations": + builder.addAbbreviations(parseAbbreviations(ctx, valueNode)); + break; + default: + ctx.reportError(keyId, String.format("Unsupported container tag: %s", keyName)); + break; + } + } + + 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); + if (!assertYamlType(ctx, valueId, node, YamlNodeType.LIST)) { + return aliasSetBuilder.build(); + } + + SequenceNode variableListNode = (SequenceNode) node; + for (Node elementNode : variableListNode.getValue()) { + aliasSetBuilder.add(parseAlias(ctx, elementNode)); + } + + return aliasSetBuilder.build(); + } + + private Alias parseAlias(ParserContext ctx, Node node) { + long id = ctx.collectMetadata(node); + if (!assertYamlType(ctx, id, node, YamlNodeType.MAP)) { + return ERROR_ALIAS; + } + + Alias.Builder builder = Alias.newBuilder(); + MappingNode attrMap = (MappingNode) node; + for (NodeTuple nodeTuple : attrMap.getValue()) { + Node keyNode = nodeTuple.getKeyNode(); + long keyId = ctx.collectMetadata(keyNode); + Node valueNode = nodeTuple.getValueNode(); + String keyName = ((ScalarNode) keyNode).getValue(); + switch (keyName) { + case "alias": + builder.setAlias(newString(ctx, valueNode)); + break; + case "qualified_name": + builder.setQualifiedName(newString(ctx, valueNode)); + break; + default: + ctx.reportError(keyId, String.format("Unsupported alias tag: %s", keyName)); + break; + } + } + + if (!assertRequiredFields(ctx, id, builder.getMissingRequiredFieldNames())) { + return ERROR_ALIAS; + } + + return builder.build(); + } + + private ImmutableSet parseAbbreviations(ParserContext ctx, Node node) { + long valueId = ctx.collectMetadata(node); + if (!assertYamlType(ctx, valueId, node, YamlNodeType.LIST)) { + return ImmutableSet.of(ERROR); + } + + ImmutableSet.Builder builder = ImmutableSet.builder(); + SequenceNode nameListNode = (SequenceNode) node; + for (Node elementNode : nameListNode.getValue()) { + long elementId = ctx.collectMetadata(elementNode); + if (!assertYamlType(ctx, elementId, elementNode, YamlNodeType.STRING)) { + return ImmutableSet.of(ERROR); + } + + builder.add(((ScalarNode) elementNode).getValue()); + } + return builder.build(); + } + + private ContextVariable parseContextVariable(ParserContext ctx, Node node) { + long id = ctx.collectMetadata(node); + if (!assertYamlType(ctx, id, node, YamlNodeType.MAP)) { + return ContextVariable.create(""); + } + + MappingNode mapNode = (MappingNode) node; + String typeName = ""; + for (NodeTuple nodeTuple : mapNode.getValue()) { + Node keyNode = nodeTuple.getKeyNode(); + long keyId = ctx.collectMetadata(keyNode); + Node valueNode = nodeTuple.getValueNode(); + String keyName = ((ScalarNode) keyNode).getValue(); + switch (keyName) { + case "type_name": + typeName = newString(ctx, valueNode); + break; + default: + ctx.reportError(keyId, String.format("Unsupported context_variable tag: %s", keyName)); + break; + } + } + + if (typeName.isEmpty()) { + ctx.reportError(id, "Missing required attribute(s): type_name"); + } + + return ContextVariable.create(typeName); + } + + private ImmutableSet parseVariables(ParserContext ctx, Node node) { + long valueId = ctx.collectMetadata(node); + ImmutableSet.Builder variableSetBuilder = ImmutableSet.builder(); + if (!assertYamlType(ctx, valueId, node, YamlNodeType.LIST)) { + return variableSetBuilder.build(); + } + + SequenceNode variableListNode = (SequenceNode) node; + for (Node elementNode : variableListNode.getValue()) { + variableSetBuilder.add(parseVariable(ctx, elementNode)); + } + + return variableSetBuilder.build(); + } + + private VariableDecl parseVariable(ParserContext ctx, Node node) { + long variableId = ctx.collectMetadata(node); + if (!assertYamlType(ctx, variableId, node, YamlNodeType.MAP)) { + return ERROR_VARIABLE_DECL; + } + + MappingNode variableMap = (MappingNode) node; + VariableDecl.Builder builder = VariableDecl.newBuilder(); + TypeDecl.Builder typeDeclBuilder = null; + for (NodeTuple nodeTuple : variableMap.getValue()) { + Node keyNode = nodeTuple.getKeyNode(); + long keyId = ctx.collectMetadata(keyNode); + Node valueNode = nodeTuple.getValueNode(); + String keyName = ((ScalarNode) keyNode).getValue(); + switch (keyName) { + case "name": + builder.setName(newString(ctx, valueNode)); + break; + case "description": + builder.setDescription(newString(ctx, valueNode)); + break; + case "type": + if (typeDeclBuilder != null) { + ctx.reportError( + keyId, + String.format( + "'type' tag cannot be used together with inlined 'type_name', 'is_type_param'" + + " or 'params': %s", + keyName)); + break; + } + builder.setType(parseTypeDecl(ctx, valueNode)); + break; + case "type_name": + case "is_type_param": + case "params": + if (typeDeclBuilder == null) { + typeDeclBuilder = TypeDecl.newBuilder(); + } + typeDeclBuilder = parseInlinedTypeDecl(ctx, keyId, keyNode, valueNode, typeDeclBuilder); + break; + default: + ctx.reportError(keyId, String.format("Unsupported variable tag: %s", keyName)); + break; + } + } + + if (typeDeclBuilder != null) { + if (!assertRequiredFields(ctx, variableId, typeDeclBuilder.getMissingRequiredFieldNames())) { + return ERROR_VARIABLE_DECL; + } + builder.setType(typeDeclBuilder.build()); + } + + if (!assertRequiredFields(ctx, variableId, builder.getMissingRequiredFieldNames())) { + return ERROR_VARIABLE_DECL; + } + + return builder.build(); + } + + private ImmutableSet parseFunctions(ParserContext ctx, Node node) { + long valueId = ctx.collectMetadata(node); + ImmutableSet.Builder functionSetBuilder = ImmutableSet.builder(); + if (!assertYamlType(ctx, valueId, node, YamlNodeType.LIST)) { + return functionSetBuilder.build(); + } + + SequenceNode functionListNode = (SequenceNode) node; + for (Node elementNode : functionListNode.getValue()) { + functionSetBuilder.add(parseFunction(ctx, elementNode)); + } + + return functionSetBuilder.build(); + } + + private FunctionDecl parseFunction(ParserContext ctx, Node node) { + long functionId = ctx.collectMetadata(node); + if (!assertYamlType(ctx, functionId, node, YamlNodeType.MAP)) { + return ERROR_FUNCTION_DECL; + } + + MappingNode functionMap = (MappingNode) node; + FunctionDecl.Builder builder = FunctionDecl.newBuilder(); + for (NodeTuple nodeTuple : functionMap.getValue()) { + Node keyNode = nodeTuple.getKeyNode(); + long keyId = ctx.collectMetadata(keyNode); + Node valueNode = nodeTuple.getValueNode(); + String keyName = ((ScalarNode) keyNode).getValue(); + switch (keyName) { + case "name": + builder.setName(newString(ctx, valueNode)); + break; + 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; + } + } + + if (!assertRequiredFields(ctx, functionId, builder.getMissingRequiredFieldNames())) { + return ERROR_FUNCTION_DECL; + } + + return builder.build(); + } + + private static ImmutableSet parseOverloads(ParserContext ctx, Node node) { + long listId = ctx.collectMetadata(node); + ImmutableSet.Builder overloadSetBuilder = ImmutableSet.builder(); + if (!assertYamlType(ctx, listId, node, YamlNodeType.LIST)) { + return overloadSetBuilder.build(); + } + + SequenceNode overloadListNode = (SequenceNode) node; + for (Node overloadMapNode : overloadListNode.getValue()) { + long overloadMapId = ctx.collectMetadata(overloadMapNode); + if (!assertYamlType(ctx, overloadMapId, overloadMapNode, YamlNodeType.MAP)) { + continue; + } + + MappingNode mapNode = (MappingNode) overloadMapNode; + OverloadDecl.Builder overloadDeclBuilder = OverloadDecl.newBuilder(); + for (NodeTuple nodeTuple : mapNode.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 fieldName = ((ScalarNode) keyNode).getValue(); + switch (fieldName) { + case "id": + overloadDeclBuilder.setId(newString(ctx, valueNode)); + break; + case "args": + overloadDeclBuilder.addArguments(parseOverloadArguments(ctx, valueNode)); + break; + case "return": + overloadDeclBuilder.setReturnType(parseTypeDecl(ctx, valueNode)); + break; + 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; + } + } + + if (assertRequiredFields( + ctx, overloadMapId, overloadDeclBuilder.getMissingRequiredFieldNames())) { + overloadSetBuilder.add(overloadDeclBuilder.build()); + } + } + + return overloadSetBuilder.build(); + } + + private static ImmutableList parseOverloadExamples(ParserContext ctx, Node node) { + long listValueId = ctx.collectMetadata(node); + if (!assertYamlType(ctx, listValueId, node, YamlNodeType.LIST)) { + return ImmutableList.of(); + } + SequenceNode paramsListNode = (SequenceNode) node; + ImmutableList.Builder builder = ImmutableList.builder(); + for (Node elementNode : paramsListNode.getValue()) { + long elementNodeId = ctx.collectMetadata(elementNode); + if (!assertYamlType(ctx, elementNodeId, elementNode, YamlNodeType.STRING)) { + continue; + } + + builder.add(((ScalarNode) elementNode).getValue()); + } + + return builder.build(); + } + + private static ImmutableList parseOverloadArguments( + ParserContext ctx, Node node) { + long listValueId = ctx.collectMetadata(node); + if (!assertYamlType(ctx, listValueId, node, YamlNodeType.LIST)) { + return ImmutableList.of(); + } + SequenceNode paramsListNode = (SequenceNode) node; + ImmutableList.Builder builder = ImmutableList.builder(); + for (Node elementNode : paramsListNode.getValue()) { + builder.add(parseTypeDecl(ctx, elementNode)); + } + + return builder.build(); + } + + private static ImmutableSet parseExtensions(ParserContext ctx, Node node) { + long valueId = ctx.collectMetadata(node); + ImmutableSet.Builder extensionConfigBuilder = ImmutableSet.builder(); + if (!assertYamlType(ctx, valueId, node, YamlNodeType.LIST)) { + return extensionConfigBuilder.build(); + } + + SequenceNode extensionListNode = (SequenceNode) node; + for (Node elementNode : extensionListNode.getValue()) { + extensionConfigBuilder.add(parseExtension(ctx, elementNode)); + } + + return extensionConfigBuilder.build(); + } + + private static ExtensionConfig parseExtension(ParserContext ctx, Node node) { + long extensionId = ctx.collectMetadata(node); + if (!assertYamlType(ctx, extensionId, node, YamlNodeType.MAP)) { + return ERROR_EXTENSION_DECL; + } + + MappingNode extensionMap = (MappingNode) node; + ExtensionConfig.Builder builder = ExtensionConfig.newBuilder(); + for (NodeTuple nodeTuple : extensionMap.getValue()) { + Node keyNode = nodeTuple.getKeyNode(); + long keyId = ctx.collectMetadata(keyNode); + Node valueNode = nodeTuple.getValueNode(); + String keyName = ((ScalarNode) keyNode).getValue(); + switch (keyName) { + case "name": + builder.setName(newString(ctx, valueNode)); + break; + case "version": + if (validateYamlType(valueNode, YamlNodeType.INTEGER)) { + builder.setVersion(newInteger(ctx, valueNode)); + break; + } else if (validateYamlType(valueNode, YamlNodeType.STRING, YamlNodeType.TEXT)) { + String versionStr = newString(ctx, valueNode); + if (versionStr.equals("latest")) { + builder.setVersion(Integer.MAX_VALUE); + break; + } + + Integer versionInt = tryParse(versionStr); + if (versionInt != null) { + builder.setVersion(versionInt); + break; + } + // Fall-through + } + ctx.reportError(keyId, String.format("Unsupported version tag: %s", keyName)); + break; + default: + ctx.reportError(keyId, String.format("Unsupported extension tag: %s", keyName)); + break; + } + } + + if (!assertRequiredFields(ctx, extensionId, builder.getMissingRequiredFieldNames())) { + return ERROR_EXTENSION_DECL; + } + + return builder.build(); + } + + private static LibrarySubset parseLibrarySubset(ParserContext ctx, Node node) { + LibrarySubset.Builder builder = LibrarySubset.newBuilder().setDisabled(false); + MappingNode subsetMap = (MappingNode) node; + for (NodeTuple nodeTuple : subsetMap.getValue()) { + Node keyNode = nodeTuple.getKeyNode(); + long keyId = ctx.collectMetadata(keyNode); + Node valueNode = nodeTuple.getValueNode(); + String keyName = ((ScalarNode) keyNode).getValue(); + switch (keyName) { + case "disabled": + builder.setDisabled(newBoolean(ctx, valueNode)); + break; + case "disable_macros": + builder.setMacrosDisabled(newBoolean(ctx, valueNode)); + break; + case "include_macros": + builder.setIncludedMacros(parseMacroNameSet(ctx, valueNode)); + break; + case "exclude_macros": + builder.setExcludedMacros(parseMacroNameSet(ctx, valueNode)); + break; + case "include_functions": + builder.setIncludedFunctions(parseFunctionSelectors(ctx, valueNode)); + break; + case "exclude_functions": + builder.setExcludedFunctions(parseFunctionSelectors(ctx, valueNode)); + break; + default: + ctx.reportError(keyId, String.format("Unsupported library subset tag: %s", keyName)); + break; + } + } + return builder.build(); + } + + private static ImmutableSet parseMacroNameSet(ParserContext ctx, Node node) { + long valueId = ctx.collectMetadata(node); + if (!assertYamlType(ctx, valueId, node, YamlNodeType.LIST)) { + return ImmutableSet.of(ERROR); + } + + ImmutableSet.Builder builder = ImmutableSet.builder(); + SequenceNode nameListNode = (SequenceNode) node; + for (Node elementNode : nameListNode.getValue()) { + long elementId = ctx.collectMetadata(elementNode); + if (!assertYamlType(ctx, elementId, elementNode, YamlNodeType.STRING)) { + return ImmutableSet.of(ERROR); + } + + builder.add(((ScalarNode) elementNode).getValue()); + } + return builder.build(); + } + + private static ImmutableSet parseFunctionSelectors( + ParserContext ctx, Node node) { + long valueId = ctx.collectMetadata(node); + ImmutableSet.Builder functionSetBuilder = ImmutableSet.builder(); + if (!assertYamlType(ctx, valueId, node, YamlNodeType.LIST)) { + return functionSetBuilder.build(); + } + + SequenceNode functionListNode = (SequenceNode) node; + for (Node elementNode : functionListNode.getValue()) { + functionSetBuilder.add(parseFunctionSelector(ctx, elementNode)); + } + + return functionSetBuilder.build(); + } + + private static FunctionSelector parseFunctionSelector(ParserContext ctx, Node node) { + FunctionSelector.Builder builder = FunctionSelector.newBuilder(); + long functionId = ctx.collectMetadata(node); + if (!assertYamlType(ctx, functionId, node, YamlNodeType.MAP)) { + return ERROR_FUNCTION_SELECTOR; + } + + MappingNode functionMap = (MappingNode) node; + for (NodeTuple nodeTuple : functionMap.getValue()) { + Node keyNode = nodeTuple.getKeyNode(); + long keyId = ctx.collectMetadata(keyNode); + Node valueNode = nodeTuple.getValueNode(); + String keyName = ((ScalarNode) keyNode).getValue(); + switch (keyName) { + case "name": + builder.setName(newString(ctx, valueNode)); + break; + case "overloads": + builder.setOverloads(parseFunctionOverloadsSelector(ctx, valueNode)); + break; + default: + ctx.reportError(keyId, String.format("Unsupported function selector tag: %s", keyName)); + break; + } + } + + if (!assertRequiredFields(ctx, functionId, builder.getMissingRequiredFieldNames())) { + return ERROR_FUNCTION_SELECTOR; + } + + return builder.build(); + } + + private static ImmutableSet parseFunctionOverloadsSelector( + ParserContext ctx, Node node) { + long listId = ctx.collectMetadata(node); + ImmutableSet.Builder overloadSetBuilder = ImmutableSet.builder(); + if (!assertYamlType(ctx, listId, node, YamlNodeType.LIST)) { + return overloadSetBuilder.build(); + } + + SequenceNode overloadListNode = (SequenceNode) node; + for (Node overloadMapNode : overloadListNode.getValue()) { + long overloadMapId = ctx.collectMetadata(overloadMapNode); + if (!assertYamlType(ctx, overloadMapId, overloadMapNode, YamlNodeType.MAP)) { + continue; + } + + MappingNode mapNode = (MappingNode) overloadMapNode; + OverloadSelector.Builder overloadDeclBuilder = OverloadSelector.newBuilder(); + for (NodeTuple nodeTuple : mapNode.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 fieldName = ((ScalarNode) keyNode).getValue(); + switch (fieldName) { + case "id": + overloadDeclBuilder.setId(newString(ctx, valueNode)); + break; + default: + ctx.reportError( + keyId, String.format("Unsupported overload selector tag: %s", fieldName)); + break; + } + } + + if (assertRequiredFields( + ctx, overloadMapId, overloadDeclBuilder.getMissingRequiredFieldNames())) { + overloadSetBuilder.add(overloadDeclBuilder.build()); + } + } + + return overloadSetBuilder.build(); + } + + private static @Nullable Integer tryParse(String str) { + try { + return Integer.parseInt(str); + } catch (NumberFormatException e) { + return null; + } + } + + @CanIgnoreReturnValue + private static 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; + } + + // 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); + + return parseTypeDeclFields(ctx, mapNode, builder); + } + + private static TypeDecl parseTypeDecl(ParserContext ctx, Node node) { + TypeDecl.Builder builder = TypeDecl.newBuilder(); + long id = ctx.collectMetadata(node); + if (!assertYamlType(ctx, id, node, YamlNodeType.MAP)) { + return ERROR_TYPE_DECL; + } + + MappingNode mapNode = (MappingNode) node; + return parseTypeDeclFields(ctx, mapNode, builder).build(); + } + + @CanIgnoreReturnValue + private static TypeDecl.Builder parseTypeDeclFields( + ParserContext ctx, MappingNode mapNode, TypeDecl.Builder builder) { + for (NodeTuple nodeTuple : mapNode.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 fieldName = ((ScalarNode) keyNode).getValue(); + switch (fieldName) { + case "type_name": + builder.setName(newString(ctx, valueNode)); + break; + case "is_type_param": + builder.setIsTypeParam(newBoolean(ctx, valueNode)); + break; + case "params": + long listValueId = ctx.collectMetadata(valueNode); + if (!assertYamlType(ctx, listValueId, valueNode, YamlNodeType.LIST)) { + break; + } + SequenceNode paramsListNode = (SequenceNode) valueNode; + for (Node elementNode : paramsListNode.getValue()) { + builder.addParams(parseTypeDecl(ctx, elementNode)); + } + break; + default: + ctx.reportError(keyId, String.format("Unsupported type decl tag: %s", fieldName)); + break; + } + } + return builder; + } + + private class ParserImpl { + + private CelEnvironment parseYaml(String source, String description) + throws CelEnvironmentException { + Node node; + try { + node = + parseYamlSource(source) + .orElseThrow( + () -> + new CelEnvironmentException( + String.format("YAML document empty or malformed: %s", source))); + } catch (RuntimeException e) { + throw new CelEnvironmentException("YAML document is malformed: " + e.getMessage(), e); + } + + CelFileSource environmentSource = + CelFileSource.newBuilder(CelCodePointArray.fromString(source)) + .setDescription(description) + .build(); + ParserContext ctx = YamlParserContextImpl.newInstance(environmentSource); + CelEnvironment.Builder builder = parseConfig(ctx, node); + environmentSource = + environmentSource.toBuilder().setPositionsMap(ctx.getIdToOffsetMap()).build(); + + if (!ctx.getIssues().isEmpty()) { + throw new CelEnvironmentException( + CelIssue.toDisplayString(ctx.getIssues(), environmentSource)); + } + + return builder.setSource(environmentSource).build(); + } + + private CelEnvironment.Builder parseConfig(ParserContext ctx, Node node) { + CelEnvironment.Builder builder = CelEnvironment.newBuilder(); + long id = ctx.collectMetadata(node); + if (!assertYamlType(ctx, id, node, YamlNodeType.MAP)) { + return builder; + } + + MappingNode rootNode = (MappingNode) node; + for (NodeTuple nodeTuple : rootNode.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 fieldName = ((ScalarNode) keyNode).getValue(); + switch (fieldName) { + case "name": + builder.setName(newString(ctx, valueNode)); + break; + case "description": + builder.setDescription(newString(ctx, valueNode)); + break; + case "container": + builder.setContainer(parseContainer(ctx, valueNode)); + break; + case "variables": + builder.setVariables(parseVariables(ctx, valueNode)); + break; + case "functions": + builder.setFunctions(parseFunctions(ctx, valueNode)); + break; + case "extensions": + builder.addExtensions(parseExtensions(ctx, valueNode)); + break; + 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 + } + } + + return builder; + } + } + + private CelEnvironmentYamlParser() {} +} diff --git a/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlSerializer.java b/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlSerializer.java new file mode 100644 index 000000000..9d5b4b69e --- /dev/null +++ b/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlSerializer.java @@ -0,0 +1,293 @@ +// 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.bundle; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import dev.cel.bundle.CelEnvironment.Alias; +import dev.cel.bundle.CelEnvironment.LibrarySubset; +import dev.cel.bundle.CelEnvironment.LibrarySubset.FunctionSelector; +import dev.cel.bundle.CelEnvironment.LibrarySubset.OverloadSelector; +import dev.cel.common.CelContainer; +import java.util.Comparator; +import java.util.Map; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.nodes.Node; +import org.yaml.snakeyaml.representer.Represent; +import org.yaml.snakeyaml.representer.Representer; + +/** Serializes a CelEnvironment into a YAML file. */ +public final class CelEnvironmentYamlSerializer extends Representer { + + private static DumperOptions initDumperOptions() { + DumperOptions options = new DumperOptions(); + options.setIndent(2); + options.setPrettyFlow(true); + options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + return options; + } + + private static final DumperOptions YAML_OPTIONS = initDumperOptions(); + + private static final CelEnvironmentYamlSerializer INSTANCE = new CelEnvironmentYamlSerializer(); + + private CelEnvironmentYamlSerializer() { + super(YAML_OPTIONS); + this.multiRepresenters.put(CelEnvironment.class, new RepresentCelEnvironment()); + this.multiRepresenters.put(CelEnvironment.VariableDecl.class, new RepresentVariableDecl()); + this.multiRepresenters.put(CelEnvironment.FunctionDecl.class, new RepresentFunctionDecl()); + this.multiRepresenters.put(CelEnvironment.OverloadDecl.class, new RepresentOverloadDecl()); + this.multiRepresenters.put(CelEnvironment.TypeDecl.class, new RepresentTypeDecl()); + this.multiRepresenters.put( + CelEnvironment.ExtensionConfig.class, new RepresentExtensionConfig()); + this.multiRepresenters.put(CelEnvironment.LibrarySubset.class, new RepresentLibrarySubset()); + this.multiRepresenters.put( + CelEnvironment.LibrarySubset.FunctionSelector.class, new RepresentFunctionSelector()); + this.multiRepresenters.put( + 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) { + // Yaml is not thread-safe, so we create a new instance for each serialization. + Yaml yaml = new Yaml(INSTANCE, YAML_OPTIONS); + return yaml.dump(environment); + } + + private final class RepresentCelEnvironment implements Represent { + @Override + public Node representData(Object data) { + CelEnvironment environment = (CelEnvironment) data; + ImmutableMap.Builder configMap = new ImmutableMap.Builder<>(); + configMap.put("name", environment.name()); + if (!environment.description().isEmpty()) { + configMap.put("description", environment.description()); + } + if (environment.container().isPresent()) { + configMap.put("container", environment.container().get()); + } + if (!environment.extensions().isEmpty()) { + configMap.put("extensions", environment.extensions().asList()); + } + if (!environment.variables().isEmpty()) { + configMap.put("variables", environment.variables().asList()); + } + if (!environment.functions().isEmpty()) { + configMap.put("functions", environment.functions().asList()); + } + 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()); + } + } + + private final class RepresentContainer implements Represent { + + @Override + public Node representData(Object data) { + CelContainer container = (CelContainer) data; + ImmutableMap.Builder configMap = ImmutableMap.builder(); + if (!container.name().isEmpty()) { + configMap.put("name", container.name()); + } + if (!container.abbreviations().isEmpty()) { + configMap.put("abbreviations", container.abbreviations()); + } + if (!container.aliases().isEmpty()) { + ImmutableList.Builder aliases = ImmutableList.builder(); + for (Map.Entry entry : container.aliases().entrySet()) { + aliases.add( + Alias.newBuilder() + .setAlias(entry.getKey()) + .setQualifiedName(entry.getValue()) + .build()); + } + configMap.put("aliases", aliases.build()); + } + return represent(configMap.buildOrThrow()); + } + } + + private final class RepresentAlias implements Represent { + + @Override + public Node representData(Object data) { + Alias alias = (Alias) data; + return represent( + ImmutableMap.of("alias", alias.alias(), "qualified_name", alias.qualifiedName())); + } + } + + private final class RepresentExtensionConfig implements Represent { + @Override + public Node representData(Object data) { + CelEnvironment.ExtensionConfig extension = (CelEnvironment.ExtensionConfig) data; + ImmutableMap.Builder configMap = new ImmutableMap.Builder<>(); + configMap.put("name", extension.name()); + if (extension.version() > 0 && extension.version() != Integer.MAX_VALUE) { + configMap.put("version", extension.version()); + } + return represent(configMap.buildOrThrow()); + } + } + + private final class RepresentVariableDecl implements Represent { + @Override + public Node representData(Object data) { + CelEnvironment.VariableDecl variable = (CelEnvironment.VariableDecl) data; + ImmutableMap.Builder configMap = new ImmutableMap.Builder<>(); + configMap.put("name", variable.name()).put("type_name", variable.type().name()); + if (!variable.type().params().isEmpty()) { + configMap.put("params", variable.type().params()); + } + return represent(configMap.buildOrThrow()); + } + } + + private final class RepresentFunctionDecl implements Represent { + @Override + public Node representData(Object data) { + CelEnvironment.FunctionDecl function = (CelEnvironment.FunctionDecl) data; + ImmutableMap.Builder configMap = new ImmutableMap.Builder<>(); + configMap.put("name", function.name()).put("overloads", function.overloads().asList()); + return represent(configMap.buildOrThrow()); + } + } + + private final class RepresentOverloadDecl implements Represent { + @Override + public Node representData(Object data) { + CelEnvironment.OverloadDecl overload = (CelEnvironment.OverloadDecl) data; + ImmutableMap.Builder configMap = new ImmutableMap.Builder<>(); + configMap.put("id", overload.id()); + if (overload.target().isPresent()) { + configMap.put("target", overload.target().get()); + } + configMap.put("args", overload.arguments()).put("return", overload.returnType()); + return represent(configMap.buildOrThrow()); + } + } + + private final class RepresentTypeDecl implements Represent { + @Override + public Node representData(Object data) { + CelEnvironment.TypeDecl type = (CelEnvironment.TypeDecl) data; + ImmutableMap.Builder configMap = new ImmutableMap.Builder<>(); + configMap.put("type_name", type.name()); + if (!type.params().isEmpty()) { + configMap.put("params", type.params()); + } + if (type.isTypeParam()) { + configMap.put("is_type_param", type.isTypeParam()); + } + return represent(configMap.buildOrThrow()); + } + } + + private final class RepresentLibrarySubset implements Represent { + @Override + public Node representData(Object data) { + LibrarySubset librarySubset = (LibrarySubset) data; + ImmutableMap.Builder configMap = new ImmutableMap.Builder<>(); + if (librarySubset.disabled()) { + configMap.put("disabled", true); + } + if (librarySubset.macrosDisabled()) { + configMap.put("disable_macros", true); + } + if (!librarySubset.includedMacros().isEmpty()) { + configMap.put("include_macros", ImmutableList.sortedCopyOf(librarySubset.includedMacros())); + } + if (!librarySubset.excludedMacros().isEmpty()) { + configMap.put("exclude_macros", ImmutableList.sortedCopyOf(librarySubset.excludedMacros())); + } + if (!librarySubset.includedFunctions().isEmpty()) { + configMap.put( + "include_functions", + ImmutableList.sortedCopyOf( + Comparator.comparing(FunctionSelector::name), librarySubset.includedFunctions())); + } + if (!librarySubset.excludedFunctions().isEmpty()) { + configMap.put( + "exclude_functions", + ImmutableList.sortedCopyOf( + Comparator.comparing(FunctionSelector::name), librarySubset.excludedFunctions())); + } + return represent(configMap.buildOrThrow()); + } + } + + private final class RepresentFunctionSelector implements Represent { + @Override + public Node representData(Object data) { + FunctionSelector functionSelector = (FunctionSelector) data; + ImmutableMap.Builder configMap = new ImmutableMap.Builder<>(); + configMap.put("name", functionSelector.name()); + if (!functionSelector.overloads().isEmpty()) { + configMap.put( + "overloads", + ImmutableList.sortedCopyOf( + Comparator.comparing(OverloadSelector::id), functionSelector.overloads())); + } + + return represent(configMap.buildOrThrow()); + } + } + + private final class RepresentOverloadSelector implements Represent { + @Override + public Node representData(Object data) { + OverloadSelector overloadSelector = (OverloadSelector) 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 a2080bc25..ac589cfe6 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelFactory.java +++ b/bundle/src/main/java/dev/cel/bundle/CelFactory.java @@ -20,6 +20,7 @@ import dev.cel.compiler.CelCompilerImpl; import dev.cel.parser.CelParserImpl; import dev.cel.runtime.CelRuntime; +import dev.cel.runtime.CelRuntimeImpl; import dev.cel.runtime.CelRuntimeLegacyImpl; /** Helper class to configure the entire CEL stack in a common interface. */ @@ -40,9 +41,34 @@ public static CelBuilder standardCelBuilder() { CelParserImpl.newBuilder(), CelCheckerLegacyImpl.newBuilder()), CelRuntimeLegacyImpl.newBuilder()) .setOptions(CelOptions.current().build()) + // CEL-Internal-2 .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 ad062d113..f0db128c1 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelImpl.java +++ b/bundle/src/main/java/dev/cel/bundle/CelImpl.java @@ -28,9 +28,11 @@ import com.google.protobuf.ExtensionRegistry; import com.google.protobuf.Message; import dev.cel.checker.CelCheckerBuilder; +import dev.cel.checker.CelStandardDeclarations; import dev.cel.checker.ProtoTypeMask; import dev.cel.checker.TypeProvider; import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelContainer; import dev.cel.common.CelFunctionDecl; import dev.cel.common.CelOptions; import dev.cel.common.CelSource; @@ -39,9 +41,9 @@ import dev.cel.common.internal.EnvVisitable; import dev.cel.common.internal.EnvVisitor; import dev.cel.common.internal.FileDescriptorSetConverter; +import dev.cel.common.types.CelProtoTypes; import dev.cel.common.types.CelType; import dev.cel.common.types.CelTypeProvider; -import dev.cel.common.types.CelTypes; import dev.cel.common.values.CelValueProvider; import dev.cel.compiler.CelCompiler; import dev.cel.compiler.CelCompilerBuilder; @@ -52,7 +54,9 @@ 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; import java.util.function.Function; @@ -87,6 +91,11 @@ public CelValidationResult check(CelAbstractSyntaxTree ast) { return compiler.get().check(ast); } + @Override + public CelTypeProvider getTypeProvider() { + return compiler.get().getTypeProvider(); + } + @Override public CelRuntime.Program createProgram(CelAbstractSyntaxTree ast) throws CelEvaluationException { return runtime.get().createProgram(ast); @@ -134,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) { @@ -184,8 +195,17 @@ public CelBuilder addMacros(Iterable macros) { } @Override - public CelBuilder setContainer(String container) { + public CelContainer container() { + return compilerBuilder.container(); + } + + @Override + public CelBuilder setContainer(CelContainer container) { compilerBuilder.setContainer(container); + if (runtimeBuilder instanceof CelRuntimeImpl.Builder) { + runtimeBuilder.setContainer(container); + } + return this; } @@ -250,21 +270,33 @@ public CelBuilder addProtoTypeMasks(Iterable typeMasks) { } @Override - public CelBuilder addFunctionBindings(CelRuntime.CelFunctionBinding... bindings) { + public CelBuilder addFunctionBindings(dev.cel.runtime.CelFunctionBinding... bindings) { runtimeBuilder.addFunctionBindings(bindings); return this; } @Override - public CelBuilder addFunctionBindings(Iterable bindings) { + public CelBuilder addFunctionBindings(Iterable bindings) { runtimeBuilder.addFunctionBindings(bindings); return this; } + @Override + public CelBuilder addLateBoundFunctions(String... lateBoundFunctionNames) { + runtimeBuilder.addLateBoundFunctions(lateBoundFunctionNames); + return this; + } + + @Override + public CelBuilder addLateBoundFunctions(Iterable lateBoundFunctionNames) { + runtimeBuilder.addLateBoundFunctions(lateBoundFunctionNames); + return this; + } + @Override public CelBuilder setResultType(CelType resultType) { checkNotNull(resultType); - return setProtoResultType(CelTypes.celTypeToType(resultType)); + return setProtoResultType(CelProtoTypes.celTypeToType(resultType)); } @Override @@ -295,6 +327,9 @@ public Builder setTypeProvider(TypeProvider typeProvider) { @Override public CelBuilder setTypeProvider(CelTypeProvider celTypeProvider) { compilerBuilder.setTypeProvider(celTypeProvider); + if (runtimeBuilder instanceof CelRuntimeImpl.Builder) { + runtimeBuilder.setTypeProvider(celTypeProvider); + } return this; } @@ -371,6 +406,20 @@ public CelBuilder addRuntimeLibraries(Iterable libraries) { return this; } + @Override + public CelBuilder setStandardDeclarations(CelStandardDeclarations standardDeclarations) { + checkNotNull(standardDeclarations); + compilerBuilder.setStandardDeclarations(standardDeclarations); + return this; + } + + @Override + public CelBuilder setStandardFunctions(CelStandardFunctions standardFunctions) { + checkNotNull(standardFunctions); + runtimeBuilder.setStandardFunctions(standardFunctions); + return this; + } + @Override public CelBuilder setExtensionRegistry(ExtensionRegistry extensionRegistry) { checkNotNull(extensionRegistry); diff --git a/bundle/src/main/java/dev/cel/bundle/RequiredFieldsChecker.java b/bundle/src/main/java/dev/cel/bundle/RequiredFieldsChecker.java new file mode 100644 index 000000000..88bef2db5 --- /dev/null +++ b/bundle/src/main/java/dev/cel/bundle/RequiredFieldsChecker.java @@ -0,0 +1,49 @@ +// 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. + +package dev.cel.bundle; + +import static com.google.common.collect.ImmutableList.toImmutableList; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import java.util.Optional; +import java.util.function.Supplier; + +/** + * Interface to be implemented on a builder that can be used to verify all required fields being + * set. + */ +interface RequiredFieldsChecker { + + ImmutableList requiredFields(); + + default ImmutableList getMissingRequiredFieldNames() { + return requiredFields().stream() + .filter(entry -> !entry.fieldValue().get().isPresent()) + .map(RequiredField::displayName) + .collect(toImmutableList()); + } + + @AutoValue + abstract class RequiredField { + abstract String displayName(); + + abstract Supplier> fieldValue(); + + static RequiredField of(String displayName, Supplier> fieldValue) { + return new AutoValue_RequiredFieldsChecker_RequiredField(displayName, fieldValue); + } + } +} diff --git a/bundle/src/test/java/dev/cel/bundle/BUILD.bazel b/bundle/src/test/java/dev/cel/bundle/BUILD.bazel index cccfc8eed..548b4483d 100644 --- a/bundle/src/test/java/dev/cel/bundle/BUILD.bazel +++ b/bundle/src/test/java/dev/cel/bundle/BUILD.bazel @@ -1,47 +1,73 @@ +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"]), + resources = [ + "//testing/environment:dump_env", + "//testing/environment:extended_env", + "//testing/environment:library_subset_env", + ], deps = [ - "//:auto_value", "//:java_truth", "//bundle:cel", + "//bundle:cel_impl", + "//bundle:environment", + "//bundle:environment_exception", + "//bundle:environment_exporter", + "//bundle:environment_yaml_parser", "//checker", "//checker:checker_legacy_environment", "//checker:proto_type_mask", - "//common", + "//common:cel_ast", + "//common:cel_descriptor_util", + "//common:cel_source", "//common:compiler_common", + "//common:container", + "//common:error_codes", "//common:options", "//common:proto_ast", + "//common:source_location", "//common/ast", - "//common/resources/testdata/proto2:messages_extensions_proto2_java_proto", - "//common/resources/testdata/proto2:messages_proto2_java_proto", - "//common/resources/testdata/proto2:test_all_types_java_proto", "//common/resources/testdata/proto3:standalone_global_enum_java_proto", - "//common/resources/testdata/proto3:test_all_types_java_proto", "//common/testing", "//common/types", - "//common/types:cel_types", + "//common/types:cel_proto_message_types", + "//common/types:cel_proto_types", "//common/types:message_type_provider", "//common/types:type_providers", + "//common/values", + "//common/values:cel_byte_string", "//compiler", "//compiler:compiler_builder", + "//extensions", "//parser", "//parser:macro", + "//parser:unparser", "//runtime", + "//runtime:evaluation_exception_builder", + "//runtime:evaluation_listener", + "//runtime:function_binding", "//runtime:unknown_attributes", - "@cel_spec//proto/cel/expr:expr_java_proto", - "@maven//:com_google_api_grpc_proto_google_common_protos", + "//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", + "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto", + "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", + "@com_google_googleapis//google/rpc/context:attribute_context_java_proto", + "@com_google_googleapis//google/type:type_java_proto", "@maven//:com_google_guava_guava", - "@maven//:com_google_guava_guava_testlib", "@maven//:com_google_protobuf_protobuf_java", - "@maven//:com_google_protobuf_protobuf_java_util", "@maven//:com_google_testparameterinjector_test_parameter_injector", "@maven//:com_google_truth_extensions_truth_proto_extension", "@maven//:junit_junit", diff --git a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentExporterTest.java b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentExporterTest.java new file mode 100644 index 000000000..ae0de2c18 --- /dev/null +++ b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentExporterTest.java @@ -0,0 +1,370 @@ +// 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.bundle; + +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.truth.Truth.assertThat; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.stream.Collectors.toCollection; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.io.Resources; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.bundle.CelEnvironment.ExtensionConfig; +import dev.cel.bundle.CelEnvironment.FunctionDecl; +import dev.cel.bundle.CelEnvironment.LibrarySubset; +import dev.cel.bundle.CelEnvironment.LibrarySubset.FunctionSelector; +import dev.cel.bundle.CelEnvironment.LibrarySubset.OverloadSelector; +import dev.cel.bundle.CelEnvironment.OverloadDecl; +import dev.cel.bundle.CelEnvironment.TypeDecl; +import dev.cel.bundle.CelEnvironment.VariableDecl; +import dev.cel.common.CelContainer; +import dev.cel.common.CelFunctionDecl; +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; +import java.util.Set; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public class CelEnvironmentExporterTest { + + public static final CelOptions CEL_OPTIONS = + CelOptions.newBuilder().enableHeterogeneousNumericComparisons(true).build(); + + @Test + public void extensions_latest() { + Cel cel = + CelFactory.standardCelBuilder() + .addCompilerLibraries(CelExtensions.math(CEL_OPTIONS, 2)) + .build(); + + CelEnvironment celEnvironment = + CelEnvironmentExporter.newBuilder().addStandardExtensions(CEL_OPTIONS).build().export(cel); + + assertThat(celEnvironment.extensions()) + .containsExactly(ExtensionConfig.newBuilder().setName("math").setVersion(2).build()); + } + + @Test + public void extensions_earlierVersion() { + Cel cel = + CelFactory.standardCelBuilder() + .addCompilerLibraries(CelExtensions.math(CEL_OPTIONS, 1)) + .build(); + + CelEnvironment celEnvironment = + CelEnvironmentExporter.newBuilder().addStandardExtensions(CEL_OPTIONS).build().export(cel); + + assertThat(celEnvironment.extensions()) + .containsExactly(ExtensionConfig.newBuilder().setName("math").setVersion(1).build()); + } + + @Test + public void standardLibrarySubset_favorExclusion() throws Exception { + URL url = Resources.getResource("environment/subset_env.yaml"); + String yamlFileContent = Resources.toString(url, UTF_8); + CelEnvironment environment = CelEnvironmentYamlParser.newInstance().parse(yamlFileContent); + + Cel standardCel = + CelFactory.standardCelBuilder() + .addCompilerLibraries(CelExtensions.math(CEL_OPTIONS, 2)) + .build(); + Cel extendedCel = environment.extend(standardCel, CEL_OPTIONS); + + CelEnvironment celEnvironment = + CelEnvironmentExporter.newBuilder() + .setMaxExcludedStandardFunctions(100) + .setMaxExcludedStandardFunctionOverloads(100) + .build() + .export(extendedCel); + + assertThat(celEnvironment.standardLibrarySubset()) + .hasValue( + LibrarySubset.newBuilder() + .setDisabled(false) + .setExcludedFunctions( + ImmutableSet.of( + FunctionSelector.create( + "_+_", ImmutableSet.of("add_bytes", "add_list", "add_string")), + FunctionSelector.create("duration", ImmutableSet.of("string_to_duration")), + FunctionSelector.create("matches", ImmutableSet.of()), + FunctionSelector.create( + "timestamp", ImmutableSet.of("string_to_timestamp")))) + .setExcludedMacros(ImmutableSet.of("map", "existsOne", "filter")) + .build()); + } + + @Test + public void standardLibrarySubset_favorInclusion() throws Exception { + URL url = Resources.getResource("environment/subset_env.yaml"); + String yamlFileContent = Resources.toString(url, UTF_8); + CelEnvironment environment = CelEnvironmentYamlParser.newInstance().parse(yamlFileContent); + + Cel standardCel = + CelFactory.standardCelBuilder() + .addCompilerLibraries(CelExtensions.math(CEL_OPTIONS, 2)) + .build(); + Cel extendedCel = environment.extend(standardCel, CEL_OPTIONS); + + CelEnvironment celEnvironment = + CelEnvironmentExporter.newBuilder() + .setMaxExcludedStandardFunctions(0) + .setMaxExcludedStandardFunctionOverloads(0) + .build() + .export(extendedCel); + + LibrarySubset actual = celEnvironment.standardLibrarySubset().get(); + + // "matches" is fully excluded + assertThat(actual.includedFunctions().stream().map(FunctionSelector::name)) + .doesNotContain("matches"); + + // A subset of overloads is included. Note the absence of string_to_timestamp + assertThat(actual.includedFunctions()) + .contains( + FunctionSelector.create( + "timestamp", ImmutableSet.of("int64_to_timestamp", "timestamp_to_timestamp"))); + + Set additionOverloads = + actual.includedFunctions().stream() + .filter(fs -> fs.name().equals("_+_")) + .flatMap(fs -> fs.overloads().stream()) + .map(OverloadSelector::id) + .collect(toCollection(HashSet::new)); + assertThat(additionOverloads).containsNoneOf("add_bytes", "add_list", "add_string"); + + assertThat(actual.includedMacros()).containsNoneOf("map", "filter"); + + // Random-check a few standard overloads + assertThat(additionOverloads).containsAtLeast("add_int64", "add_uint64", "add_double"); + + // Random-check a couple of standard functions + assertThat(actual.includedFunctions()) + .contains(FunctionSelector.create("-_", ImmutableSet.of())); + assertThat(actual.includedFunctions()) + .contains(FunctionSelector.create("getDayOfYear", ImmutableSet.of())); + assertThat(actual.includedMacros()).containsAtLeast("all", "exists", "exists_one", "has"); + } + + @Test + public void customFunctions() { + Cel cel = + CelFactory.standardCelBuilder() + .addCompilerLibraries(CelExtensions.math(CEL_OPTIONS, 1)) + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "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( + "timestamp_addWeeks", + SimpleType.BOOL, + SimpleType.TIMESTAMP, + SimpleType.INT))) + .build(); + + CelEnvironmentExporter exporter = + CelEnvironmentExporter.newBuilder().addStandardExtensions(CEL_OPTIONS).build(); + CelEnvironment celEnvironment = exporter.export(cel); + + assertThat(celEnvironment.functions()) + .containsAtLeast( + FunctionDecl.create( + "math.isFinite", + ImmutableSet.of( + OverloadDecl.newBuilder() + .setId("math_isFinite_int64") + .setArguments(ImmutableList.of(TypeDecl.create("int"))) + .setReturnType(TypeDecl.create("bool")) + .build())), + FunctionDecl.create( + "addWeeks", + ImmutableSet.of( + OverloadDecl.newBuilder() + .setId("timestamp_addWeeks") + .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. + assertThat( + celEnvironment.functions().stream().map(FunctionDecl::name).collect(toImmutableList())) + .containsNoneOf("_+_", "math.abs", "_in_", "__not_strictly_false__"); + } + + @Test + public void customVariables() { + Cel cel = + CelFactory.standardCelBuilder() + .addVarDeclarations( + CelVarDecl.newVarDeclaration("x", SimpleType.INT), + CelVarDecl.newVarDeclaration("y", OpaqueType.create("foo.Bar"))) + .build(); + + CelEnvironmentExporter exporter = + CelEnvironmentExporter.newBuilder().addStandardExtensions(CEL_OPTIONS).build(); + CelEnvironment celEnvironment = exporter.export(cel); + + assertThat(celEnvironment.variables()) + .containsAtLeast( + VariableDecl.create("x", TypeDecl.create("int")), + VariableDecl.create("y", TypeDecl.create("foo.Bar"))); + + // Random-check some standard variables: we don't want to see them explicitly included in + // the CelEnvironment. + assertThat( + celEnvironment.variables().stream().map(VariableDecl::name).collect(toImmutableList())) + .containsNoneOf("double", "null_type"); + } + + @Test + public void container() { + Cel cel = + CelFactory.standardCelBuilder() + .setContainer( + CelContainer.newBuilder() + .setName("cntnr") + .addAbbreviations("foo.Bar", "baz.Qux") + .addAlias("nm", "user.name") + .addAlias("id", "user.id") + .build()) + .build(); + + CelEnvironmentExporter exporter = CelEnvironmentExporter.newBuilder().build(); + CelEnvironment celEnvironment = exporter.export(cel); + 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) + .enableQuotedIdentifierSyntax(true) + .enableHeterogeneousNumericComparisons(true) + .populateMacroCalls(true) + .build()) + .build(); + + CelEnvironmentExporter exporter = CelEnvironmentExporter.newBuilder().build(); + CelEnvironment celEnvironment = exporter.export(cel); + assertThat(celEnvironment.features()) + .containsExactly( + CelEnvironment.FeatureFlag.create("cel.feature.backtick_escape_syntax", true), + CelEnvironment.FeatureFlag.create("cel.feature.cross_type_numeric_comparisons", true), + CelEnvironment.FeatureFlag.create("cel.feature.macro_call_tracking", true)); + assertThat(celEnvironment.limits()) + .containsExactly( + CelEnvironment.Limit.create("cel.limit.expression_code_points", 100), + CelEnvironment.Limit.create("cel.limit.parse_error_recovery", 10), + CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 10)); + } +} diff --git a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentTest.java b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentTest.java new file mode 100644 index 000000000..a5a2f3e6d --- /dev/null +++ b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentTest.java @@ -0,0 +1,467 @@ +// 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.bundle; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableSet; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.bundle.CelEnvironment.CanonicalCelExtension; +import dev.cel.bundle.CelEnvironment.ExtensionConfig; +import dev.cel.bundle.CelEnvironment.LibrarySubset; +import dev.cel.bundle.CelEnvironment.LibrarySubset.FunctionSelector; +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.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; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public class CelEnvironmentTest { + + @Test + public void newBuilder_defaults() { + CelEnvironment environment = CelEnvironment.newBuilder().build(); + + assertThat(environment.source()).isEmpty(); + assertThat(environment.name()).isEmpty(); + assertThat(environment.description()).isEmpty(); + assertThat(environment.container()).isEmpty(); + assertThat(environment.extensions()).isEmpty(); + assertThat(environment.variables()).isEmpty(); + assertThat(environment.functions()).isEmpty(); + } + + @Test + public void container() { + CelEnvironment environment = + CelEnvironment.newBuilder() + .setContainer( + CelContainer.newBuilder() + .setName("cntr") + .addAbbreviations("foo.Bar", "baz.Qux") + .addAlias("nm", "user.name") + .addAlias("id", "user.id") + .build()) + .build(); + + 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 + public void extend_allExtensions() throws Exception { + ImmutableSet extensionConfigs = + ImmutableSet.of( + ExtensionConfig.latest("bindings"), + ExtensionConfig.latest("encoders"), + ExtensionConfig.latest("lists"), + ExtensionConfig.latest("math"), + ExtensionConfig.latest("optional"), + ExtensionConfig.latest("protos"), + ExtensionConfig.latest("regex"), + ExtensionConfig.latest("sets"), + ExtensionConfig.latest("strings"), + ExtensionConfig.latest("two-var-comprehensions")); + CelEnvironment environment = + CelEnvironment.newBuilder().addExtensions(extensionConfigs).build(); + + Cel cel = environment.extend(CelFactory.standardCelBuilder().build(), CelOptions.DEFAULT); + CelAbstractSyntaxTree ast = + cel.compile( + "cel.bind(x, 10, math.greatest([1,x])) < int(' 11 '.trim()) &&" + + " optional.none().orValue(true) && [].flatten() == []") + .getAst(); + boolean result = (boolean) cel.createProgram(ast).eval(); + + assertThat(extensionConfigs.size()).isEqualTo(CelEnvironment.CEL_EXTENSION_CONFIG_MAP.size()); + assertThat(extensionConfigs.size()).isEqualTo(CanonicalCelExtension.values().length); + 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)) + .build(); + + Cel cel = + environment.extend( + CelFactory.standardCelBuilder() + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .build(), + CelOptions.DEFAULT); + CelOptions checkerOptions = cel.toCheckerBuilder().options(); + assertThat(checkerOptions.maxExpressionCodePointSize()).isEqualTo(20); + assertThat(checkerOptions.maxParseErrorRecoveryLimit()).isEqualTo(10); + assertThat(checkerOptions.maxParseRecursionDepth()).isEqualTo(10); + + CelAbstractSyntaxTree ast = cel.compile("1 + 2 + 3 + 4 + 5").getAst(); + Long result = (Long) cel.createProgram(ast).eval(); + assertThat(result).isEqualTo(15L); + + CelValidationResult validationResult = cel.compile("1 + 2 + 3 + 4 + 5 + 6"); + assertThat(validationResult.hasError()).isTrue(); + assertThat(validationResult.getErrorString()) + .contains("expression code point size exceeds limit: size: 21, limit 20"); + } + + @Test + public void extend_unsupportedFeatureFlag_throws() throws Exception { + CelEnvironment environment = + 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 = + CelEnvironment.newBuilder().addExtensions(ExtensionConfig.of("math", 1)).build(); + + Cel cel = environment.extend(CelFactory.standardCelBuilder().build(), CelOptions.DEFAULT); + CelAbstractSyntaxTree ast1 = cel.compile("math.abs(-4)").getAst(); + assertThat(cel.createProgram(ast1).eval()).isEqualTo(4); + + // Version 1 of the 'math' extension does not include sqrt + assertThat( + assertThrows( + CelValidationException.class, + () -> { + cel.compile("math.sqrt(4)").getAst(); + })) + .hasMessageThat() + .contains("undeclared reference to 'sqrt'"); + } + + @Test + public void extensionVersion_latest() throws Exception { + CelEnvironment environment = + CelEnvironment.newBuilder().addExtensions(ExtensionConfig.latest("math")).build(); + + Cel cel = environment.extend(CelFactory.standardCelBuilder().build(), CelOptions.DEFAULT); + CelAbstractSyntaxTree ast = cel.compile("math.sqrt(4)").getAst(); + double result = (double) cel.createProgram(ast).eval(); + assertThat(result).isEqualTo(2.0); + } + + @Test + public void extensionVersion_unsupportedVersion_throws() { + CelEnvironment environment = + CelEnvironment.newBuilder().addExtensions(ExtensionConfig.of("math", -5)).build(); + + assertThat( + assertThrows( + CelEnvironmentException.class, + () -> { + environment.extend(CelFactory.standardCelBuilder().build(), CelOptions.DEFAULT); + })) + .hasMessageThat() + .contains("Unsupported 'math' extension version -5"); + } + + @Test + public void stdlibSubset_bothIncludeExcludeSet_throws() { + assertThat( + assertThrows( + IllegalArgumentException.class, + () -> + CelEnvironment.newBuilder() + .setStandardLibrarySubset( + LibrarySubset.newBuilder() + .setDisabled(false) + .setIncludedMacros(ImmutableSet.of("foo")) + .setExcludedMacros(ImmutableSet.of("bar")) + .build()) + .build())) + .hasMessageThat() + .contains("cannot both include and exclude macros"); + + assertThat( + assertThrows( + IllegalArgumentException.class, + () -> + CelEnvironment.newBuilder() + .setStandardLibrarySubset( + LibrarySubset.newBuilder() + .setDisabled(false) + .setIncludedFunctions( + ImmutableSet.of( + FunctionSelector.create("foo", ImmutableSet.of()))) + .setExcludedFunctions( + ImmutableSet.of( + FunctionSelector.create("bar", ImmutableSet.of()))) + .build()) + .build())) + .hasMessageThat() + .contains("cannot both include and exclude functions"); + } + + @Test + public void stdlibSubset_disabled() throws Exception { + CelEnvironment environment = + CelEnvironment.newBuilder() + .setStandardLibrarySubset(LibrarySubset.newBuilder().setDisabled(true).build()) + .build(); + + CelCompiler compiler = CelCompilerFactory.standardCelCompilerBuilder().build(); + CelCompiler extendedCompiler = environment.extend(compiler, CelOptions.DEFAULT); + CelValidationResult result = extendedCompiler.compile("1 != 2"); + assertThat(result.getErrorString()).contains("undeclared reference to '_!=_'"); + } + + @Test + public void stdlibSubset_macrosDisabled() throws Exception { + CelEnvironment environment = + CelEnvironment.newBuilder() + .setStandardLibrarySubset( + LibrarySubset.newBuilder().setDisabled(false).setMacrosDisabled(true).build()) + .build(); + + CelCompiler compiler = CelCompilerFactory.standardCelCompilerBuilder().build(); + CelCompiler extendedCompiler = environment.extend(compiler, CelOptions.DEFAULT); + CelValidationResult result = + extendedCompiler.compile("['hello', 'world'].exists(v, v == 'hello')"); + assertThat(result.getErrorString()).contains("undeclared reference to 'exists'"); + } + + @Test + public void stdlibSubset_macrosIncluded() throws Exception { + CelEnvironment environment = + CelEnvironment.newBuilder() + .setStandardLibrarySubset( + LibrarySubset.newBuilder() + .setDisabled(false) + .setIncludedMacros(ImmutableSet.of(CelStandardMacro.EXISTS.getFunction())) + .build()) + .build(); + + CelCompiler compiler = CelCompilerFactory.standardCelCompilerBuilder().build(); + CelCompiler extendedCompiler = environment.extend(compiler, CelOptions.DEFAULT); + CelValidationResult result = + extendedCompiler.compile("['hello', 'world'].exists(v, v == 'hello')"); + assertThat(result.hasError()).isFalse(); + + result = extendedCompiler.compile("['hello', 'world'].exists_one(v, v == 'hello')"); + assertThat(result.getErrorString()).contains("undeclared reference to 'exists_one'"); + } + + @Test + public void stdlibSubset_macrosExcluded() throws Exception { + CelEnvironment environment = + CelEnvironment.newBuilder() + .setStandardLibrarySubset( + LibrarySubset.newBuilder() + .setDisabled(false) + .setExcludedMacros(ImmutableSet.of(CelStandardMacro.EXISTS_ONE.getFunction())) + .build()) + .build(); + + CelCompiler compiler = CelCompilerFactory.standardCelCompilerBuilder().build(); + CelCompiler extendedCompiler = environment.extend(compiler, CelOptions.DEFAULT); + CelValidationResult result = + extendedCompiler.compile("['hello', 'world'].exists(v, v == 'hello')"); + assertThat(result.hasError()).isFalse(); + + result = extendedCompiler.compile("['hello', 'world'].exists_one(v, v == 'hello')"); + assertThat(result.getErrorString()).contains("undeclared reference to 'exists_one'"); + } + + @Test + public void stdlibSubset_functionsIncluded() throws Exception { + CelEnvironment environment = + CelEnvironment.newBuilder() + .setStandardLibrarySubset( + LibrarySubset.newBuilder() + .setDisabled(false) + .setIncludedFunctions( + ImmutableSet.of( + FunctionSelector.create("_==_", ImmutableSet.of()), + FunctionSelector.create("_!=_", ImmutableSet.of()), + FunctionSelector.create("_&&_", ImmutableSet.of()))) + .build()) + .build(); + + CelCompiler compiler = CelCompilerFactory.standardCelCompilerBuilder().build(); + CelCompiler extendedCompiler = environment.extend(compiler, CelOptions.DEFAULT); + CelValidationResult result = extendedCompiler.compile("1 == 1 && 1 != 2"); + assertThat(result.hasError()).isFalse(); + + result = extendedCompiler.compile("1 == 1 && 1 != 1 + 1"); + assertThat(result.getErrorString()).contains("undeclared reference to '_+_'"); + } + + @Test + public void stdlibSubset_functionOverloadIncluded() throws Exception { + CelEnvironment environment = + CelEnvironment.newBuilder() + .setStandardLibrarySubset( + LibrarySubset.newBuilder() + .setDisabled(false) + .setIncludedFunctions( + ImmutableSet.of( + FunctionSelector.create("_+_", ImmutableSet.of("add_int64")))) + .build()) + .build(); + + CelCompiler compiler = CelCompilerFactory.standardCelCompilerBuilder().build(); + CelCompiler extendedCompiler = environment.extend(compiler, CelOptions.DEFAULT); + CelValidationResult result = extendedCompiler.compile("1 + 2"); + assertThat(result.hasError()).isFalse(); + + result = extendedCompiler.compile("1.0 + 2.0"); + assertThat(result.getErrorString()) + .contains("found no matching overload for '_+_' applied to '(double, double)'"); + } + + @Test + public void stdlibSubset_functionsExcluded() throws Exception { + CelEnvironment environment = + CelEnvironment.newBuilder() + .setStandardLibrarySubset( + LibrarySubset.newBuilder() + .setDisabled(false) + .setExcludedFunctions( + ImmutableSet.of(FunctionSelector.create("_+_", ImmutableSet.of()))) + .build()) + .build(); + + CelCompiler compiler = CelCompilerFactory.standardCelCompilerBuilder().build(); + CelCompiler extendedCompiler = environment.extend(compiler, CelOptions.DEFAULT); + CelValidationResult result = extendedCompiler.compile("1 == 1 && 1 != 2"); + assertThat(result.hasError()).isFalse(); + + result = extendedCompiler.compile("1 == 1 && 1 != 1 + 1"); + assertThat(result.getErrorString()).contains("undeclared reference to '_+_'"); + } + + @Test + public void stdlibSubset_functionOverloadExcluded() throws Exception { + CelEnvironment environment = + CelEnvironment.newBuilder() + .setStandardLibrarySubset( + LibrarySubset.newBuilder() + .setDisabled(false) + .setExcludedFunctions( + ImmutableSet.of( + FunctionSelector.create("_+_", ImmutableSet.of("add_int64")))) + .build()) + .build(); + + CelCompiler compiler = CelCompilerFactory.standardCelCompilerBuilder().build(); + CelCompiler extendedCompiler = environment.extend(compiler, CelOptions.DEFAULT); + CelValidationResult result = extendedCompiler.compile("1 == 1 && 1 != 2"); + assertThat(result.hasError()).isFalse(); + + 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 new file mode 100644 index 000000000..043664e8e --- /dev/null +++ b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlParserTest.java @@ -0,0 +1,1031 @@ +// 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.bundle; + +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.base.Ascii; +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.ExtensionConfig; +import dev.cel.bundle.CelEnvironment.FunctionDecl; +import dev.cel.bundle.CelEnvironment.LibrarySubset; +import dev.cel.bundle.CelEnvironment.LibrarySubset.FunctionSelector; +import dev.cel.bundle.CelEnvironment.OverloadDecl; +import dev.cel.bundle.CelEnvironment.TypeDecl; +import dev.cel.bundle.CelEnvironment.VariableDecl; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelContainer; +import dev.cel.common.CelOptions; +import dev.cel.common.CelSource; +import dev.cel.common.ast.CelExpr; +import dev.cel.common.types.SimpleType; +import dev.cel.parser.CelUnparserFactory; +import dev.cel.runtime.CelEvaluationListener; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.CelLateFunctionBindings; +import java.io.IOException; +import java.net.URL; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public final class CelEnvironmentYamlParserTest { + + private static final Cel CEL_WITH_MESSAGE_TYPES = + CelFactory.standardCelBuilder() + .addMessageTypes(AttributeContext.Request.getDescriptor()) + .build(); + + private static final CelEnvironmentYamlParser ENVIRONMENT_PARSER = + CelEnvironmentYamlParser.newInstance(); + + @Test + public void environment_setEmpty() throws Exception { + assertThrows(CelEnvironmentException.class, () -> ENVIRONMENT_PARSER.parse("")); + } + + @Test + public void environment_setBasicProperties() throws Exception { + String yamlConfig = "name: hello\n" + "description: empty\n" + "container: pb.pkg\n"; + + CelEnvironment environment = ENVIRONMENT_PARSER.parse(yamlConfig); + + assertThat(environment) + .isEqualTo( + CelEnvironment.newBuilder() + .setSource(environment.source().get()) + .setName("hello") + .setDescription("empty") + .setContainer("pb.pkg") + .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 = + "extensions:\n" + + " - name: 'bindings'\n" + + " - name: 'encoders'\n" + + " - name: 'lists'\n" + + " - name: 'math'\n" + + " - name: 'optional'\n" + + " - name: 'protos'\n" + + " - name: 'sets'\n" + + " - name: 'strings'\n" + + " version: 1"; + + CelEnvironment environment = ENVIRONMENT_PARSER.parse(yamlConfig); + + assertThat(environment) + .isEqualTo( + CelEnvironment.newBuilder() + .setSource(environment.source().get()) + .addExtensions( + ImmutableSet.of( + ExtensionConfig.of("bindings"), + ExtensionConfig.of("encoders"), + ExtensionConfig.of("lists"), + ExtensionConfig.of("math"), + ExtensionConfig.of("optional"), + ExtensionConfig.of("protos"), + ExtensionConfig.of("sets"), + ExtensionConfig.of("strings", 1))) + .build()); + assertThat(environment.extend(CEL_WITH_MESSAGE_TYPES, CelOptions.DEFAULT)).isNotNull(); + } + + @Test + public void environment_setExtensionVersionToLatest() throws Exception { + String yamlConfig = + "extensions:\n" // + + " - name: 'bindings'\n" // + + " version: latest"; + + CelEnvironment environment = ENVIRONMENT_PARSER.parse(yamlConfig); + + assertThat(environment) + .isEqualTo( + CelEnvironment.newBuilder() + .setSource(environment.source().get()) + .addExtensions(ImmutableSet.of(ExtensionConfig.of("bindings", Integer.MAX_VALUE))) + .build()); + assertThat(environment.extend(CEL_WITH_MESSAGE_TYPES, CelOptions.DEFAULT)).isNotNull(); + } + + @Test + public void environment_setExtensionVersionToInvalidValue() throws Exception { + String yamlConfig = + "extensions:\n" // + + " - name: 'bindings'\n" // + + " version: invalid"; + + CelEnvironmentException e = + assertThrows(CelEnvironmentException.class, () -> ENVIRONMENT_PARSER.parse(yamlConfig)); + assertThat(e) + .hasMessageThat() + .contains( + "ERROR: :3:5: Unsupported version tag: version\n" + + " | version: invalid\n" + + " | ....^"); + } + + @Test + public void environment_setFunctions() throws Exception { + String yamlConfig = + "functions:\n" + + " - name: 'coalesce'\n" + + " overloads:\n" + + " - id: 'null_coalesce_int'\n" + + " target:\n" + + " type_name: 'null_type'\n" + + " args:\n" + + " - type_name: 'int'\n" + + " return:\n" + + " type_name: 'int'\n" + + " - id: 'coalesce_null_int'\n" + + " args:\n" + + " - type_name: 'null_type'\n" + + " - type_name: 'int'\n" + + " return:\n" + + " type_name: 'int' \n" + + " - id: 'int_coalesce_int'\n" + + " target: \n" + + " type_name: 'int'\n" + + " args:\n" + + " - type_name: 'int'\n" + + " return: \n" + + " type_name: 'int'\n" + + " - id: 'optional_T_coalesce_T'\n" + + " target: \n" + + " type_name: 'optional_type'\n" + + " params:\n" + + " - type_name: 'T'\n" + + " is_type_param: true\n" + + " args:\n" + + " - type_name: 'T'\n" + + " is_type_param: true\n" + + " return: \n" + + " type_name: 'T'\n" + + " is_type_param: true"; + + CelEnvironment environment = ENVIRONMENT_PARSER.parse(yamlConfig); + + assertThat(environment) + .isEqualTo( + CelEnvironment.newBuilder() + .setSource(environment.source().get()) + .setFunctions( + ImmutableSet.of( + FunctionDecl.create( + "coalesce", + ImmutableSet.of( + OverloadDecl.newBuilder() + .setId("null_coalesce_int") + .setTarget(TypeDecl.create("null_type")) + .addArguments(TypeDecl.create("int")) + .setReturnType(TypeDecl.create("int")) + .build(), + OverloadDecl.newBuilder() + .setId("coalesce_null_int") + .addArguments( + TypeDecl.create("null_type"), TypeDecl.create("int")) + .setReturnType(TypeDecl.create("int")) + .build(), + OverloadDecl.newBuilder() + .setId("int_coalesce_int") + .setTarget(TypeDecl.create("int")) + .addArguments(TypeDecl.create("int")) + .setReturnType(TypeDecl.create("int")) + .build(), + OverloadDecl.newBuilder() + .setId("optional_T_coalesce_T") + .setTarget( + TypeDecl.newBuilder() + .setName("optional_type") + .addParams( + TypeDecl.newBuilder() + .setName("T") + .setIsTypeParam(true) + .build()) + .build()) + .addArguments( + TypeDecl.newBuilder() + .setName("T") + .setIsTypeParam(true) + .build()) + .setReturnType( + TypeDecl.newBuilder() + .setName("T") + .setIsTypeParam(true) + .build()) + .build())))) + .build()); + assertThat(environment.extend(CEL_WITH_MESSAGE_TYPES, CelOptions.DEFAULT)).isNotNull(); + } + + @Test + public void environment_setListVariable() throws Exception { + String yamlConfig = + "variables:\n" + + "- name: 'request'\n" + + " type_name: 'list'\n" + + " params:\n" + + " - type_name: 'string'"; + + 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()); + } + + @Test + public void environment_setMapVariable() 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_setMessageVariable() throws Exception { + String yamlConfig = + "variables:\n" + + "- name: 'request'\n" + + " type:\n" + + " type_name: '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_setContainer() throws Exception { + String yamlConfig = + "container: google.rpc.context\n" + + "variables:\n" + + "- name: 'request'\n" + + " type:\n" + + " type_name: 'google.rpc.context.AttributeContext.Request'"; + + CelEnvironment environment = ENVIRONMENT_PARSER.parse(yamlConfig); + + assertThat(environment) + .isEqualTo( + CelEnvironment.newBuilder() + .setContainer("google.rpc.context") + .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_setContainerWithAliasesAndAbbreviations() throws Exception { + String yamlConfig = + "container:\n" + + " name: 'google.rpc.context'\n" + + " abbreviations:\n" + + " - 'pkg3.lib3.Baz'\n" + + " - pkg4.lib4.Qux\n" + + " aliases:\n" + + " - alias: 'foo'\n" + + " qualified_name: 'pkg1.lib1.Foo'\n" + + " - alias: bar\n" + + " qualified_name: pkg2.lib2.Bar\n"; + + CelEnvironment environment = ENVIRONMENT_PARSER.parse(yamlConfig); + + assertThat(environment) + .isEqualTo( + CelEnvironment.newBuilder() + .setContainer( + CelContainer.newBuilder() + .setName("google.rpc.context") + .addAbbreviations("pkg3.lib3.Baz", "pkg4.lib4.Qux") + .addAlias("foo", "pkg1.lib1.Foo") + .addAlias("bar", "pkg2.lib2.Bar") + .build()) + .setSource(environment.source().get()) + .build()); + } + + @Test + public void environment_withInlinedVariableDecl() throws Exception { + String yamlConfig = + "variables:\n" + + "- name: 'request'\n" + + " type_name: 'google.rpc.context.AttributeContext.Request'\n" + + "- name: 'map_var'\n" + + " type_name: 'map'\n" + + " params:\n" + + " - type_name: 'string'\n" + + " - type_name: 'string'"; + + 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")), + VariableDecl.create( + "map_var", + TypeDecl.newBuilder() + .setName("map") + .addParams(TypeDecl.create("string"), TypeDecl.create("string")) + .build()))) + .build()); + assertThat(environment.extend(CEL_WITH_MESSAGE_TYPES, CelOptions.DEFAULT)).isNotNull(); + } + + @Test + public void environment_parseErrors(@TestParameter EnvironmentParseErrorTestcase testCase) { + CelEnvironmentException e = + assertThrows( + CelEnvironmentException.class, () -> ENVIRONMENT_PARSER.parse(testCase.yamlConfig)); + assertThat(e).hasMessageThat().isEqualTo(testCase.expectedErrorMessage); + } + + @Test + public void environment_extendErrors(@TestParameter EnvironmentExtendErrorTestCase testCase) + throws Exception { + CelEnvironment environment = ENVIRONMENT_PARSER.parse(testCase.yamlConfig); + + CelEnvironmentException e = + assertThrows( + CelEnvironmentException.class, + () -> environment.extend(CEL_WITH_MESSAGE_TYPES, CelOptions.DEFAULT)); + assertThat(e).hasMessageThat().isEqualTo(testCase.expectedErrorMessage); + } + + // Note: dangling comments in expressions below is to retain the newlines by preventing auto + // formatter from compressing them in a single line. + private enum EnvironmentParseErrorTestcase { + MALFORMED_YAML_DOCUMENT( + "a:\na", + "YAML document is malformed: while scanning a simple key\n" + + " in 'reader', line 2, column 1:\n" + + " a\n" + + " ^\n" + + "could not find expected ':'\n" + + " in 'reader', line 2, column 2:\n" + + " a\n" + + " ^\n"), + ILLEGAL_YAML_TYPE_CONFIG_KEY( + "1: test", + "ERROR: :1:1: Got yaml node type tag:yaml.org,2002:int, wanted type(s)" + + " [tag:yaml.org,2002:str !txt]\n" + + " | 1: test\n" + + " | ^"), + ILLEGAL_YAML_TYPE_CONFIG_VALUE( + "test: 1", "ERROR: :1:1: Unknown config tag: test\n" + " | test: 1\n" + " | ^"), + ILLEGAL_YAML_TYPE_VARIABLE_LIST( + "variables: 1", + "ERROR: :1:12: Got yaml node type tag:yaml.org,2002:int, wanted type(s)" + + " [tag:yaml.org,2002:seq]\n" + + " | variables: 1\n" + + " | ...........^"), + ILLEGAL_YAML_TYPE_VARIABLE_VALUE( + "variables:\n - 1", + "ERROR: :2:4: Got yaml node type tag:yaml.org,2002:int, wanted type(s)" + + " [tag:yaml.org,2002:map]\n" + + " | - 1\n" + + " | ...^"), + ILLEGAL_YAML_TYPE_FUNCTION_LIST( + "functions: 1", + "ERROR: :1:12: Got yaml node type tag:yaml.org,2002:int, wanted type(s)" + + " [tag:yaml.org,2002:seq]\n" + + " | functions: 1\n" + + " | ...........^"), + ILLEGAL_YAML_TYPE_FUNCTION_VALUE( + "functions:\n - 1", + "ERROR: :2:4: Got yaml node type tag:yaml.org,2002:int, wanted type(s)" + + " [tag:yaml.org,2002:map]\n" + + " | - 1\n" + + " | ...^"), + ILLEGAL_YAML_TYPE_OVERLOAD_LIST( + "functions:\n" // + + " - name: foo\n" // + + " overloads: 1", + "ERROR: :3:15: Got yaml node type tag:yaml.org,2002:int, wanted type(s)" + + " [tag:yaml.org,2002:seq]\n" + + " | overloads: 1\n" + + " | ..............^"), + ILLEGAL_YAML_TYPE_OVERLOAD_VALUE( + "functions:\n" // + + " - name: foo\n" // + + " overloads:\n" // + + " - 2", + "ERROR: :4:7: Got yaml node type tag:yaml.org,2002:int, wanted type(s)" + + " [tag:yaml.org,2002:map]\n" + + " | - 2\n" + + " | ......^"), + ILLEGAL_YAML_TYPE_OVERLOAD_VALUE_MAP_KEY( + "functions:\n" // + + " - name: foo\n" // + + " overloads:\n" // + + " - 2: test", + "ERROR: :4:9: Got yaml node type tag:yaml.org,2002:int, wanted type(s)" + + " [tag:yaml.org,2002:str !txt]\n" + + " | - 2: test\n" + + " | ........^\n" + + "ERROR: :4:9: Missing required attribute(s): id, return\n" + + " | - 2: test\n" + + " | ........^"), + ILLEGAL_YAML_TYPE_EXTENSION_LIST( + "extensions: 1", + "ERROR: :1:13: Got yaml node type tag:yaml.org,2002:int, wanted type(s)" + + " [tag:yaml.org,2002:seq]\n" + + " | extensions: 1\n" + + " | ............^"), + ILLEGAL_YAML_TYPE_EXTENSION_VALUE( + "extensions:\n - 1", + "ERROR: :2:4: Got yaml node type tag:yaml.org,2002:int, wanted type(s)" + + " [tag:yaml.org,2002:map]\n" + + " | - 1\n" + + " | ...^"), + ILLEGAL_YAML_TYPE_TYPE_DECL( + "variables:\n" // + + " - 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" + + " | type: 1\n" + + " | .........^"), + ILLEGAL_YAML_TYPE_TYPE_VALUE( + "variables:\n" + + " - name: foo\n" + + " type:\n" + + " type_name: bar\n" + + " 1: hello", + "ERROR: :5:6: Got yaml node type tag:yaml.org,2002:int, wanted type(s)" + + " [tag:yaml.org,2002:str !txt]\n" + + " | 1: hello\n" + + " | .....^"), + ILLEGAL_YAML_TYPE_TYPE_PARAMS_LIST( + "variables:\n" + + " - name: foo\n" + + " type:\n" + + " type_name: bar\n" + + " params: 1", + "ERROR: :5:14: Got yaml node type tag:yaml.org,2002:int, wanted type(s)" + + " [tag:yaml.org,2002:seq]\n" + + " | params: 1\n" + + " | .............^"), + ILLEGAL_YAML_TYPE_WITH_INLINED_TYPE_TAGS( + "variables:\n" + + " - name: foo\n" + + " type_name: bar\n" + + " type:\n" + + " type_name: qux\n", + "ERROR: :4:4: 'type' tag cannot be used together with inlined 'type_name'," + + " 'is_type_param' or 'params': type\n" + + " | type:\n" + + " | ...^"), + ILLEGAL_YAML_INLINED_TYPE_VALUE( + "variables:\n" // + + " - name: foo\n" // + + " type_name: 1\n", + "ERROR: :3:15: Got yaml node type tag:yaml.org,2002:int, wanted type(s)" + + " [tag:yaml.org,2002:str !txt]\n" + + " | type_name: 1\n" + + " | ..............^"), + UNSUPPORTED_CONFIG_TAG( + "unsupported: test", + "ERROR: :1:1: Unknown config tag: unsupported\n" + + " | unsupported: test\n" + + " | ^"), + UNSUPPORTED_EXTENSION_TAG( + "extensions:\n" // + + " - name: foo\n" // + + " unsupported: test", + "ERROR: :3:5: Unsupported extension tag: unsupported\n" + + " | unsupported: test\n" + + " | ....^"), + UNSUPPORTED_TYPE_DECL_TAG( + "variables:\n" + + "- name: foo\n" + + " type:\n" + + " type_name: bar\n" + + " unsupported: hello", + "ERROR: :5:6: Unsupported type decl tag: unsupported\n" + + " | unsupported: hello\n" + + " | .....^"), + MISSING_VARIABLE_PROPERTIES( + "variables:\n - illegal: 2", + "ERROR: :2:4: Unsupported variable tag: illegal\n" + + " | - illegal: 2\n" + + " | ...^\n" + + "ERROR: :2:4: Missing required attribute(s): name, type\n" + + " | - illegal: 2\n" + + " | ...^"), + MISSING_OVERLOAD_RETURN( + "functions:\n" + + " - name: 'missing_return'\n" + + " overloads:\n" + + " - id: 'zero_arity'\n", + "ERROR: :4:9: Missing required attribute(s): return\n" + + " | - id: 'zero_arity'\n" + + " | ........^"), + MISSING_FUNCTION_NAME( + "functions:\n" + + " - overloads:\n" + + " - id: 'foo'\n" + + " return:\n" + + " type_name: 'string'\n", + "ERROR: :2:5: Missing required attribute(s): name\n" + + " | - overloads:\n" + + " | ....^"), + MISSING_OVERLOAD( + "functions:\n" + " - name: 'missing_overload'\n", + "ERROR: :2:5: Missing required attribute(s): overloads\n" + + " | - name: 'missing_overload'\n" + + " | ....^"), + MISSING_EXTENSION_NAME( + "extensions:\n" + "- version: 0", + "ERROR: :2:3: Missing required attribute(s): name\n" + + " | - version: 0\n" + + " | ..^"), + ILLEGAL_LIBRARY_SUBSET_TAG( + "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" + + " | ..^"), + ILLEGAL_LIBRARY_SUBSET_FUNCTION_SELECTOR_TAG( + "name: 'test_suite_name'\n" + + "stdlib:\n" + + " include_functions:\n" + + " - name: 'test_function'\n" + + " unknown_tag: 'test_value'\n", + "ERROR: :5:7: Unsupported function selector tag: unknown_tag\n" + + " | unknown_tag: 'test_value'\n" + + " | ......^"), + MISSING_LIBRARY_SUBSET_FUNCTION_SELECTOR_NAME( + "name: 'test_suite_name'\n" + + "stdlib:\n" + + " include_functions:\n" + + " - overloads:\n" + + " - id: add_bytes\n", + "ERROR: :4:7: Missing required attribute(s): name\n" + + " | - overloads:\n" + + " | ......^"), + ILLEGAL_LIBRARY_SUBSET_OVERLOAD_SELECTOR_TAG( + "name: 'test_suite_name'\n" + + "stdlib:\n" + + " include_functions:\n" + + " - name: _+_\n" + + " overloads:\n" + + " - id: test_overload\n" + + " unknown_tag: 'test_value'\n", + "ERROR: :7:11: Unsupported overload selector tag: unknown_tag\n" + + " | unknown_tag: 'test_value'\n" + + " | ..........^"), + MISSING_ALIAS_FIELDS( + "container:\n" + + " name: 'test_container'\n" + + " aliases:\n" + + " - qualified_name: 'test_qualified_name'\n", + "ERROR: :4:7: Missing required attribute(s): alias\n" + + " | - qualified_name: 'test_qualified_name'\n" + + " | ......^"), + ILLEGAL_ALIAS_TAG( + "container:\n" + + " name: 'test_container'\n" + + " aliases:\n" + + " - alias: 'test_alias'\n" + + " qualified_name: 'test_qualified_name'\n" + + " unknown_tag: 'test_value'\n", + "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; + private final String expectedErrorMessage; + + EnvironmentParseErrorTestcase(String yamlConfig, String expectedErrorMessage) { + this.yamlConfig = yamlConfig; + this.expectedErrorMessage = expectedErrorMessage; + } + } + + private enum EnvironmentExtendErrorTestCase { + BAD_EXTENSION("extensions:\n" + " - name: 'bad_name'", "Unrecognized extension: bad_name"), + BAD_TYPE( + "variables:\n" + "- name: 'bad_type'\n" + " type:\n" + " type_name: 'strings'", + "Undefined type name: strings"), + BAD_LIST( + "variables:\n" + " - name: 'bad_list'\n" + " type:\n" + " type_name: 'list'", + "List type has unexpected param count: 0"), + BAD_MAP( + "variables:\n" + + " - name: 'bad_map'\n" + + " type:\n" + + " type_name: 'map'\n" + + " params:\n" + + " - type_name: 'string'", + "Map type has unexpected param count: 1"), + BAD_LIST_TYPE_PARAM( + "variables:\n" + + " - name: 'bad_list_type_param'\n" + + " type:\n" + + " type_name: 'list'\n" + + " params:\n" + + " - type_name: 'number'", + "Undefined type name: number"), + BAD_MAP_TYPE_PARAM( + "variables:\n" + + " - name: 'bad_map_type_param'\n" + + " type:\n" + + " type_name: 'map'\n" + + " params:\n" + + " - type_name: 'string'\n" + + " - type_name: 'optional'", + "Undefined type name: optional"), + BAD_RETURN( + "functions:\n" + + " - name: 'bad_return'\n" + + " overloads:\n" + + " - id: 'zero_arity'\n" + + " return:\n" + + " type_name: 'mystery'", + "Undefined type name: mystery"), + BAD_OVERLOAD_TARGET( + "functions:\n" + + " - name: 'bad_target'\n" + + " overloads:\n" + + " - id: 'unary_member'\n" + + " target:\n" + + " type_name: 'unknown'\n" + + " return:\n" + + " type_name: 'null_type'", + "Undefined type name: unknown"), + BAD_OVERLOAD_ARG( + "functions:\n" + + " - name: 'bad_arg'\n" + + " overloads:\n" + + " - id: 'unary_global'\n" + + " args:\n" + + " - type_name: 'unknown'\n" + + " return:\n" + + " type_name: 'null_type'", + "Undefined type name: unknown"), + ; + + private final String yamlConfig; + private final String expectedErrorMessage; + + EnvironmentExtendErrorTestCase(String yamlConfig, String expectedErrorMessage) { + this.yamlConfig = yamlConfig; + this.expectedErrorMessage = expectedErrorMessage; + } + } + + @SuppressWarnings("ImmutableEnumChecker") // Test only + private enum EnvironmentYamlResourceTestCase { + EXTENDED_ENV( + "environment/extended_env.yaml", + CelEnvironment.newBuilder() + .setName("extended-env") + .setContainer("cel.expr") + .addExtensions( + ImmutableSet.of( + ExtensionConfig.of("optional", 2), + ExtensionConfig.of("math", Integer.MAX_VALUE))) + .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.newBuilder() + .setName("isEmpty") + .setDescription( + "determines whether a list is empty,\nor a string has no characters") + .setOverloads( + ImmutableSet.of( + OverloadDecl.newBuilder() + .setId("wrapper_string_isEmpty") + .setTarget(TypeDecl.create("google.protobuf.StringValue")) + .addExamples("''.isEmpty() // true") + .setReturnType(TypeDecl.create("bool")) + .build(), + OverloadDecl.newBuilder() + .setId("list_isEmpty") + .addExamples("[].isEmpty() // true") + .addExamples("[1].isEmpty() // false") + .setTarget( + TypeDecl.newBuilder() + .setName("list") + .addParams( + TypeDecl.newBuilder() + .setName("T") + .setIsTypeParam(true) + .build()) + .build()) + .setReturnType(TypeDecl.create("bool")) + .build())) + .build()) + .setFeatures(CelEnvironment.FeatureFlag.create("cel.feature.macro_call_tracking", true)) + .setLimits( + ImmutableSet.of( + CelEnvironment.Limit.create("cel.limit.expression_code_points", 1000), + CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 7))) + .build()), + + LIBRARY_SUBSET_ENV( + "environment/subset_env.yaml", + CelEnvironment.newBuilder() + .setName("subset-env") + .setStandardLibrarySubset( + LibrarySubset.newBuilder() + .setDisabled(false) + .setExcludedMacros(ImmutableSet.of("map", "filter")) + .setExcludedFunctions( + ImmutableSet.of( + FunctionSelector.create( + "_+_", ImmutableSet.of("add_bytes", "add_list", "add_string")), + FunctionSelector.create("matches", ImmutableSet.of()), + FunctionSelector.create( + "timestamp", ImmutableSet.of("string_to_timestamp")), + FunctionSelector.create( + "duration", ImmutableSet.of("string_to_duration")))) + .build()) + .setVariables( + VariableDecl.create("x", TypeDecl.create("int")), + VariableDecl.create("y", TypeDecl.create("double")), + VariableDecl.create("z", TypeDecl.create("uint"))) + .build()), + ; + + private final String yamlFileContent; + private final CelEnvironment expectedEnvironment; + + EnvironmentYamlResourceTestCase(String yamlResourcePath, CelEnvironment expectedEnvironment) { + try { + this.yamlFileContent = readFile(yamlResourcePath); + } catch (IOException e) { + throw new RuntimeException(e); + } + this.expectedEnvironment = expectedEnvironment; + } + } + + @Test + public void environment_withYamlResource(@TestParameter EnvironmentYamlResourceTestCase testCase) + throws Exception { + CelEnvironment environment = ENVIRONMENT_PARSER.parse(testCase.yamlFileContent); + + // Empty out the parsed yaml source, as it's not relevant in the assertion here, and that it's + // only obtainable through the yaml parser. + environment = environment.toBuilder().setSource(Optional.empty()).build(); + assertThat(environment).isEqualTo(testCase.expectedEnvironment); + } + + @Test + public void lateBoundFunction_evaluate_callExpr() throws Exception { + String configSource = + "name: late_bound_function_config\n" + + "functions:\n" + + " - name: 'test'\n" + + " overloads:\n" + + " - id: 'test_bool'\n" + + " args:\n" + + " - type_name: 'bool'\n" + + " return:\n" + + " type_name: 'bool'"; + CelEnvironment celEnvironment = ENVIRONMENT_PARSER.parse(configSource); + Cel celDetails = + CelFactory.standardCelBuilder() + .addVar("a", SimpleType.INT) + .addVar("b", SimpleType.INT) + .addVar("c", SimpleType.INT) + .build(); + Cel cel = celEnvironment.extend(celDetails, CelOptions.DEFAULT); + CelAbstractSyntaxTree ast = cel.compile("a < 0 && b < 0 && c < 0 && test(a<0)").getAst(); + CelLateFunctionBindings bindings = + CelLateFunctionBindings.from( + CelFunctionBinding.from("test_bool", Boolean.class, result -> result)); + + boolean result = + (boolean) cel.createProgram(ast).eval(ImmutableMap.of("a", -1, "b", -1, "c", -4), bindings); + + assertThat(result).isTrue(); + } + + @Test + public void lateBoundFunction_trace_callExpr_identifyFalseBranch() throws Exception { + AtomicReference capturedExpr = new AtomicReference<>(); + CelEvaluationListener listener = + (expr, res) -> { + if (res instanceof Boolean && !(boolean) res && capturedExpr.get() == null) { + capturedExpr.set(expr); + } + }; + + String configSource = + "name: late_bound_function_config\n" + + "functions:\n" + + " - name: 'test'\n" + + " overloads:\n" + + " - id: 'test_bool'\n" + + " args:\n" + + " - type_name: 'bool'\n" + + " return:\n" + + " type_name: 'bool'"; + + CelEnvironment celEnvironment = ENVIRONMENT_PARSER.parse(configSource); + Cel celDetails = + CelFactory.standardCelBuilder() + .addVar("a", SimpleType.INT) + .addVar("b", SimpleType.INT) + .addVar("c", SimpleType.INT) + .build(); + Cel cel = celEnvironment.extend(celDetails, CelOptions.DEFAULT); + CelAbstractSyntaxTree ast = cel.compile("a < 0 && b < 0 && c < 0 && test(a<0)").getAst(); + CelLateFunctionBindings bindings = + CelLateFunctionBindings.from( + CelFunctionBinding.from("test_bool", Boolean.class, result -> result)); + + boolean result = + (boolean) + cel.createProgram(ast) + .trace(ImmutableMap.of("a", -1, "b", 1, "c", -4), bindings, listener); + + assertThat(result).isFalse(); + // Demonstrate that "b < 0" is what caused the expression to be false + CelAbstractSyntaxTree subtree = + CelAbstractSyntaxTree.newParsedAst(capturedExpr.get(), CelSource.newBuilder().build()); + assertThat(CelUnparserFactory.newUnparser().unparse(subtree)).isEqualTo("b < 0"); + } + + private static String readFile(String path) throws IOException { + URL url = Resources.getResource(Ascii.toLowerCase(path)); + return Resources.toString(url, UTF_8); + } +} diff --git a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlSerializerTest.java b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlSerializerTest.java new file mode 100644 index 000000000..aad72a578 --- /dev/null +++ b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlSerializerTest.java @@ -0,0 +1,250 @@ +// 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.bundle; + +import static com.google.common.truth.Truth.assertThat; +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.base.Ascii; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.io.Resources; +import dev.cel.bundle.CelEnvironment.ExtensionConfig; +import dev.cel.bundle.CelEnvironment.FunctionDecl; +import dev.cel.bundle.CelEnvironment.LibrarySubset; +import dev.cel.bundle.CelEnvironment.LibrarySubset.FunctionSelector; +import dev.cel.bundle.CelEnvironment.OverloadDecl; +import dev.cel.bundle.CelEnvironment.TypeDecl; +import dev.cel.bundle.CelEnvironment.VariableDecl; +import dev.cel.common.CelContainer; +import java.io.IOException; +import java.net.URL; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class CelEnvironmentYamlSerializerTest { + + @Test + public void toYaml_success() throws Exception { + CelEnvironment environment = + CelEnvironment.newBuilder() + .setName("dump_env") + .setDescription("dump_env description") + .setContainer( + CelContainer.newBuilder() + .setName("test.container") + .addAbbreviations("abbr1.Abbr1", "abbr2.Abbr2") + .addAlias("alias1", "qual.name1") + .addAlias("alias2", "qual.name2") + .build()) + .addExtensions( + ImmutableSet.of( + ExtensionConfig.of("bindings"), + ExtensionConfig.of("encoders"), + ExtensionConfig.of("lists"), + ExtensionConfig.of("math"), + ExtensionConfig.of("optional"), + ExtensionConfig.of("protos"), + ExtensionConfig.of("sets"), + ExtensionConfig.of("strings", 1))) + .setVariables( + ImmutableSet.of( + VariableDecl.create( + "request", TypeDecl.create("google.rpc.context.AttributeContext.Request")), + VariableDecl.create( + "map_var", + TypeDecl.newBuilder() + .setName("map") + .addParams(TypeDecl.create("string")) + .addParams(TypeDecl.create("string")) + .build()))) + .setFunctions( + ImmutableSet.of( + FunctionDecl.create( + "getOrDefault", + ImmutableSet.of( + OverloadDecl.newBuilder() + .setId("getOrDefault_key_value") + .setTarget( + TypeDecl.newBuilder() + .setName("map") + .addParams( + TypeDecl.newBuilder() + .setName("K") + .setIsTypeParam(true) + .build()) + .addParams( + TypeDecl.newBuilder() + .setName("V") + .setIsTypeParam(true) + .build()) + .build()) + .setArguments( + ImmutableList.of( + TypeDecl.newBuilder() + .setName("K") + .setIsTypeParam(true) + .build(), + TypeDecl.newBuilder() + .setName("V") + .setIsTypeParam(true) + .build())) + .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( + OverloadDecl.newBuilder() + .setId("coalesce_null_int") + .setTarget(TypeDecl.create("google.protobuf.Int64Value")) + .setArguments(ImmutableList.of(TypeDecl.create("int"))) + .setReturnType(TypeDecl.create("int")) + .build())))) + .setStandardLibrarySubset( + LibrarySubset.newBuilder() + .setDisabled(true) + .setMacrosDisabled(true) + .setIncludedMacros(ImmutableSet.of("has", "exists")) + .setIncludedFunctions( + ImmutableSet.of( + FunctionSelector.create("_!=_", ImmutableSet.of()), + 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); + try { + String yamlFileContent = readFile("environment/dump_env.yaml"); + // Strip the license at the beginning of the file + String expectedOutput = yamlFileContent.replaceAll("#.*\\n", "").replaceAll("^\\n", ""); + assertThat(yamlOutput).isEqualTo(expectedOutput); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static String readFile(String path) throws IOException { + URL url = Resources.getResource(Ascii.toLowerCase(path)); + return Resources.toString(url, UTF_8); + } + + @Test + public void standardLibrary_excludeMacrosAndFunctions() throws Exception { + CelEnvironment environment = + CelEnvironment.newBuilder() + .setName("dump_env") + .setStandardLibrarySubset( + LibrarySubset.newBuilder() + .setDisabled(false) + .setExcludedMacros(ImmutableSet.of("has", "exists")) + .setExcludedFunctions( + ImmutableSet.of( + FunctionSelector.create("_!=_", ImmutableSet.of()), + FunctionSelector.create( + "_+_", ImmutableSet.of("add_bytes", "add_list")))) + .build()) + .build(); + + String yamlOutput = CelEnvironmentYamlSerializer.toYaml(environment); + + String expectedYaml = + "name: dump_env\n" + + "stdlib:\n" + + " exclude_macros:\n" + + " - exists\n" + + " - has\n" + + " exclude_functions:\n" + + " - name: _!=_\n" + + " - name: _+_\n" + + " overloads:\n" + + " - id: add_bytes\n" + + " - id: add_list\n"; + + assertThat(yamlOutput).isEqualTo(expectedYaml); + } +} diff --git a/bundle/src/test/java/dev/cel/bundle/CelImplTest.java b/bundle/src/test/java/dev/cel/bundle/CelImplTest.java index 2b3cf30bb..a3ad60d40 100644 --- a/bundle/src/test/java/dev/cel/bundle/CelImplTest.java +++ b/bundle/src/test/java/dev/cel/bundle/CelImplTest.java @@ -16,6 +16,9 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; +import static dev.cel.common.CelFunctionDecl.newFunctionDeclaration; +import static dev.cel.common.CelOverloadDecl.newGlobalOverload; +import static dev.cel.common.CelOverloadDecl.newMemberOverload; import static org.junit.Assert.assertThrows; import dev.cel.expr.CheckedExpr; @@ -38,14 +41,21 @@ import com.google.common.util.concurrent.MoreExecutors; import com.google.protobuf.Any; import com.google.protobuf.ByteString; +import com.google.protobuf.DescriptorProtos.FileDescriptorProto; import com.google.protobuf.DescriptorProtos.FileDescriptorSet; +import com.google.protobuf.Descriptors.Descriptor; +import com.google.protobuf.Descriptors.FileDescriptor; import com.google.protobuf.Duration; +import com.google.protobuf.DynamicMessage; +import com.google.protobuf.Empty; +import com.google.protobuf.ExtensionRegistry; import com.google.protobuf.FieldMask; import com.google.protobuf.Message; -import com.google.protobuf.NullValue; import com.google.protobuf.Struct; +import com.google.protobuf.TextFormat; import com.google.protobuf.Timestamp; -import com.google.protobuf.util.Timestamps; +import com.google.protobuf.TypeRegistry; +import com.google.protobuf.WrappersProto; import com.google.rpc.context.AttributeContext; import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; @@ -55,20 +65,24 @@ import dev.cel.checker.ProtoTypeMask; import dev.cel.checker.TypeProvider; import dev.cel.common.CelAbstractSyntaxTree; -import dev.cel.common.CelFunctionDecl; +import dev.cel.common.CelContainer; +import dev.cel.common.CelDescriptorUtil; +import dev.cel.common.CelErrorCode; import dev.cel.common.CelIssue; import dev.cel.common.CelOptions; -import dev.cel.common.CelOverloadDecl; import dev.cel.common.CelProtoAbstractSyntaxTree; +import dev.cel.common.CelSource.Extension; +import dev.cel.common.CelSourceLocation; import dev.cel.common.CelValidationException; import dev.cel.common.CelValidationResult; import dev.cel.common.CelVarDecl; import dev.cel.common.ast.CelExpr; -import dev.cel.common.ast.CelExpr.CelCreateList; +import dev.cel.common.ast.CelExpr.CelList; import dev.cel.common.testing.RepeatedTestProvider; import dev.cel.common.types.CelKind; +import dev.cel.common.types.CelProtoMessageTypes; +import dev.cel.common.types.CelProtoTypes; import dev.cel.common.types.CelType; -import dev.cel.common.types.CelTypes; import dev.cel.common.types.EnumType; import dev.cel.common.types.ListType; import dev.cel.common.types.MapType; @@ -76,33 +90,45 @@ import dev.cel.common.types.ProtoMessageTypeProvider; import dev.cel.common.types.SimpleType; import dev.cel.common.types.StructTypeReference; +import dev.cel.common.values.CelByteString; +import dev.cel.common.values.NullValue; import dev.cel.compiler.CelCompiler; import dev.cel.compiler.CelCompilerFactory; import dev.cel.compiler.CelCompilerImpl; +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; import dev.cel.runtime.CelAttribute.Qualifier; import dev.cel.runtime.CelAttributePattern; import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.CelEvaluationExceptionBuilder; +import dev.cel.runtime.CelFunctionBinding; import dev.cel.runtime.CelRuntime; -import dev.cel.runtime.CelRuntime.CelFunctionBinding; import dev.cel.runtime.CelRuntime.Program; import dev.cel.runtime.CelRuntimeFactory; import dev.cel.runtime.CelRuntimeLegacyImpl; import dev.cel.runtime.CelUnknownSet; import dev.cel.runtime.CelVariableResolver; import dev.cel.runtime.UnknownContext; +import dev.cel.testing.CelRuntimeFlavor; +import dev.cel.testing.testdata.SingleFile; +import dev.cel.testing.testdata.SingleFileExtensionsProto; import dev.cel.testing.testdata.proto3.StandaloneGlobalEnum; -import dev.cel.testing.testdata.proto3.TestAllTypesProto.TestAllTypes; +import java.time.Instant; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ThreadPoolExecutor; -import org.jspecify.nullness.Nullable; +import org.jspecify.annotations.Nullable; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; @@ -150,10 +176,10 @@ public final class CelImplTest { private static final CheckedExpr CHECKED_EXPR = CheckedExpr.newBuilder() .setExpr(EXPR) - .putTypeMap(1L, CelTypes.BOOL) - .putTypeMap(2L, CelTypes.BOOL) - .putTypeMap(3L, CelTypes.BOOL) - .putTypeMap(4L, CelTypes.BOOL) + .putTypeMap(1L, CelProtoTypes.BOOL) + .putTypeMap(2L, CelProtoTypes.BOOL) + .putTypeMap(3L, CelProtoTypes.BOOL) + .putTypeMap(4L, CelProtoTypes.BOOL) .putReferenceMap(2L, Reference.newBuilder().addOverloadId("logical_and").build()) .putReferenceMap(3L, Reference.newBuilder().addOverloadId("logical_not").build()) .build(); @@ -181,13 +207,11 @@ public void build_badFileDescriptorSet() { IllegalArgumentException.class, () -> standardCelBuilderWithMacros() - .setContainer("google.rpc.context.AttributeContext") + .setContainer(CelContainer.ofName("cel.expr.conformance.proto2")) .addFileTypes( FileDescriptorSet.newBuilder() - .addFile(AttributeContext.getDescriptor().getFile().toProto()) + .addFile(TestAllTypesExtensions.getDescriptor().getFile().toProto()) .build()) - .setProtoResultType( - CelTypes.createMessage("google.rpc.context.AttributeContext.Resource")) .build()); assertThat(e).hasMessageThat().contains("file descriptor set with unresolved proto file"); } @@ -213,7 +237,7 @@ public void check() throws Exception { public void compile(boolean useProtoResultType) throws Exception { CelBuilder celBuilder = standardCelBuilderWithMacros(); if (useProtoResultType) { - celBuilder.setProtoResultType(CelTypes.BOOL); + celBuilder.setProtoResultType(CelProtoTypes.BOOL); } else { celBuilder.setResultType(SimpleType.BOOL); } @@ -227,7 +251,7 @@ public void compile(boolean useProtoResultType) throws Exception { public void compile_resultTypeCheckFailure(boolean useProtoResultType) { CelBuilder celBuilder = standardCelBuilderWithMacros(); if (useProtoResultType) { - celBuilder.setProtoResultType(CelTypes.STRING); + celBuilder.setProtoResultType(CelProtoTypes.STRING); } else { celBuilder.setResultType(SimpleType.STRING); } @@ -245,13 +269,13 @@ public void compile_combinedTypeProvider() { new ProtoMessageTypeProvider(ImmutableList.of(AttributeContext.getDescriptor())); Cel cel = standardCelBuilderWithMacros() - .setContainer("google") + .setContainer(CelContainer.ofName("google")) .setTypeProvider(celTypeProvider) .addMessageTypes(com.google.type.Expr.getDescriptor()) .addProtoTypeMasks( ImmutableList.of(ProtoTypeMask.ofAllFields("google.rpc.context.AttributeContext"))) .addVar("condition", StructTypeReference.create("google.type.Expr")) - .setProtoResultType(CelTypes.BOOL) + .setProtoResultType(CelProtoTypes.BOOL) .build(); CelValidationResult result = cel.compile("type.Expr{expression: \"'hello'\"}.expression == condition.expression"); @@ -266,7 +290,7 @@ public void compile_customTypeProvider() { AttributeContext.getDescriptor(), com.google.type.Expr.getDescriptor())); Cel cel = standardCelBuilderWithMacros() - .setContainer("google") + .setContainer(CelContainer.ofName("google")) .setTypeProvider(celTypeProvider) .addVar("condition", StructTypeReference.create("google.type.Expr")) .setResultType(SimpleType.BOOL) @@ -283,7 +307,8 @@ public void compile_customTypesWithAliasingCombinedProviders() throws Exception // However, the first type resolution from the alias to the qualified type name won't be // sufficient as future checks will expect the resolved alias to also be a type. TypeProvider customTypeProvider = - aliasingProvider(ImmutableMap.of("Condition", CelTypes.createMessage("google.type.Expr"))); + aliasingProvider( + ImmutableMap.of("Condition", CelProtoTypes.createMessage("google.type.Expr"))); // The registration of the aliasing TypeProvider and the google.type.Expr descriptor // ensures that once the alias is resolved, the additional details about the Expr type @@ -315,9 +340,9 @@ public void compile_customTypesWithAliasingSelfContainedProvider() throws Except aliasingProvider( ImmutableMap.of( "Condition", - CelTypes.createMessage("google.type.Expr"), + CelProtoTypes.createMessage("google.type.Expr"), "google.type.Expr", - CelTypes.createMessage("google.type.Expr"))); + CelProtoTypes.createMessage("google.type.Expr"))); // The registration of the aliasing TypeProvider and the google.type.Expr descriptor // ensures that once the alias is resolved, the additional details about the Expr type @@ -352,6 +377,7 @@ public void program_setTypeFactoryOnAnyPackedMessage_fieldSelectionSuccess() thr CelAbstractSyntaxTree ast = celCompiler.compile("input.expression").getAst(); CelRuntime celRuntime = CelRuntimeFactory.standardCelRuntimeBuilder() + // CEL-Internal-2 .setTypeFactory( (typeName) -> typeName.equals("google.type.Expr") ? com.google.type.Expr.newBuilder() : null) @@ -381,6 +407,7 @@ public void program_setTypeFactoryOnAnyPackedMessage_messageConstructionSucceeds CelRuntime celRuntime = CelRuntimeFactory.standardCelRuntimeBuilder() + // CEL-Internal-2 .setTypeFactory( (typeName) -> typeName.equals("google.type.Expr") ? com.google.type.Expr.newBuilder() : null) @@ -398,6 +425,7 @@ public void program_setTypeFactoryOnAnyPackedMessage_messageConstructionSucceeds } @Test + @SuppressWarnings("unused") // testRunIndex name retained for test result readability public void program_concurrentMessageConstruction_succeeds( @TestParameter(valuesProvider = RepeatedTestProvider.class) int testRunIndex) throws Exception { @@ -405,7 +433,7 @@ public void program_concurrentMessageConstruction_succeeds( int threadCount = 10; Cel cel = standardCelBuilderWithMacros() - .setContainer("google.rpc.context.AttributeContext") + .setContainer(CelContainer.ofName("google.rpc.context.AttributeContext")) .addFileTypes( Any.getDescriptor().getFile(), Duration.getDescriptor().getFile(), @@ -463,7 +491,10 @@ public void compile_typeCheckFailure() { assertThat(syntaxErrorResult.hasError()).isTrue(); assertThat(syntaxErrorResult.getErrors()) .containsExactly( - CelIssue.formatError(1, 0, "undeclared reference to 'variable' (in container '')")); + CelIssue.formatError( + /* exprId= */ 1L, + CelSourceLocation.of(1, 0), + "undeclared reference to 'variable' (in container '')")); assertThat(syntaxErrorResult.getErrorString()) .isEqualTo( "ERROR: :1:1: undeclared reference to 'variable' (in container '')\n" @@ -481,9 +512,9 @@ public void compile_withOptionalTypes() throws Exception { CelAbstractSyntaxTree ast = cel.compile("[?a]").getAst(); - CelCreateList createList = ast.getExpr().createList(); - assertThat(createList.optionalIndices()).containsExactly(0); - assertThat(createList.elements()).containsExactly(CelExpr.ofIdentExpr(2, "a")); + CelList list = ast.getExpr().list(); + assertThat(list.optionalIndices()).containsExactly(0); + assertThat(list.elements()).containsExactly(CelExpr.ofIdent(2, "a")); } @Test @@ -493,12 +524,14 @@ public void compile_overlappingVarsFailure() { .addDeclarations( Decl.newBuilder() .setName("variable") - .setIdent(IdentDecl.newBuilder().setType(CelTypes.STRING)) + .setIdent(IdentDecl.newBuilder().setType(CelProtoTypes.STRING)) .build()) .addDeclarations( Decl.newBuilder() .setName("variable") - .setIdent(IdentDecl.newBuilder().setType(CelTypes.createList(CelTypes.STRING))) + .setIdent( + IdentDecl.newBuilder() + .setType(CelProtoTypes.createList(CelProtoTypes.STRING))) .build()) .setResultType(SimpleType.BOOL) .build(); @@ -522,7 +555,7 @@ public void program_withVars() throws Exception { .addDeclarations( Decl.newBuilder() .setName("variable") - .setIdent(IdentDecl.newBuilder().setType(CelTypes.STRING)) + .setIdent(IdentDecl.newBuilder().setType(CelProtoTypes.STRING)) .build()) .setResultType(SimpleType.BOOL) .build(); @@ -538,7 +571,7 @@ public void program_withCelValue() throws Exception { .addDeclarations( Decl.newBuilder() .setName("variable") - .setIdent(IdentDecl.newBuilder().setType(CelTypes.STRING)) + .setIdent(IdentDecl.newBuilder().setType(CelProtoTypes.STRING)) .build()) .setResultType(SimpleType.BOOL) .build(); @@ -574,6 +607,53 @@ public void program_withProtoVars() throws Exception { .isEqualTo(true); } + @Test + public void program_withAllFieldsHidden_emptyMessageConstructionSuccess() throws Exception { + Cel cel = + standardCelBuilderWithMacros() + .addMessageTypes(AttributeContext.getDescriptor()) + .setContainer(CelContainer.ofName("google.rpc.context.AttributeContext")) + .addProtoTypeMasks( + ProtoTypeMask.ofAllFieldsHidden("google.rpc.context.AttributeContext")) + .build(); + + assertThat(cel.createProgram(cel.compile("AttributeContext{}").getAst()).eval()) + .isEqualTo(AttributeContext.getDefaultInstance()); + } + + @Test + public void compile_withAllFieldsHidden_selectHiddenField_throws() throws Exception { + Cel cel = + standardCelBuilderWithMacros() + .addMessageTypes(AttributeContext.getDescriptor()) + .setContainer(CelContainer.ofName("google.rpc.context.AttributeContext")) + .addProtoTypeMasks( + ProtoTypeMask.ofAllFieldsHidden("google.rpc.context.AttributeContext")) + .build(); + + CelValidationException e = + assertThrows( + CelValidationException.class, + () -> cel.compile("AttributeContext{ request: AttributeContext.Request{} }").getAst()); + assertThat(e).hasMessageThat().contains("undefined field 'request'"); + } + + @Test + public void compile_withAllFieldsHidden_selectHiddenFieldOnVar_throws() throws Exception { + Cel cel = + standardCelBuilderWithMacros() + .addMessageTypes(AttributeContext.getDescriptor()) + .setContainer(CelContainer.ofName("google.rpc.context.AttributeContext")) + .addProtoTypeMasks( + ProtoTypeMask.ofAllFieldsHidden("google.rpc.context.AttributeContext")) + .addVar("attr_ctx", StructTypeReference.create("google.rpc.context.AttributeContext")) + .build(); + + CelValidationException e = + assertThrows(CelValidationException.class, () -> cel.compile("attr_ctx.source").getAst()); + assertThat(e).hasMessageThat().contains("undefined field 'source'"); + } + @Test public void program_withNestedRestrictedProtoVars() throws Exception { Cel cel = @@ -601,11 +681,11 @@ public void program_withFunctions() throws Exception { ImmutableList.of( Decl.newBuilder() .setName("one") - .setIdent(IdentDecl.newBuilder().setType(CelTypes.BOOL)) + .setIdent(IdentDecl.newBuilder().setType(CelProtoTypes.BOOL)) .build(), Decl.newBuilder() .setName("two") - .setIdent(IdentDecl.newBuilder().setType(CelTypes.BOOL)) + .setIdent(IdentDecl.newBuilder().setType(CelProtoTypes.BOOL)) .build(), Decl.newBuilder() .setName("any") @@ -614,21 +694,21 @@ public void program_withFunctions() throws Exception { .addOverloads( Overload.newBuilder() .setOverloadId("any_bool") - .addParams(CelTypes.BOOL) - .setResultType(CelTypes.BOOL)) + .addParams(CelProtoTypes.BOOL) + .setResultType(CelProtoTypes.BOOL)) .addOverloads( Overload.newBuilder() .setOverloadId("any_bool_bool") - .addParams(CelTypes.BOOL) - .addParams(CelTypes.BOOL) - .setResultType(CelTypes.BOOL)) + .addParams(CelProtoTypes.BOOL) + .addParams(CelProtoTypes.BOOL) + .setResultType(CelProtoTypes.BOOL)) .addOverloads( Overload.newBuilder() .setOverloadId("any_bool_bool_bool") - .addParams(CelTypes.BOOL) - .addParams(CelTypes.BOOL) - .addParams(CelTypes.BOOL) - .setResultType(CelTypes.BOOL))) + .addParams(CelProtoTypes.BOOL) + .addParams(CelProtoTypes.BOOL) + .addParams(CelProtoTypes.BOOL) + .setResultType(CelProtoTypes.BOOL))) .build())) .addFunctionBindings(CelFunctionBinding.from("any_bool", Boolean.class, (arg) -> arg)) .addFunctionBindings( @@ -662,13 +742,13 @@ public void program_withThrowingFunction() throws Exception { .addOverloads( Overload.newBuilder() .setOverloadId("throws") - .setResultType(CelTypes.BOOL))) + .setResultType(CelProtoTypes.BOOL))) .build()) .addFunctionBindings( CelFunctionBinding.from( "throws", ImmutableList.of(), - (args) -> { + (unused) -> { throw new CelEvaluationException("this method always throws"); })) .setResultType(SimpleType.BOOL) @@ -690,15 +770,16 @@ public void program_withThrowingFunctionShortcircuited() throws Exception { .addOverloads( Overload.newBuilder() .setOverloadId("throws") - .setResultType(CelTypes.BOOL))) + .setResultType(CelProtoTypes.BOOL))) .build()) .addFunctionBindings( CelFunctionBinding.from( "throws", ImmutableList.of(), - (args) -> { - throw new CelEvaluationException( - "this method always throws", new RuntimeException("reason")); + (unused) -> { + throw CelEvaluationExceptionBuilder.newBuilder("this method always throws") + .setCause(new RuntimeException("reason")) + .build(); })) .setResultType(SimpleType.BOOL) .build(); @@ -728,7 +809,7 @@ public void program_simpleStructTypeReference() throws Exception { public void program_messageConstruction() throws Exception { Cel cel = standardCelBuilderWithMacros() - .setContainer("google.type") + .setContainer(CelContainer.ofName("google.type")) .addMessageTypes(com.google.type.Expr.getDescriptor()) .setResultType(StructTypeReference.create("google.type.Expr")) .setStandardEnvironmentEnabled(false) @@ -745,25 +826,27 @@ public void program_duplicateTypeDescriptor() throws Exception { standardCelBuilderWithMacros() .addMessageTypes(Timestamp.getDescriptor()) .addMessageTypes(ImmutableList.of(Timestamp.getDescriptor())) - .setContainer("google") + .setContainer(CelContainer.ofName("google")) .setResultType(SimpleType.TIMESTAMP) .build(); CelRuntime.Program program = cel.createProgram(cel.compile("protobuf.Timestamp{seconds: 12}").getAst()); - assertThat(program.eval()).isEqualTo(Timestamps.fromSeconds(12)); + + assertThat(program.eval()).isEqualTo(Instant.ofEpochSecond(12)); } @Test public void program_hermeticDescriptors_wellKnownProtobuf() throws Exception { Cel cel = standardCelBuilderWithMacros() + // CEL-Internal-2 .addMessageTypes(Timestamp.getDescriptor()) - .setContainer("google") + .setContainer(CelContainer.ofName("google")) .setResultType(SimpleType.TIMESTAMP) .build(); CelRuntime.Program program = cel.createProgram(cel.compile("protobuf.Timestamp{seconds: 12}").getAst()); - assertThat(program.eval()).isEqualTo(Timestamps.fromSeconds(12)); + assertThat(program.eval()).isEqualTo(Instant.ofEpochSecond(12)); } @Test @@ -780,7 +863,7 @@ public void program_partialMessageTypes() throws Exception { // defined in checked.proto. Because the `Expr` type is referenced within a message // field of the CheckedExpr, it is available for use. .setOptions(CelOptions.current().resolveTypeDependencies(false).build()) - .setContainer(packageName) + .setContainer(CelContainer.ofName(packageName)) .setResultType(StructTypeReference.create(packageName + ".Expr")) .build(); CelRuntime.Program program = cel.createProgram(cel.compile("Expr{}").getAst()); @@ -797,7 +880,7 @@ public void program_partialMessageTypeFailure() { // defined in checked.proto. Because the `ParsedExpr` type is not referenced, it is not // available for use within CEL when deep type resolution is disabled. .setOptions(CelOptions.current().resolveTypeDependencies(false).build()) - .setContainer(packageName) + .setContainer(CelContainer.ofName(packageName)) .setResultType(StructTypeReference.create(packageName + ".ParsedExpr")) .build(); CelValidationException e = @@ -816,7 +899,7 @@ public void program_deepTypeResolution() throws Exception { // defined in checked.proto. Because deep type dependency resolution is enabled, the // `ParsedExpr` may be used within CEL. .setOptions(CelOptions.current().resolveTypeDependencies(true).build()) - .setContainer(packageName) + .setContainer(CelContainer.ofName(packageName)) .setResultType(StructTypeReference.create(packageName + ".ParsedExpr")) .build(); CelRuntime.Program program = cel.createProgram(cel.compile("ParsedExpr{}").getAst()); @@ -830,7 +913,7 @@ public void program_deepTypeResolutionEnabledForRuntime_success() throws Excepti CelCompilerFactory.standardCelCompilerBuilder() .addFileTypes(ParsedExpr.getDescriptor().getFile()) .setResultType(StructTypeReference.create(packageName + ".ParsedExpr")) - .setContainer(packageName) + .setContainer(CelContainer.ofName(packageName)) .build(); CelAbstractSyntaxTree ast = celCompiler.compile("ParsedExpr{}").getAst(); @@ -839,6 +922,7 @@ public void program_deepTypeResolutionEnabledForRuntime_success() throws Excepti CelRuntimeFactory.standardCelRuntimeBuilder() .addFileTypes(CheckedExpr.getDescriptor().getFile()) .setOptions(CelOptions.current().resolveTypeDependencies(true).build()) + // CEL-Internal-2 .build(); CelRuntime.Program program = celRuntime.createProgram(ast); @@ -856,7 +940,7 @@ public void program_deepTypeResolutionDisabledForRuntime_fails() throws Exceptio .addFileTypes(CheckedExpr.getDescriptor().getFile()) .setOptions(CelOptions.current().resolveTypeDependencies(true).build()) .setResultType(StructTypeReference.create(packageName + ".ParsedExpr")) - .setContainer(packageName) + .setContainer(CelContainer.ofName(packageName)) .build(); // 'ParsedExpr' is defined in syntax.proto but the descriptor provided is from 'checked.proto'. @@ -867,6 +951,7 @@ public void program_deepTypeResolutionDisabledForRuntime_fails() throws Exceptio CelRuntimeFactory.standardCelRuntimeBuilder() .addFileTypes(CheckedExpr.getDescriptor().getFile()) .setOptions(CelOptions.current().resolveTypeDependencies(false).build()) + // CEL-Internal-2 .build(); CelRuntime.Program program = celRuntime.createProgram(ast); @@ -887,12 +972,12 @@ public void program_typeProvider() throws Exception { standardCelBuilderWithMacros() .setTypeProvider( new DescriptorTypeProvider(ImmutableList.of(Timestamp.getDescriptor()))) - .setContainer("google") + .setContainer(CelContainer.ofName("google")) .setResultType(SimpleType.TIMESTAMP) .build(); CelRuntime.Program program = cel.createProgram(cel.compile("protobuf.Timestamp{seconds: 12}").getAst()); - assertThat(program.eval()).isEqualTo(Timestamps.fromSeconds(12)); + assertThat(program.eval()).isEqualTo(Instant.ofEpochSecond(12)); } @Test @@ -906,7 +991,7 @@ public void program_protoActivation() throws Exception { .setIdent( IdentDecl.newBuilder() .setType( - CelTypes.createMessage( + CelProtoTypes.createMessage( "google.rpc.context.AttributeContext.Resource"))) .build()) .setResultType(SimpleType.STRING) @@ -929,7 +1014,8 @@ public void program_enumTypeDirectResolution(boolean resolveTypeDependencies) th .addFileTypes(StandaloneGlobalEnum.getDescriptor().getFile()) .setOptions( CelOptions.current().resolveTypeDependencies(resolveTypeDependencies).build()) - .setContainer("dev.cel.testing.testdata.proto3.StandaloneGlobalEnum") + .setContainer( + CelContainer.ofName("dev.cel.testing.testdata.proto3.StandaloneGlobalEnum")) .setResultType(SimpleType.BOOL) .build(); @@ -953,7 +1039,7 @@ public void program_enumTypeReferenceResolution(boolean resolveTypeDependencies) CelOptions.current().resolveTypeDependencies(resolveTypeDependencies).build()) .addMessageTypes(Struct.getDescriptor()) .setResultType(StructTypeReference.create("google.protobuf.NullValue")) - .setContainer("google.protobuf") + .setContainer(CelContainer.ofName("google.protobuf")) .build(); // `Value` is defined in `Struct` proto and NullValue is an enum within this `Value` struct. @@ -969,16 +1055,15 @@ public void program_enumTypeTransitiveResolution() throws Exception { Cel cel = standardCelBuilderWithMacros() .setOptions(CelOptions.current().resolveTypeDependencies(true).build()) - .addMessageTypes(TestAllTypes.getDescriptor()) + .addMessageTypes(Proto2ExtensionScopedMessage.getDescriptor()) .setResultType(StructTypeReference.create("google.protobuf.NullValue")) - .setContainer("google.protobuf") + .setContainer(CelContainer.ofName("google.protobuf")) .build(); // 'Value' is a struct defined as a dependency of messages_proto2.proto and 'NullValue' is an // enum within this 'Value' struct. // As deep type dependency is enabled, the following evaluation should work by as the // 'NullValue' enum type is transitively discovered - CelRuntime.Program program = cel.createProgram(cel.compile("Value{null_value: NullValue.NULL_VALUE}").getAst()); assertThat(program.eval()).isEqualTo(NullValue.NULL_VALUE); @@ -1003,9 +1088,9 @@ public void compile_enumTypeTransitiveResolutionFailure() { Cel cel = standardCelBuilderWithMacros() .setOptions(CelOptions.current().resolveTypeDependencies(false).build()) - .addMessageTypes(TestAllTypes.getDescriptor()) + .addMessageTypes(Proto2ExtensionScopedMessage.getDescriptor()) .setResultType(StructTypeReference.create("google.protobuf.NullValue")) - .setContainer("google.protobuf") + .setContainer(CelContainer.ofName("google.protobuf")) .build(); // 'Value' is a struct defined as a dependency of messages_proto2.proto and 'NullValue' is an @@ -1019,6 +1104,34 @@ public void compile_enumTypeTransitiveResolutionFailure() { assertThat(e).hasMessageThat().contains("undeclared reference to 'NullValue'"); } + @Test + public void compile_multipleInstancesOfEnumDescriptor_dedupedByFullName() throws Exception { + String enumTextProto = + "name: \"standalone_global_enum.proto\"\n" + + "package: \"dev.cel.testing.testdata.proto3\"\n" + + "enum_type {\n" + + " name: \"StandaloneGlobalEnum\"\n" + + " value {\n" + + " name: \"SGOO\"\n" + + " number: 0\n" + + " }\n" + + "}\n" + + "syntax: \"proto3\"\n"; + FileDescriptorProto enumFileDescriptorProto = + TextFormat.parse(enumTextProto, FileDescriptorProto.class); + FileDescriptor enumFileDescriptor = + FileDescriptor.buildFrom(enumFileDescriptorProto, new FileDescriptor[] {}); + Cel cel = + standardCelBuilderWithMacros() + .setContainer(CelContainer.ofName("dev.cel.testing.testdata")) + .addFileTypes(enumFileDescriptor) + .addFileTypes(StandaloneGlobalEnum.getDescriptor().getFile()) + .build(); + + assertThat(cel.compile("dev.cel.testing.testdata.proto3.StandaloneGlobalEnum.SGOO").getAst()) + .isNotNull(); + } + @Test public void program_customVarResolver() throws Exception { Cel cel = @@ -1026,7 +1139,7 @@ public void program_customVarResolver() throws Exception { .addDeclarations( Decl.newBuilder() .setName("variable") - .setIdent(IdentDecl.newBuilder().setType(CelTypes.STRING)) + .setIdent(IdentDecl.newBuilder().setType(CelProtoTypes.STRING)) .build()) .setResultType(SimpleType.BOOL) .build(); @@ -1035,7 +1148,7 @@ public void program_customVarResolver() throws Exception { program.eval( (name) -> name.equals("variable") ? Optional.of("hello") : Optional.empty())) .isEqualTo(true); - assertThat(program.eval((name) -> Optional.of(""))).isEqualTo(false); + assertThat(program.eval((unused) -> Optional.of(""))).isEqualTo(false); } @Test @@ -1069,7 +1182,7 @@ public void program_messageTypeAddedAsVarWithoutDescriptor_throwsHumanReadableEr String packageName = CheckedExpr.getDescriptor().getFile().getPackage(); Cel cel = standardCelBuilderWithMacros() - .addVar("parsedExprVar", CelTypes.createMessage(ParsedExpr.getDescriptor())) + .addVar("parsedExprVar", CelProtoMessageTypes.createMessage(ParsedExpr.getDescriptor())) .build(); CelValidationException exception = assertThrows( @@ -1248,7 +1361,7 @@ public void programAdvanceEvaluation_unknownsNamespaceSupport() throws Exception .setOptions(CelOptions.current().enableUnknownTracking(true).build()) .addVar("com.google.a", SimpleType.BOOL) .addVar("com.google.b", SimpleType.BOOL) - .setContainer("com.google") + .setContainer(CelContainer.ofName("com.google")) .addFunctionBindings() .setResultType(SimpleType.BOOL) .build(); @@ -1277,7 +1390,7 @@ public void programAdvanceEvaluation_unknownsIterativeEvalExample() throws Excep .setOptions(CelOptions.current().enableUnknownTracking(true).build()) .addVar("com.google.a", SimpleType.BOOL) .addVar("com.google.b", SimpleType.BOOL) - .setContainer("com.google") + .setContainer(CelContainer.ofName("com.google")) .addFunctionBindings() .setResultType(SimpleType.BOOL) .build(); @@ -1356,7 +1469,7 @@ public void programAdvanceEvaluation_argumentMergeErrorPriority() throws Excepti .setResultType( Type.newBuilder().setPrimitive(PrimitiveType.BOOL)))) .build()) - .setContainer("") + .setContainer(CelContainer.ofName("")) .addFunctionBindings() .setResultType(SimpleType.BOOL) .build(); @@ -1399,7 +1512,7 @@ public void programAdvanceEvaluation_argumentMergeUnknowns() throws Exception { .setResultType( Type.newBuilder().setPrimitive(PrimitiveType.BOOL)))) .build()) - .setContainer("") + .setContainer(CelContainer.ofName("")) .addFunctionBindings() .setResultType(SimpleType.BOOL) .build(); @@ -1426,7 +1539,7 @@ public void programAdvanceEvaluation_mapSelectUnknowns() throws Exception { standardCelBuilderWithMacros() .setOptions(CelOptions.current().enableUnknownTracking(true).build()) .addVar("unk", MapType.create(SimpleType.STRING, SimpleType.BOOL)) - .setContainer("") + .setContainer(CelContainer.ofName("")) .addFunctionBindings() .setResultType(SimpleType.BOOL) .build(); @@ -1452,7 +1565,7 @@ public void programAdvanceEvaluation_mapIndexUnknowns() throws Exception { standardCelBuilderWithMacros() .setOptions(CelOptions.current().enableUnknownTracking(true).build()) .addVar("unk", MapType.create(SimpleType.STRING, SimpleType.BOOL)) - .setContainer("") + .setContainer(CelContainer.ofName("")) .addFunctionBindings() .setResultType(SimpleType.BOOL) .build(); @@ -1481,7 +1594,7 @@ public void programAdvanceEvaluation_listIndexUnknowns() throws Exception { standardCelBuilderWithMacros() .setOptions(CelOptions.current().enableUnknownTracking(true).build()) .addVar("unk", ListType.create(SimpleType.BOOL)) - .setContainer("") + .setContainer(CelContainer.ofName("")) .addFunctionBindings() .setResultType(SimpleType.BOOL) .build(); @@ -1510,7 +1623,7 @@ public void programAdvanceEvaluation_indexOnUnknownContainer() throws Exception standardCelBuilderWithMacros() .setOptions(CelOptions.current().enableUnknownTracking(true).build()) .addVar("unk", ListType.create(SimpleType.BOOL)) - .setContainer("") + .setContainer(CelContainer.ofName("")) .addFunctionBindings() .setResultType(SimpleType.BOOL) .build(); @@ -1532,7 +1645,7 @@ public void programAdvanceEvaluation_unsupportedIndexIgnored() throws Exception standardCelBuilderWithMacros() .setOptions(CelOptions.current().enableUnknownTracking(true).build()) .addVar("unk", MapType.create(SimpleType.STRING, SimpleType.BOOL)) - .setContainer("") + .setContainer(CelContainer.ofName("")) .addFunctionBindings() .setResultType(SimpleType.BOOL) .build(); @@ -1558,7 +1671,7 @@ public void programAdvanceEvaluation_unsupportedIndexIgnored() throws Exception UnknownContext.create( fromMap( ImmutableMap.of( - "unk", ImmutableMap.of(ByteString.copyFromUtf8("a"), false))), + "unk", ImmutableMap.of(CelByteString.copyFromUtf8("a"), false))), ImmutableList.of()))) .isEqualTo(false); } @@ -1569,7 +1682,7 @@ public void programAdvanceEvaluation_listIndexMacroTracking() throws Exception { standardCelBuilderWithMacros() .setOptions(CelOptions.current().enableUnknownTracking(true).build()) .addVar("testList", ListType.create(SimpleType.BOOL)) - .setContainer("") + .setContainer(CelContainer.ofName("")) .addFunctionBindings() .setResultType(SimpleType.BOOL) .build(); @@ -1602,7 +1715,7 @@ public void programAdvanceEvaluation_mapIndexMacroTracking() throws Exception { standardCelBuilderWithMacros() .setOptions(CelOptions.current().enableUnknownTracking(true).build()) .addVar("testMap", MapType.create(SimpleType.STRING, SimpleType.BOOL)) - .setContainer("") + .setContainer(CelContainer.ofName("")) .addFunctionBindings() .setResultType(SimpleType.BOOL) .build(); @@ -1652,7 +1765,7 @@ public void programAdvanceEvaluation_boolOperatorMergeUnknownPriority() throws E .addVarDeclarations( CelVarDecl.newVarDeclaration("unk", SimpleType.BOOL), CelVarDecl.newVarDeclaration("err", SimpleType.BOOL)) - .setContainer("com.google") + .setContainer(CelContainer.ofName("com.google")) .addFunctionBindings() .setResultType(SimpleType.BOOL) .build(); @@ -1664,7 +1777,8 @@ public void programAdvanceEvaluation_boolOperatorMergeUnknownPriority() throws E UnknownContext.create( fromMap(ImmutableMap.of()), ImmutableList.of(CelAttributePattern.fromQualifiedIdentifier("unk"))))) - .isEqualTo(CelUnknownSet.create(CelAttribute.fromQualifiedIdentifier("unk"))); + .isEqualTo( + CelUnknownSet.create(CelAttribute.fromQualifiedIdentifier("unk"), ImmutableSet.of(3L))); } @Test @@ -1676,7 +1790,7 @@ public void programAdvanceEvaluation_partialUnknownMapEntryPropagates() throws E ImmutableList.of( CelVarDecl.newVarDeclaration("partialList1", ListType.create(SimpleType.INT)), CelVarDecl.newVarDeclaration("partialList2", ListType.create(SimpleType.INT)))) - .setContainer("com.google") + .setContainer(CelContainer.ofName("com.google")) .addFunctionBindings() .build(); CelRuntime.Program program = @@ -1707,7 +1821,7 @@ public void programAdvanceEvaluation_partialUnknownListElementPropagates() throw .setOptions(CelOptions.current().enableUnknownTracking(true).build()) .addVar("partialList1", ListType.create(SimpleType.INT)) .addVar("partialList2", ListType.create(SimpleType.INT)) - .setContainer("com.google") + .setContainer(CelContainer.ofName("com.google")) .addFunctionBindings() .build(); CelRuntime.Program program = @@ -1737,13 +1851,13 @@ public void programAdvanceEvaluation_partialUnknownMessageFieldPropagates() thro .addMessageTypes(TestAllTypes.getDescriptor()) .addVar( "partialMessage1", - StructTypeReference.create("dev.cel.testing.testdata.proto3.TestAllTypes")) + StructTypeReference.create("cel.expr.conformance.proto3.TestAllTypes")) .addVar( "partialMessage2", - StructTypeReference.create("dev.cel.testing.testdata.proto3.TestAllTypes")) + StructTypeReference.create("cel.expr.conformance.proto3.TestAllTypes")) .setResultType( - StructTypeReference.create("dev.cel.testing.testdata.proto3.NestedTestAllTypes")) - .setContainer("dev.cel.testing.testdata.proto3") + StructTypeReference.create("cel.expr.conformance.proto3.NestedTestAllTypes")) + .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) .addFunctionBindings() .build(); Program program = @@ -1800,9 +1914,9 @@ public boolean isAssignableFrom(CelType other) { CelFactory.standardCelBuilder() .addVar("x", SimpleType.INT) .addFunctionDeclarations( - CelFunctionDecl.newFunctionDeclaration( + newFunctionDeclaration( "print", - CelOverloadDecl.newGlobalOverload( + newGlobalOverload( "print_overload", SimpleType.STRING, customType))) // The overload would accept either Int or CustomType @@ -1816,6 +1930,199 @@ public boolean isAssignableFrom(CelType other) { assertThat(result).isEqualTo("5"); } + @Test + @SuppressWarnings("unchecked") // test only + public void program_functionParamWithWellKnownType() throws Exception { + Cel cel = + CelFactory.standardCelBuilder() + .addFunctionDeclarations( + newFunctionDeclaration( + "hasStringValue", + newMemberOverload( + "struct_hasStringValue_string_string", + SimpleType.BOOL, + StructTypeReference.create("google.protobuf.Struct"), + SimpleType.STRING, + SimpleType.STRING))) + .addFunctionBindings( + CelFunctionBinding.from( + "struct_hasStringValue_string_string", + ImmutableList.of(Map.class, String.class, String.class), + args -> { + Map map = (Map) args[0]; + return map.containsKey(args[1]) && map.containsValue(args[2]); + })) + .build(); + CelAbstractSyntaxTree ast = cel.compile("{'a': 'b'}.hasStringValue('a', 'b')").getAst(); + + boolean result = (boolean) cel.createProgram(ast).eval(); + + assertThat(result).isTrue(); + } + + @Test + public void program_nativeTypeUnknownsEnabled_asIdentifiers() throws Exception { + Cel cel = + CelFactory.standardCelBuilder() + .addVar("x", SimpleType.BOOL) + .addVar("y", SimpleType.BOOL) + .setOptions(CelOptions.current().build()) + .build(); + CelAbstractSyntaxTree ast = cel.compile("x || y").getAst(); + + CelUnknownSet result = (CelUnknownSet) cel.createProgram(ast).eval(); + + assertThat(result.unknownExprIds()).containsExactly(1L, 3L); + assertThat(result.attributes()).isEmpty(); + } + + @Test + public void program_nativeTypeUnknownsEnabled_asCallArguments() throws Exception { + Cel cel = + CelFactory.standardCelBuilder() + .addVar("x", SimpleType.BOOL) + .addFunctionDeclarations( + newFunctionDeclaration( + "foo", newGlobalOverload("foo_bool", SimpleType.BOOL, SimpleType.BOOL))) + .setOptions(CelOptions.current().build()) + .build(); + CelAbstractSyntaxTree ast = cel.compile("foo(x)").getAst(); + + CelUnknownSet result = (CelUnknownSet) cel.createProgram(ast).eval(); + + assertThat(result.unknownExprIds()).containsExactly(2L); + assertThat(result.attributes()).isEmpty(); + } + + @Test + public void program_comprehensionDisabled_throws() throws Exception { + Cel cel = + standardCelBuilderWithMacros() + .setOptions(CelOptions.current().enableComprehension(false).build()) + .build(); + CelAbstractSyntaxTree ast = cel.compile("['foo', 'bar'].map(x, x)").getAst(); + + CelEvaluationException e = + assertThrows(CelEvaluationException.class, () -> cel.createProgram(ast).eval()); + assertThat(e).hasMessageThat().contains("Iteration budget exceeded: 0"); + assertThat(e.getErrorCode()).isEqualTo(CelErrorCode.ITERATION_BUDGET_EXCEEDED); + } + + @Test + public void program_regexProgramSizeUnderLimit_success() throws Exception { + Cel cel = + standardCelBuilderWithMacros() + .setOptions(CelOptions.current().maxRegexProgramSize(7).build()) + .build(); + // See + // https://github.com/google/re2j/blob/84237cbbd0fbd637c6eb6856717c1e248daae729/javatests/com/google/re2j/PatternTest.java#L175 for program size + CelAbstractSyntaxTree ast = cel.compile("'foo'.matches('(a+b)')").getAst(); + + assertThat(cel.createProgram(ast).eval()).isEqualTo(false); + } + + @Test + public void program_regexProgramSizeExceedsLimit_throws() throws Exception { + Cel cel = + standardCelBuilderWithMacros() + .setOptions(CelOptions.current().maxRegexProgramSize(6).build()) + .build(); + // See + // https://github.com/google/re2j/blob/84237cbbd0fbd637c6eb6856717c1e248daae729/javatests/com/google/re2j/PatternTest.java#L175 for program size + CelAbstractSyntaxTree ast = cel.compile("'foo'.matches('(a+b)')").getAst(); + + CelEvaluationException e = + assertThrows(CelEvaluationException.class, () -> cel.createProgram(ast).eval()); + assertThat(e) + .hasMessageThat() + .contains( + "evaluation error at :13: Regex pattern exceeds allowed program size. Allowed:" + + " 6, Provided: 7"); + assertThat(e.getErrorCode()).isEqualTo(CelErrorCode.INVALID_ARGUMENT); + } + + @Test + @SuppressWarnings("unchecked") // test only + public void program_evaluateCanonicalTypesToNativeTypesDisabled_producesProtoValues() + throws Exception { + Cel cel = + standardCelBuilderWithMacros() + .setOptions(CelOptions.current().evaluateCanonicalTypesToNativeValues(false).build()) + .build(); + CelAbstractSyntaxTree ast = cel.compile("[null, {b'abc': null}]").getAst(); + Map expectedNestedMap = new LinkedHashMap<>(); + expectedNestedMap.put(ByteString.copyFromUtf8("abc"), com.google.protobuf.NullValue.NULL_VALUE); + + List result = (List) cel.createProgram(ast).eval(); + + assertThat(result).containsExactly(com.google.protobuf.NullValue.NULL_VALUE, expectedNestedMap); + } + + @Test + public void program_evaluateCanonicalTypesToNativeTypesDisabled_producesBytesProto() + throws Exception { + Cel cel = + standardCelBuilderWithMacros() + .addMessageTypes(TestAllTypes.getDescriptor()) + .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) + .setOptions(CelOptions.current().evaluateCanonicalTypesToNativeValues(false).build()) + .build(); + CelAbstractSyntaxTree ast = + cel.compile("TestAllTypes{single_bytes: bytes('abc')}.single_bytes").getAst(); + + ByteString result = (ByteString) cel.createProgram(ast).eval(); + + assertThat(result).isEqualTo(ByteString.copyFromUtf8("abc")); + } + + @Test + public void program_fdsContainsWktDependency_descriptorInstancesMatch() throws Exception { + // Force serialization of the descriptor to get a unique instance + FileDescriptorProto proto = TestAllTypes.getDescriptor().getFile().toProto(); + FileDescriptorSet fds = FileDescriptorSet.newBuilder().addFile(proto).build(); + ImmutableSet fileDescriptors = + CelDescriptorUtil.getFileDescriptorsFromFileDescriptorSet(fds); + ImmutableSet descriptors = + CelDescriptorUtil.getAllDescriptorsFromFileDescriptor(fileDescriptors) + .messageTypeDescriptors(); + Descriptor testAllTypesDescriptor = + descriptors.stream() + .filter(x -> x.getFullName().equals(TestAllTypes.getDescriptor().getFullName())) + .findAny() + .get(); + + // Parse text proto using this fds + TypeRegistry typeRegistry = TypeRegistry.newBuilder().add(descriptors).build(); + TestAllTypes.Builder testAllTypesBuilder = TestAllTypes.newBuilder(); + TextFormat.Parser textFormatParser = + TextFormat.Parser.newBuilder().setTypeRegistry(typeRegistry).build(); + String textProto = + "single_timestamp {\n" // + + " seconds: 100\n" // + + "}"; + textFormatParser.merge(textProto, testAllTypesBuilder); + TestAllTypes testAllTypesFromTextProto = testAllTypesBuilder.build(); + DynamicMessage dynamicMessage = + DynamicMessage.parseFrom( + testAllTypesDescriptor, + testAllTypesFromTextProto.toByteArray(), + ExtensionRegistry.getEmptyRegistry()); + // Setup CEL environment with the same descriptors obtained from FDS + Cel cel = + standardCelBuilderWithMacros() + .addMessageTypes(descriptors) + // CEL-Internal-2 + .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) + .build(); + CelAbstractSyntaxTree ast = + cel.compile("TestAllTypes{single_timestamp: timestamp(100)}").getAst(); + + DynamicMessage evalResult = (DynamicMessage) cel.createProgram(ast).eval(); + + // This should strictly equal regardless of where the descriptors came from for WKTs + assertThat(evalResult).isEqualTo(dynamicMessage); + } + @Test public void toBuilder_isImmutable() { CelBuilder celBuilder = CelFactory.standardCelBuilder(); @@ -1837,13 +2144,164 @@ public void toBuilder_isImmutable() { assertThat(newRuntimeBuilder).isNotEqualTo(celImpl.toRuntimeBuilder()); } + @Test + 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())); + + assertThat(result).isTrue(); + } + + @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 + public void eval_withJsonFieldName_runtimeOptionDisabled_throws() throws Exception { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addVar("file", StructTypeReference.create(SingleFile.getDescriptor().getFullName())) + .addMessageTypes(SingleFile.getDescriptor()) + .setOptions(CelOptions.current().enableJsonFieldNames(true).build()) + .build(); + CelRuntime celRuntime = + CelRuntimeFactory.standardCelRuntimeBuilder() + .addMessageTypes(SingleFile.getDescriptor()) + .setOptions(CelOptions.current().enableJsonFieldNames(false).build()) + .build(); + CelAbstractSyntaxTree ast = celCompiler.compile("file.int64CamelCaseJsonName").getAst(); + + CelEvaluationException e = + assertThrows( + CelEvaluationException.class, + () -> + celRuntime + .createProgram(ast) + .eval(ImmutableMap.of("file", SingleFile.getDefaultInstance()))); + assertThat(e) + .hasMessageThat() + .contains( + "field 'int64CamelCaseJsonName' is not declared in message" + + " 'dev.cel.testing.testdata.SingleFile"); + } + + @Test + public void compile_withJsonFieldName_astTagged() 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.int64CamelCaseJsonName").getAst(); + + assertThat(ast.getSource().getExtensions()) + .contains( + Extension.create( + "json_name", Extension.Version.of(1L, 1L), Extension.Component.COMPONENT_RUNTIME)); + } + + @Test + public void compile_withJsonFieldName_protoFieldNameComparison_throws() throws Exception { + Cel cel = + standardCelBuilderWithMacros() + .addVar("file", StructTypeReference.create(SingleFile.getDescriptor().getFullName())) + .addMessageTypes(SingleFile.getDescriptor()) + .setOptions(CelOptions.current().enableJsonFieldNames(true).build()) + .build(); + + CelValidationException e = + assertThrows( + CelValidationException.class, + () -> cel.compile("file.camelCased == file.snake_cased").getAst()); + assertThat(e).hasMessageThat().contains("undefined field 'snake_cased'"); + } + private static TypeProvider aliasingProvider(ImmutableMap typeAliases) { return new TypeProvider() { @Override public @Nullable Type lookupType(String typeName) { Type alias = typeAliases.get(typeName); if (alias != null) { - return CelTypes.create(alias); + return CelProtoTypes.create(alias); } return null; } @@ -1856,10 +2314,28 @@ private static TypeProvider aliasingProvider(ImmutableMap typeAlia @Override public TypeProvider.@Nullable FieldType lookupFieldType(Type type, String fieldName) { if (typeAliases.containsKey(type.getMessageType())) { - return TypeProvider.FieldType.of(CelTypes.STRING); + return TypeProvider.FieldType.of(CelProtoTypes.STRING); } return null; } }; } + + private static Cel setupEnv(CelBuilder celBuilder) { + ExtensionRegistry extensionRegistry = ExtensionRegistry.newInstance(); + SingleFileExtensionsProto.registerAllExtensions(extensionRegistry); + return celBuilder + .addVar("file", StructTypeReference.create(SingleFile.getDescriptor().getFullName())) + .addMessageTypes(SingleFile.getDescriptor()) + .addFileTypes(SingleFileExtensionsProto.getDescriptor()) + .addCompilerLibraries(CelExtensions.protos()) + .setExtensionRegistry(extensionRegistry) + .setOptions( + CelOptions.current() + .enableJsonFieldNames(true) + .enableHeterogeneousNumericComparisons(true) + .enableQuotedIdentifierSyntax(true) + .build()) + .build(); + } } diff --git a/cel_android_rules.bzl b/cel_android_rules.bzl new file mode 100644 index 000000000..9bd2fd8bc --- /dev/null +++ b/cel_android_rules.bzl @@ -0,0 +1,63 @@ +# 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. +"""Macro to create android_library rules with CEL specific options applied.""" + +load("@rules_android//rules:rules.bzl", "android_library", "android_local_test") + +DEFAULT_JAVACOPTS = [ + "-XDstringConcat=inline", # SDK forces usage of StringConcatFactory (java 9+) +] + +def cel_android_library(name, **kwargs): + """ + Generates android_library target with CEL specific javacopts applied + + Args: + name: name of the android_library target + **kwargs: rest of the args accepted by android_library + """ + + javacopts = kwargs.get("javacopts", []) + all_javacopts = DEFAULT_JAVACOPTS + javacopts + + # 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"]) + 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 + ) + +def cel_android_local_test(name, **kwargs): + """ + Generates android_local_test target with CEL specific javacopts applied + + Args: + name: name of the android_local_test target + **kwargs: rest of the args accepted by android_local_test + """ + + javacopts = kwargs.get("javacopts", []) + all_javacopts = DEFAULT_JAVACOPTS + javacopts + + android_local_test( + name = name, + javacopts = all_javacopts, + **kwargs + ) diff --git a/checker/BUILD.bazel b/checker/BUILD.bazel index 25fc8d739..5fa3c6c05 100644 --- a/checker/BUILD.bazel +++ b/checker/BUILD.bazel @@ -1,3 +1,5 @@ +load("@rules_java//java:defs.bzl", "java_library") + package( default_applicable_licenses = ["//:license"], default_visibility = ["//visibility:public"], @@ -26,7 +28,7 @@ java_library( java_library( name = "type_provider_legacy_impl", - visibility = ["//visibility:public"], + visibility = ["//:internal"], exports = ["//checker/src/main/java/dev/cel/checker:type_provider_legacy_impl"], ) @@ -37,13 +39,13 @@ java_library( java_library( name = "checker_legacy_environment", - visibility = ["//visibility:public"], + deprecation = "See go/cel-java-migration-guide. Please use CEL-Java Fluent APIs //compiler instead", exports = ["//checker/src/main/java/dev/cel/checker:checker_legacy_environment"], ) java_library( name = "type_inferencer", - visibility = ["//visibility:public"], # Planned for use in a new type-checker. + visibility = ["//:internal"], # Planned for use in a new type-checker. exports = ["//checker/src/main/java/dev/cel/checker:type_inferencer"], ) @@ -51,3 +53,8 @@ java_library( name = "proto_expr_visitor", exports = ["//checker/src/main/java/dev/cel/checker:proto_expr_visitor"], ) + +java_library( + name = "standard_decl", + exports = ["//checker/src/main/java/dev/cel/checker:standard_decl"], +) diff --git a/checker/src/main/java/dev/cel/checker/BUILD.bazel b/checker/src/main/java/dev/cel/checker/BUILD.bazel index 0053d9061..99d4da586 100644 --- a/checker/src/main/java/dev/cel/checker/BUILD.bazel +++ b/checker/src/main/java/dev/cel/checker/BUILD.bazel @@ -1,3 +1,5 @@ +load("@rules_java//java:defs.bzl", "java_library") + package( default_applicable_licenses = [ "//:license", @@ -28,7 +30,6 @@ CHECKER_LEGACY_ENV_SOURCES = [ "ExprChecker.java", "ExprVisitor.java", "InferenceContext.java", - "Standard.java", "TypeFormatter.java", "TypeProvider.java", "Types.java", @@ -48,9 +49,9 @@ java_library( "//common/annotations", "//common/internal:file_descriptor_converter", "//common/types", - "//common/types:cel_types", + "//common/types:cel_proto_types", "//common/types:type_providers", - "@cel_spec//proto/cel/expr:expr_java_proto", + "@cel_spec//proto/cel/expr:checked_java_proto", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", @@ -68,23 +69,29 @@ java_library( ":checker_builder", ":checker_legacy_environment", ":proto_type_mask", + ":standard_decl", ":type_provider_legacy_impl", "//:auto_value", - "//common", + "//common:cel_ast", + "//common:cel_descriptor_util", + "//common:cel_source", "//common:compiler_common", + "//common:container", "//common:options", + "//common:source_location", "//common/annotations", "//common/ast:expr_converter", "//common/internal:env_visitor", "//common/internal:errors", "//common/types", - "//common/types:cel_types", + "//common/types:cel_proto_types", "//common/types:message_type_provider", "//common/types:type_providers", - "@cel_spec//proto/cel/expr:expr_java_proto", + "@cel_spec//proto/cel/expr:checked_java_proto", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", + "@maven//:org_jspecify_jspecify", ], ) @@ -96,11 +103,13 @@ java_library( deps = [ ":checker_legacy_environment", ":proto_type_mask", - "//common", + ":standard_decl", + "//common:cel_ast", "//common:compiler_common", + "//common:container", "//common:options", "//common/types:type_providers", - "@cel_spec//proto/cel/expr:expr_java_proto", + "@cel_spec//proto/cel/expr:checked_java_proto", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_protobuf_protobuf_java", ], @@ -131,9 +140,9 @@ java_library( "//common/annotations", "//common/ast", "//common/ast:expr_converter", - "//common/types:cel_types", + "//common/types:cel_proto_types", "//common/types:type_providers", - "@cel_spec//proto/cel/expr:expr_java_proto", + "@cel_spec//proto/cel/expr:checked_java_proto", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", ], @@ -149,9 +158,9 @@ java_library( "//:auto_value", "//common/annotations", "//common/types", - "//common/types:cel_types", + "//common/types:cel_proto_types", "//common/types:type_providers", - "@cel_spec//proto/cel/expr:expr_java_proto", + "@cel_spec//proto/cel/expr:checked_java_proto", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:org_jspecify_jspecify", @@ -165,22 +174,29 @@ java_library( ], deps = [ ":cel_ident_decl", + ":standard_decl", "//:auto_value", + "//common:cel_ast", + "//common:cel_source", "//common:compiler_common", - "//common:features", + "//common:container", + "//common:mutable_ast", + "//common:operator", "//common:options", "//common:proto_ast", "//common/annotations", "//common/ast", "//common/ast:expr_converter", + "//common/ast:mutable_expr", "//common/internal:errors", "//common/internal:file_descriptor_converter", "//common/types", + "//common/types:cel_proto_types", "//common/types:cel_types", "//common/types:type_providers", "//parser:macro", - "//parser:operator", - "@cel_spec//proto/cel/expr:expr_java_proto", + "@cel_spec//proto/cel/expr:checked_java_proto", + "@cel_spec//proto/cel/expr:syntax_java_proto", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", @@ -211,6 +227,26 @@ java_library( ], deps = [ ":checker_legacy_environment", - "//common", + "//common:cel_ast", + "//common:proto_ast", + ], +) + +java_library( + name = "standard_decl", + srcs = [ + "CelStandardDeclarations.java", + ], + tags = [ + ], + deps = [ + ":cel_ident_decl", + "//common:compiler_common", + "//common:operator", + "//common/types", + "//common/types:cel_types", + "//common/types:type_providers", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", ], ) diff --git a/checker/src/main/java/dev/cel/checker/CelChecker.java b/checker/src/main/java/dev/cel/checker/CelChecker.java index 4022212cb..077850be1 100644 --- a/checker/src/main/java/dev/cel/checker/CelChecker.java +++ b/checker/src/main/java/dev/cel/checker/CelChecker.java @@ -17,6 +17,7 @@ import com.google.errorprone.annotations.Immutable; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelValidationResult; +import dev.cel.common.types.CelTypeProvider; /** Public interface for type-checking parsed CEL expressions. */ @Immutable @@ -29,5 +30,8 @@ public interface CelChecker { */ CelValidationResult check(CelAbstractSyntaxTree ast); + /** Returns the underlying type provider. */ + CelTypeProvider getTypeProvider(); + CelCheckerBuilder toCheckerBuilder(); } diff --git a/checker/src/main/java/dev/cel/checker/CelCheckerBuilder.java b/checker/src/main/java/dev/cel/checker/CelCheckerBuilder.java index 53827c387..a7d531f88 100644 --- a/checker/src/main/java/dev/cel/checker/CelCheckerBuilder.java +++ b/checker/src/main/java/dev/cel/checker/CelCheckerBuilder.java @@ -21,6 +21,7 @@ import com.google.protobuf.DescriptorProtos.FileDescriptorSet; import com.google.protobuf.Descriptors.Descriptor; import com.google.protobuf.Descriptors.FileDescriptor; +import dev.cel.common.CelContainer; import dev.cel.common.CelFunctionDecl; import dev.cel.common.CelOptions; import dev.cel.common.CelVarDecl; @@ -34,12 +35,18 @@ public interface CelCheckerBuilder { @CanIgnoreReturnValue CelCheckerBuilder setOptions(CelOptions options); + /** Retrieves the currently configured {@link CelOptions} in the builder. */ + CelOptions options(); + /** - * Set the {@code container} name to use as the namespace for resolving CEL expression variables - * and functions. + * Set the {@link CelContainer} to use as the namespace for resolving CEL expression variables and + * functions. */ @CanIgnoreReturnValue - CelCheckerBuilder setContainer(String container); + CelCheckerBuilder setContainer(CelContainer container); + + /** Retrieves the currently configured {@link CelContainer} in the builder. */ + CelContainer container(); /** Add variable and function {@code declarations} to the CEL environment. */ @CanIgnoreReturnValue @@ -152,6 +159,14 @@ public interface CelCheckerBuilder { @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. + */ + @CanIgnoreReturnValue + CelCheckerBuilder setStandardDeclarations(CelStandardDeclarations standardDeclarations); + /** Adds one or more libraries for parsing and type-checking. */ @CanIgnoreReturnValue CelCheckerBuilder addLibraries(CelCheckerLibrary... libraries); diff --git a/checker/src/main/java/dev/cel/checker/CelCheckerLegacyImpl.java b/checker/src/main/java/dev/cel/checker/CelCheckerLegacyImpl.java index 950c919e3..ceab0fa93 100644 --- a/checker/src/main/java/dev/cel/checker/CelCheckerLegacyImpl.java +++ b/checker/src/main/java/dev/cel/checker/CelCheckerLegacyImpl.java @@ -31,6 +31,7 @@ import com.google.protobuf.Descriptors.Descriptor; import com.google.protobuf.Descriptors.FileDescriptor; import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelContainer; import dev.cel.common.CelDescriptorUtil; import dev.cel.common.CelFunctionDecl; import dev.cel.common.CelIssue; @@ -45,15 +46,16 @@ import dev.cel.common.internal.EnvVisitable; import dev.cel.common.internal.EnvVisitor; import dev.cel.common.internal.Errors; +import dev.cel.common.types.CelProtoTypes; import dev.cel.common.types.CelType; import dev.cel.common.types.CelTypeProvider; -import dev.cel.common.types.CelTypes; import dev.cel.common.types.ProtoMessageTypeProvider; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.SortedSet; import java.util.TreeSet; +import org.jspecify.annotations.Nullable; /** * {@code CelChecker} implementation which uses the original CEL-Java APIs to provide a simple, @@ -67,7 +69,7 @@ public final class CelCheckerLegacyImpl implements CelChecker, EnvVisitable { private final CelOptions celOptions; - private final String container; + private final CelContainer container; private final ImmutableSet identDeclarations; private final ImmutableSet functionDeclarations; private final Optional expectedResultType; @@ -75,11 +77,17 @@ public final class CelCheckerLegacyImpl implements CelChecker, EnvVisitable { @SuppressWarnings("Immutable") private final TypeProvider typeProvider; + private final CelTypeProvider celTypeProvider; private final boolean standardEnvironmentEnabled; - // Builder is mutable by design. APIs must make defensive copies in and out of this class. + private final CelStandardDeclarations overriddenStandardDeclarations; + + // This does not affect the type-checking behavior in any manner. @SuppressWarnings("Immutable") - private final Builder checkerBuilder; + private final ImmutableSet checkerLibraries; + + private final ImmutableSet fileDescriptors; + private final ImmutableSet protoTypeMasks; @Override public CelValidationResult check(CelAbstractSyntaxTree ast) { @@ -98,9 +106,34 @@ public CelValidationResult check(CelAbstractSyntaxTree ast) { return new CelValidationResult(checkedAst, ImmutableList.of()); } + @Override + public CelTypeProvider getTypeProvider() { + return this.celTypeProvider; + } + @Override public CelCheckerBuilder toCheckerBuilder() { - return new Builder(checkerBuilder); + CelCheckerBuilder builder = + new Builder() + .addIdentDeclarations(identDeclarations) + .setOptions(celOptions) + .setTypeProvider(celTypeProvider) + .setContainer(container) + .setStandardEnvironmentEnabled(standardEnvironmentEnabled) + .addFunctionDeclarations(functionDeclarations) + .addLibraries(checkerLibraries) + .addFileTypes(fileDescriptors) + .addProtoTypeMasks(protoTypeMasks); + + if (expectedResultType.isPresent()) { + builder.setResultType(expectedResultType.get()); + } + + if (overriddenStandardDeclarations != null) { + builder.setStandardDeclarations(overriddenStandardDeclarations); + } + + return builder; } @Override @@ -131,6 +164,8 @@ private Env getEnv(Errors errors) { Env env; if (standardEnvironmentEnabled) { env = Env.standard(errors, typeProvider, celOptions); + } else if (overriddenStandardDeclarations != null) { + env = Env.standard(overriddenStandardDeclarations, errors, typeProvider, celOptions); } else { env = Env.unconfigured(errors, typeProvider, celOptions); } @@ -153,12 +188,13 @@ public static final class Builder implements CelCheckerBuilder { private final ImmutableSet.Builder messageTypes; private final ImmutableSet.Builder fileTypes; private final ImmutableSet.Builder celCheckerLibraries; + private CelContainer container; private CelOptions celOptions; - private String container; private CelType expectedResultType; private TypeProvider customTypeProvider; private CelTypeProvider celTypeProvider; private boolean standardEnvironmentEnabled; + private CelStandardDeclarations standardDeclarations; @Override public CelCheckerBuilder setOptions(CelOptions celOptions) { @@ -167,12 +203,22 @@ public CelCheckerBuilder setOptions(CelOptions celOptions) { } @Override - public CelCheckerBuilder setContainer(String container) { + public CelOptions options() { + return this.celOptions; + } + + @Override + public CelCheckerBuilder setContainer(CelContainer container) { checkNotNull(container); this.container = container; return this; } + @Override + public CelContainer container() { + return this.container; + } + @Override public CelCheckerBuilder addDeclarations(Decl... declarations) { checkNotNull(declarations); @@ -188,7 +234,7 @@ public CelCheckerBuilder addDeclarations(Iterable declarations) { CelIdentDecl.Builder identBuilder = CelIdentDecl.newBuilder() .setName(decl.getName()) - .setType(CelTypes.typeToCelType(decl.getIdent().getType())) + .setType(CelProtoTypes.typeToCelType(decl.getIdent().getType())) // Note: Setting doc and constant value exists for compatibility reason. This // should not be set by the users. .setDoc(decl.getIdent().getDoc()); @@ -267,7 +313,7 @@ public CelCheckerBuilder setResultType(CelType resultType) { @Override public CelCheckerBuilder setProtoResultType(Type resultType) { checkNotNull(resultType); - return setResultType(CelTypes.typeToCelType(resultType)); + return setResultType(CelProtoTypes.typeToCelType(resultType)); } @Override @@ -314,7 +360,13 @@ public CelCheckerBuilder addFileTypes(FileDescriptorSet fileDescriptorSet) { @Override public CelCheckerBuilder setStandardEnvironmentEnabled(boolean value) { - standardEnvironmentEnabled = value; + this.standardEnvironmentEnabled = value; + return this; + } + + @Override + public CelCheckerBuilder setStandardDeclarations(CelStandardDeclarations standardDeclarations) { + this.standardDeclarations = checkNotNull(standardDeclarations); return this; } @@ -331,43 +383,66 @@ public CelCheckerBuilder addLibraries(Iterable libr return this; } - // The following getters exist for asserting immutability for collections held by this builder, - // and shouldn't be exposed to the public. + @CanIgnoreReturnValue + Builder addIdentDeclarations(ImmutableSet identDeclarations) { + this.identDeclarations.addAll(identDeclarations); + return this; + } + + // The following getters marked @VisibleForTesting exist for testing toCheckerBuilder copies + // over all properties. Do not expose these to public @VisibleForTesting - ImmutableSet.Builder getFunctionDecls() { + ImmutableSet.Builder functionDecls() { return this.functionDeclarations; } @VisibleForTesting - ImmutableSet.Builder getIdentDecls() { + ImmutableSet.Builder identDecls() { return this.identDeclarations; } @VisibleForTesting - ImmutableSet.Builder getProtoTypeMasks() { + ImmutableSet.Builder protoTypeMasks() { return this.protoTypeMasks; } @VisibleForTesting - ImmutableSet.Builder getMessageTypes() { + ImmutableSet.Builder messageTypes() { return this.messageTypes; } @VisibleForTesting - ImmutableSet.Builder getFileTypes() { + ImmutableSet.Builder fileTypes() { return this.fileTypes; } @VisibleForTesting - ImmutableSet.Builder getCheckerLibraries() { + ImmutableSet.Builder checkerLibraries() { return this.celCheckerLibraries; } + @VisibleForTesting + CelStandardDeclarations standardDeclarations() { + return this.standardDeclarations; + } + + @VisibleForTesting + CelTypeProvider celTypeProvider() { + return this.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 - celCheckerLibraries.build().forEach(celLibrary -> celLibrary.setCheckerOptions(this)); + ImmutableSet checkerLibraries = celCheckerLibraries.build(); + checkerLibraries.forEach(celLibrary -> celLibrary.setCheckerOptions(this)); // Configure the type provider. ImmutableSet fileTypeSet = fileTypes.build(); @@ -381,9 +456,12 @@ public CelCheckerLegacyImpl build() { } CelTypeProvider messageTypeProvider = - new ProtoMessageTypeProvider( - CelDescriptorUtil.getAllDescriptorsFromFileDescriptor( - fileTypeSet, celOptions.resolveTypeDependencies())); + ProtoMessageTypeProvider.newBuilder() + .setAllowJsonFieldNames(celOptions.enableJsonFieldNames()) + .setResolveTypeDependencies(celOptions.resolveTypeDependencies()) + .addFileDescriptors(fileTypeSet) + .build(); + if (celTypeProvider != null && fileTypeSet.isEmpty()) { messageTypeProvider = celTypeProvider; } else if (celTypeProvider != null) { @@ -422,8 +500,12 @@ public CelCheckerLegacyImpl build() { functionDeclarations.build(), Optional.fromNullable(expectedResultType), legacyProvider, + messageTypeProvider, standardEnvironmentEnabled, - this); + standardDeclarations, + checkerLibraries, + fileTypeSet, + protoTypeMaskSet); } private Builder() { @@ -434,51 +516,35 @@ private Builder() { this.messageTypes = ImmutableSet.builder(); this.protoTypeMasks = ImmutableSet.builder(); this.celCheckerLibraries = ImmutableSet.builder(); - this.container = ""; - } - - private Builder(Builder builder) { - // The following properties are either immutable or simple primitives, thus can be assigned - // directly. - this.celOptions = builder.celOptions; - this.celTypeProvider = builder.celTypeProvider; - this.container = builder.container; - this.customTypeProvider = builder.customTypeProvider; - this.expectedResultType = builder.expectedResultType; - this.standardEnvironmentEnabled = builder.standardEnvironmentEnabled; - // The following needs to be deep copied as they are collection builders - this.functionDeclarations = deepCopy(builder.functionDeclarations); - this.identDeclarations = deepCopy(builder.identDeclarations); - this.fileTypes = deepCopy(builder.fileTypes); - this.messageTypes = deepCopy(builder.messageTypes); - this.protoTypeMasks = deepCopy(builder.protoTypeMasks); - this.celCheckerLibraries = deepCopy(builder.celCheckerLibraries); - } - - private static ImmutableSet.Builder deepCopy(ImmutableSet.Builder builderToCopy) { - ImmutableSet.Builder newBuilder = ImmutableSet.builder(); - newBuilder.addAll(builderToCopy.build()); - return newBuilder; + this.container = CelContainer.ofName(""); } } private CelCheckerLegacyImpl( CelOptions celOptions, - String container, + CelContainer container, ImmutableSet identDeclarations, ImmutableSet functionDeclarations, Optional expectedResultType, TypeProvider typeProvider, + CelTypeProvider celTypeProvider, boolean standardEnvironmentEnabled, - Builder checkerBuilder) { + @Nullable CelStandardDeclarations overriddenStandardDeclarations, + ImmutableSet checkerLibraries, + ImmutableSet fileDescriptors, + ImmutableSet protoTypeMasks) { this.celOptions = celOptions; this.container = container; this.identDeclarations = identDeclarations; this.functionDeclarations = functionDeclarations; this.expectedResultType = expectedResultType; this.typeProvider = typeProvider; + this.celTypeProvider = celTypeProvider; this.standardEnvironmentEnabled = standardEnvironmentEnabled; - this.checkerBuilder = new Builder(checkerBuilder); + this.overriddenStandardDeclarations = overriddenStandardDeclarations; + this.checkerLibraries = checkerLibraries; + this.fileDescriptors = fileDescriptors; + this.protoTypeMasks = protoTypeMasks; } private static ImmutableList errorsToIssues(Errors errors) { @@ -489,7 +555,11 @@ private static ImmutableList errorsToIssues(Errors errors) { e -> { Errors.SourceLocation loc = errors.getPositionLocation(e.position()); CelSourceLocation newLoc = CelSourceLocation.of(loc.line(), loc.column() - 1); - return issueBuilder.setMessage(e.rawMessage()).setSourceLocation(newLoc).build(); + return issueBuilder + .setExprId(e.exprId()) + .setMessage(e.rawMessage()) + .setSourceLocation(newLoc) + .build(); }) .collect(toImmutableList()); } diff --git a/checker/src/main/java/dev/cel/checker/CelIdentDecl.java b/checker/src/main/java/dev/cel/checker/CelIdentDecl.java index ac71523fe..60f747b77 100644 --- a/checker/src/main/java/dev/cel/checker/CelIdentDecl.java +++ b/checker/src/main/java/dev/cel/checker/CelIdentDecl.java @@ -23,8 +23,8 @@ import dev.cel.common.annotations.Internal; import dev.cel.common.ast.CelConstant; import dev.cel.common.ast.CelExprConverter; +import dev.cel.common.types.CelProtoTypes; import dev.cel.common.types.CelType; -import dev.cel.common.types.CelTypes; import java.util.Optional; /** @@ -57,7 +57,7 @@ public static Decl celIdentToDecl(CelIdentDecl identDecl) { IdentDecl.Builder identBuilder = IdentDecl.newBuilder() .setDoc(identDecl.doc()) - .setType(CelTypes.celTypeToType(identDecl.type())); + .setType(CelProtoTypes.celTypeToType(identDecl.type())); if (identDecl.constant().isPresent()) { identBuilder.setValue(CelExprConverter.celConstantToExprConstant(identDecl.constant().get())); } diff --git a/checker/src/main/java/dev/cel/checker/CelProtoExprVisitor.java b/checker/src/main/java/dev/cel/checker/CelProtoExprVisitor.java index cd5656642..08552d56f 100644 --- a/checker/src/main/java/dev/cel/checker/CelProtoExprVisitor.java +++ b/checker/src/main/java/dev/cel/checker/CelProtoExprVisitor.java @@ -18,7 +18,7 @@ import dev.cel.common.CelProtoAbstractSyntaxTree; /** - * CEL expression visitor implementation based on the {@link com.google.api.expr.Expr} proto. + * CEL expression visitor implementation based on the {@link dev.cel.expr.Expr} proto. * *

Note: Prefer using {@link dev.cel.common.ast.CelExprVisitor} if protobuf support is not * needed. diff --git a/checker/src/main/java/dev/cel/checker/CelStandardDeclarations.java b/checker/src/main/java/dev/cel/checker/CelStandardDeclarations.java new file mode 100644 index 000000000..53615604f --- /dev/null +++ b/checker/src/main/java/dev/cel/checker/CelStandardDeclarations.java @@ -0,0 +1,1796 @@ +// 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. + +package dev.cel.checker; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.ImmutableSet.toImmutableSet; +import static java.util.Arrays.stream; + +import com.google.common.collect.ImmutableSet; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.CelFunctionDecl; +import dev.cel.common.CelOverloadDecl; +import dev.cel.common.Operator; +import dev.cel.common.types.CelType; +import dev.cel.common.types.CelTypes; +import dev.cel.common.types.ListType; +import dev.cel.common.types.MapType; +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. + * + *

Refer to CEL + * Specification for comprehensive listing of functions and identifiers included in the standard + * environment. + */ +@Immutable +public final class CelStandardDeclarations { + // Some shortcuts we use when building declarations. + private static final TypeParamType TYPE_PARAM_A = TypeParamType.create("A"); + private static final ListType LIST_OF_A = ListType.create(TYPE_PARAM_A); + 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); + + private final ImmutableSet celFunctionDecls; + private final ImmutableSet celIdentDecls; + + /** Enumeration of Standard Functions. */ + public enum StandardFunction { + // Deprecated - use {@link #IN} + OLD_IN( + true, + Operator.OLD_IN.getFunction(), + () -> + CelOverloadDecl.newGlobalOverload( + "in_list", "list membership", SimpleType.BOOL, TYPE_PARAM_A, LIST_OF_A), + () -> + CelOverloadDecl.newGlobalOverload( + "in_map", "map key membership", SimpleType.BOOL, TYPE_PARAM_A, MAP_OF_AB)), + + // Deprecated - use {@link #NOT_STRICTLY_FALSE} + OLD_NOT_STRICTLY_FALSE( + true, + Operator.OLD_NOT_STRICTLY_FALSE.getFunction(), + () -> + CelOverloadDecl.newGlobalOverload( + "not_strictly_false", + "false if argument is false, true otherwise (including errors and unknowns)", + SimpleType.BOOL, + SimpleType.BOOL)), + + // Internal (rewritten by macro) + IN(Operator.IN, Overload.InternalOperator.IN_LIST, Overload.InternalOperator.IN_MAP), + NOT_STRICTLY_FALSE(Operator.NOT_STRICTLY_FALSE, Overload.InternalOperator.NOT_STRICTLY_FALSE), + TYPE("type", Overload.InternalOperator.TYPE), + + // Booleans + CONDITIONAL(Operator.CONDITIONAL, Overload.BooleanOperator.CONDITIONAL), + LOGICAL_NOT(Operator.LOGICAL_NOT, Overload.BooleanOperator.LOGICAL_NOT), + LOGICAL_OR(Operator.LOGICAL_OR, Overload.BooleanOperator.LOGICAL_OR), + LOGICAL_AND(Operator.LOGICAL_AND, Overload.BooleanOperator.LOGICAL_AND), + + // Relations + EQUALS(Operator.EQUALS, Overload.Relation.EQUALS), + NOT_EQUALS(Operator.NOT_EQUALS, Overload.Relation.NOT_EQUALS), + + // Arithmetic + ADD( + Operator.ADD, + Overload.Arithmetic.ADD_INT64, + Overload.Arithmetic.ADD_UINT64, + Overload.Arithmetic.ADD_DOUBLE, + Overload.Arithmetic.ADD_STRING, + Overload.Arithmetic.ADD_BYTES, + Overload.Arithmetic.ADD_LIST, + Overload.Arithmetic.ADD_TIMESTAMP_DURATION, + Overload.Arithmetic.ADD_DURATION_TIMESTAMP, + Overload.Arithmetic.ADD_DURATION_DURATION), + SUBTRACT( + Operator.SUBTRACT, + Overload.Arithmetic.SUBTRACT_INT64, + Overload.Arithmetic.SUBTRACT_UINT64, + Overload.Arithmetic.SUBTRACT_DOUBLE, + Overload.Arithmetic.SUBTRACT_TIMESTAMP_TIMESTAMP, + Overload.Arithmetic.SUBTRACT_TIMESTAMP_DURATION, + Overload.Arithmetic.SUBTRACT_DURATION_DURATION), + MULTIPLY( + Operator.MULTIPLY, + Overload.Arithmetic.MULTIPLY_INT64, + Overload.Arithmetic.MULTIPLY_UINT64, + Overload.Arithmetic.MULTIPLY_DOUBLE), + DIVIDE( + Operator.DIVIDE, + Overload.Arithmetic.DIVIDE_INT64, + Overload.Arithmetic.DIVIDE_UINT64, + Overload.Arithmetic.DIVIDE_DOUBLE), + MODULO(Operator.MODULO, Overload.Arithmetic.MODULO_INT64, Overload.Arithmetic.MODULO_UINT64), + + NEGATE(Operator.NEGATE, Overload.Arithmetic.NEGATE_INT64, Overload.Arithmetic.NEGATE_DOUBLE), + + // Index + INDEX(Operator.INDEX, Overload.Index.INDEX_LIST, Overload.Index.INDEX_MAP), + + // Size + SIZE( + "size", + Overload.Size.SIZE_STRING, + Overload.Size.SIZE_BYTES, + Overload.Size.SIZE_LIST, + Overload.Size.SIZE_MAP, + Overload.Size.STRING_SIZE, + Overload.Size.BYTES_SIZE, + Overload.Size.LIST_SIZE, + Overload.Size.MAP_SIZE), + + // Conversions + INT( + "int", + Overload.Conversions.INT64_TO_INT64, + Overload.Conversions.UINT64_TO_INT64, + Overload.Conversions.DOUBLE_TO_INT64, + Overload.Conversions.STRING_TO_INT64, + Overload.Conversions.TIMESTAMP_TO_INT64), + UINT( + "uint", + Overload.Conversions.UINT64_TO_UINT64, + Overload.Conversions.INT64_TO_UINT64, + Overload.Conversions.DOUBLE_TO_UINT64, + Overload.Conversions.STRING_TO_UINT64), + DOUBLE( + "double", + Overload.Conversions.DOUBLE_TO_DOUBLE, + Overload.Conversions.INT64_TO_DOUBLE, + Overload.Conversions.UINT64_TO_DOUBLE, + Overload.Conversions.STRING_TO_DOUBLE), + STRING( + "string", + Overload.Conversions.STRING_TO_STRING, + Overload.Conversions.INT64_TO_STRING, + Overload.Conversions.UINT64_TO_STRING, + Overload.Conversions.DOUBLE_TO_STRING, + Overload.Conversions.BOOL_TO_STRING, + Overload.Conversions.BYTES_TO_STRING, + Overload.Conversions.TIMESTAMP_TO_STRING, + Overload.Conversions.DURATION_TO_STRING), + BYTES("bytes", Overload.Conversions.BYTES_TO_BYTES, Overload.Conversions.STRING_TO_BYTES), + DYN("dyn", Overload.Conversions.TO_DYN), + DURATION( + "duration", + Overload.Conversions.DURATION_TO_DURATION, + Overload.Conversions.STRING_TO_DURATION), + TIMESTAMP( + "timestamp", + Overload.Conversions.STRING_TO_TIMESTAMP, + Overload.Conversions.TIMESTAMP_TO_TIMESTAMP, + Overload.Conversions.INT64_TO_TIMESTAMP), + BOOL("bool", Overload.Conversions.BOOL_TO_BOOL, Overload.Conversions.STRING_TO_BOOL), + + // String matchers + MATCHES("matches", Overload.StringMatchers.MATCHES, Overload.StringMatchers.MATCHES_STRING), + CONTAINS("contains", Overload.StringMatchers.CONTAINS_STRING), + ENDS_WITH("endsWith", Overload.StringMatchers.ENDS_WITH_STRING), + STARTS_WITH("startsWith", Overload.StringMatchers.STARTS_WITH_STRING), + + // Date/time operations + GET_FULL_YEAR( + "getFullYear", + Overload.DateTime.TIMESTAMP_TO_YEAR, + Overload.DateTime.TIMESTAMP_TO_YEAR_WITH_TZ), + GET_MONTH( + "getMonth", + Overload.DateTime.TIMESTAMP_TO_MONTH, + Overload.DateTime.TIMESTAMP_TO_MONTH_WITH_TZ), + GET_DAY_OF_YEAR( + "getDayOfYear", + Overload.DateTime.TIMESTAMP_TO_DAY_OF_YEAR, + Overload.DateTime.TIMESTAMP_TO_DAY_OF_YEAR_WITH_TZ), + GET_DAY_OF_MONTH( + "getDayOfMonth", + Overload.DateTime.TIMESTAMP_TO_DAY_OF_MONTH, + Overload.DateTime.TIMESTAMP_TO_DAY_OF_MONTH_WITH_TZ), + GET_DATE( + "getDate", + Overload.DateTime.TIMESTAMP_TO_DAY_OF_MONTH_1_BASED, + Overload.DateTime.TIMESTAMP_TO_DAY_OF_MONTH_1_BASED_WITH_TZ), + GET_DAY_OF_WEEK( + "getDayOfWeek", + Overload.DateTime.TIMESTAMP_TO_DAY_OF_WEEK, + Overload.DateTime.TIMESTAMP_TO_DAY_OF_WEEK_WITH_TZ), + GET_HOURS( + "getHours", + Overload.DateTime.TIMESTAMP_TO_HOURS, + Overload.DateTime.TIMESTAMP_TO_HOURS_WITH_TZ, + Overload.DateTime.DURATION_TO_HOURS), + GET_MINUTES( + "getMinutes", + Overload.DateTime.TIMESTAMP_TO_MINUTES, + Overload.DateTime.TIMESTAMP_TO_MINUTES_WITH_TZ, + Overload.DateTime.DURATION_TO_MINUTES), + GET_SECONDS( + "getSeconds", + Overload.DateTime.TIMESTAMP_TO_SECONDS, + Overload.DateTime.TIMESTAMP_TO_SECONDS_WITH_TZ, + Overload.DateTime.DURATION_TO_SECONDS), + GET_MILLISECONDS( + "getMilliseconds", + Overload.DateTime.TIMESTAMP_TO_MILLISECONDS, + Overload.DateTime.TIMESTAMP_TO_MILLISECONDS_WITH_TZ, + Overload.DateTime.DURATION_TO_MILLISECONDS), + + // Comparisons + LESS( + Operator.LESS, + Overload.Comparison.LESS_BOOL, + Overload.Comparison.LESS_INT64, + Overload.Comparison.LESS_UINT64, + Overload.Comparison.LESS_DOUBLE, + Overload.Comparison.LESS_STRING, + Overload.Comparison.LESS_BYTES, + Overload.Comparison.LESS_TIMESTAMP, + Overload.Comparison.LESS_DURATION, + Overload.Comparison.LESS_INT64_UINT64, + Overload.Comparison.LESS_UINT64_INT64, + Overload.Comparison.LESS_INT64_DOUBLE, + Overload.Comparison.LESS_DOUBLE_INT64, + Overload.Comparison.LESS_UINT64_DOUBLE, + Overload.Comparison.LESS_DOUBLE_UINT64), + LESS_EQUALS( + Operator.LESS_EQUALS, + Overload.Comparison.LESS_EQUALS_BOOL, + Overload.Comparison.LESS_EQUALS_INT64, + Overload.Comparison.LESS_EQUALS_UINT64, + Overload.Comparison.LESS_EQUALS_DOUBLE, + Overload.Comparison.LESS_EQUALS_STRING, + Overload.Comparison.LESS_EQUALS_BYTES, + Overload.Comparison.LESS_EQUALS_TIMESTAMP, + Overload.Comparison.LESS_EQUALS_DURATION, + Overload.Comparison.LESS_EQUALS_INT64_UINT64, + Overload.Comparison.LESS_EQUALS_UINT64_INT64, + Overload.Comparison.LESS_EQUALS_INT64_DOUBLE, + Overload.Comparison.LESS_EQUALS_DOUBLE_INT64, + Overload.Comparison.LESS_EQUALS_UINT64_DOUBLE, + Overload.Comparison.LESS_EQUALS_DOUBLE_UINT64), + GREATER( + Operator.GREATER, + Overload.Comparison.GREATER_BOOL, + Overload.Comparison.GREATER_INT64, + Overload.Comparison.GREATER_UINT64, + Overload.Comparison.GREATER_DOUBLE, + Overload.Comparison.GREATER_STRING, + Overload.Comparison.GREATER_BYTES, + Overload.Comparison.GREATER_TIMESTAMP, + Overload.Comparison.GREATER_DURATION, + Overload.Comparison.GREATER_INT64_UINT64, + Overload.Comparison.GREATER_UINT64_INT64, + Overload.Comparison.GREATER_INT64_DOUBLE, + Overload.Comparison.GREATER_DOUBLE_INT64, + Overload.Comparison.GREATER_UINT64_DOUBLE, + Overload.Comparison.GREATER_DOUBLE_UINT64), + GREATER_EQUALS( + Operator.GREATER_EQUALS, + Overload.Comparison.GREATER_EQUALS_BOOL, + Overload.Comparison.GREATER_EQUALS_INT64, + Overload.Comparison.GREATER_EQUALS_UINT64, + Overload.Comparison.GREATER_EQUALS_DOUBLE, + Overload.Comparison.GREATER_EQUALS_STRING, + Overload.Comparison.GREATER_EQUALS_BYTES, + Overload.Comparison.GREATER_EQUALS_TIMESTAMP, + Overload.Comparison.GREATER_EQUALS_DURATION, + Overload.Comparison.GREATER_EQUALS_INT64_UINT64, + Overload.Comparison.GREATER_EQUALS_UINT64_INT64, + Overload.Comparison.GREATER_EQUALS_INT64_DOUBLE, + Overload.Comparison.GREATER_EQUALS_DOUBLE_INT64, + Overload.Comparison.GREATER_EQUALS_UINT64_DOUBLE, + Overload.Comparison.GREATER_EQUALS_DOUBLE_UINT64), + ; + + private final String functionName; + private final CelFunctionDecl celFunctionDecl; + private final ImmutableSet standardOverloads; + private final boolean isDeprecated; + + /** Container class for CEL standard function overloads. */ + public static final class Overload { + + /** + * Overloads for internal functions that may have been rewritten by macros (ex: @in), or those + * used for special purposes (comprehensions, type denotations etc). + */ + public enum InternalOperator implements StandardOverload { + IN_LIST( + CelOverloadDecl.newGlobalOverload( + "in_list", "list membership", SimpleType.BOOL, TYPE_PARAM_A, LIST_OF_A)), + IN_MAP( + CelOverloadDecl.newGlobalOverload( + "in_map", "map key membership", SimpleType.BOOL, TYPE_PARAM_A, MAP_OF_AB)), + NOT_STRICTLY_FALSE( + CelOverloadDecl.newGlobalOverload( + "not_strictly_false", + "false if argument is false, true otherwise (including errors and unknowns)", + SimpleType.BOOL, + SimpleType.BOOL)), + TYPE( + CelOverloadDecl.newGlobalOverload( + "type", "returns type of value", TypeType.create(TYPE_PARAM_A), TYPE_PARAM_A)), + ; + + private final CelOverloadDecl celOverloadDecl; + + InternalOperator(CelOverloadDecl overloadDecl) { + this.celOverloadDecl = overloadDecl; + } + + @Override + public CelOverloadDecl celOverloadDecl() { + return this.celOverloadDecl; + } + } + + /** Overloads for logical operators that return a bool as a result. */ + public enum BooleanOperator implements StandardOverload { + CONDITIONAL( + CelOverloadDecl.newGlobalOverload( + "conditional", + "conditional", + TYPE_PARAM_A, + SimpleType.BOOL, + TYPE_PARAM_A, + TYPE_PARAM_A)), + LOGICAL_NOT( + CelOverloadDecl.newGlobalOverload( + "logical_not", "logical not", SimpleType.BOOL, SimpleType.BOOL)), + LOGICAL_OR( + CelOverloadDecl.newGlobalOverload( + "logical_or", "logical or", SimpleType.BOOL, SimpleType.BOOL, SimpleType.BOOL)), + LOGICAL_AND( + CelOverloadDecl.newGlobalOverload( + "logical_and", "logical_and", SimpleType.BOOL, SimpleType.BOOL, SimpleType.BOOL)), + ; + + private final CelOverloadDecl celOverloadDecl; + + BooleanOperator(CelOverloadDecl overloadDecl) { + this.celOverloadDecl = overloadDecl; + } + + @Override + public CelOverloadDecl celOverloadDecl() { + return this.celOverloadDecl; + } + } + + /** Overloads for functions that test relations. */ + public enum Relation implements StandardOverload { + EQUALS( + CelOverloadDecl.newGlobalOverload( + "equals", "equality", SimpleType.BOOL, TYPE_PARAM_A, TYPE_PARAM_A)), + NOT_EQUALS( + CelOverloadDecl.newGlobalOverload( + "not_equals", "inequality", SimpleType.BOOL, TYPE_PARAM_A, TYPE_PARAM_A)), + ; + private final CelOverloadDecl celOverloadDecl; + + Relation(CelOverloadDecl overloadDecl) { + this.celOverloadDecl = overloadDecl; + } + + @Override + public CelOverloadDecl celOverloadDecl() { + return this.celOverloadDecl; + } + } + + /** Overloads for performing arithmetic operations. */ + public enum Arithmetic implements StandardOverload { + + // Add + ADD_STRING( + CelOverloadDecl.newGlobalOverload( + "add_string", + "string concatenation", + SimpleType.STRING, + SimpleType.STRING, + SimpleType.STRING)), + ADD_BYTES( + CelOverloadDecl.newGlobalOverload( + "add_bytes", + "bytes concatenation", + SimpleType.BYTES, + SimpleType.BYTES, + SimpleType.BYTES)), + ADD_LIST( + CelOverloadDecl.newGlobalOverload( + "add_list", "list concatenation", LIST_OF_A, LIST_OF_A, LIST_OF_A)), + ADD_TIMESTAMP_DURATION( + CelOverloadDecl.newGlobalOverload( + "add_timestamp_duration", + "arithmetic", + SimpleType.TIMESTAMP, + SimpleType.TIMESTAMP, + SimpleType.DURATION)), + ADD_DURATION_TIMESTAMP( + CelOverloadDecl.newGlobalOverload( + "add_duration_timestamp", + "arithmetic", + SimpleType.TIMESTAMP, + SimpleType.DURATION, + SimpleType.TIMESTAMP)), + ADD_DURATION_DURATION( + CelOverloadDecl.newGlobalOverload( + "add_duration_duration", + "arithmetic", + SimpleType.DURATION, + SimpleType.DURATION, + SimpleType.DURATION)), + ADD_INT64( + CelOverloadDecl.newGlobalOverload( + "add_int64", "arithmetic", SimpleType.INT, SimpleType.INT, SimpleType.INT)), + ADD_UINT64( + CelOverloadDecl.newGlobalOverload( + "add_uint64", "arithmetic", SimpleType.UINT, SimpleType.UINT, SimpleType.UINT)), + ADD_DOUBLE( + CelOverloadDecl.newGlobalOverload( + "add_double", + "arithmetic", + SimpleType.DOUBLE, + SimpleType.DOUBLE, + SimpleType.DOUBLE)), + + // Subtract + SUBTRACT_INT64( + CelOverloadDecl.newGlobalOverload( + "subtract_int64", "arithmetic", SimpleType.INT, SimpleType.INT, SimpleType.INT)), + SUBTRACT_UINT64( + CelOverloadDecl.newGlobalOverload( + "subtract_uint64", + "arithmetic", + SimpleType.UINT, + SimpleType.UINT, + SimpleType.UINT)), + SUBTRACT_DOUBLE( + CelOverloadDecl.newGlobalOverload( + "subtract_double", + "arithmetic", + SimpleType.DOUBLE, + SimpleType.DOUBLE, + SimpleType.DOUBLE)), + SUBTRACT_TIMESTAMP_TIMESTAMP( + CelOverloadDecl.newGlobalOverload( + "subtract_timestamp_timestamp", + "arithmetic", + SimpleType.DURATION, + SimpleType.TIMESTAMP, + SimpleType.TIMESTAMP)), + SUBTRACT_TIMESTAMP_DURATION( + CelOverloadDecl.newGlobalOverload( + "subtract_timestamp_duration", + "arithmetic", + SimpleType.TIMESTAMP, + SimpleType.TIMESTAMP, + SimpleType.DURATION)), + SUBTRACT_DURATION_DURATION( + CelOverloadDecl.newGlobalOverload( + "subtract_duration_duration", + "arithmetic", + SimpleType.DURATION, + SimpleType.DURATION, + SimpleType.DURATION)), + + // Multiply + MULTIPLY_INT64( + CelOverloadDecl.newGlobalOverload( + "multiply_int64", "arithmetic", SimpleType.INT, SimpleType.INT, SimpleType.INT)), + MULTIPLY_UINT64( + CelOverloadDecl.newGlobalOverload( + "multiply_uint64", + "arithmetic", + SimpleType.UINT, + SimpleType.UINT, + SimpleType.UINT)), + MULTIPLY_DOUBLE( + CelOverloadDecl.newGlobalOverload( + "multiply_double", + "arithmetic", + SimpleType.DOUBLE, + SimpleType.DOUBLE, + SimpleType.DOUBLE)), + + // Divide + DIVIDE_INT64( + CelOverloadDecl.newGlobalOverload( + "divide_int64", "arithmetic", SimpleType.INT, SimpleType.INT, SimpleType.INT)), + DIVIDE_UINT64( + CelOverloadDecl.newGlobalOverload( + "divide_uint64", "arithmetic", SimpleType.UINT, SimpleType.UINT, SimpleType.UINT)), + DIVIDE_DOUBLE( + CelOverloadDecl.newGlobalOverload( + "divide_double", + "arithmetic", + SimpleType.DOUBLE, + SimpleType.DOUBLE, + SimpleType.DOUBLE)), + + // Modulo + MODULO_INT64( + CelOverloadDecl.newGlobalOverload( + "modulo_int64", "arithmetic", SimpleType.INT, SimpleType.INT, SimpleType.INT)), + MODULO_UINT64( + CelOverloadDecl.newGlobalOverload( + "modulo_uint64", "arithmetic", SimpleType.UINT, SimpleType.UINT, SimpleType.UINT)), + NEGATE_INT64( + CelOverloadDecl.newGlobalOverload( + "negate_int64", "negation", SimpleType.INT, SimpleType.INT)), + NEGATE_DOUBLE( + CelOverloadDecl.newGlobalOverload( + "negate_double", "negation", SimpleType.DOUBLE, SimpleType.DOUBLE)), + ; + private final CelOverloadDecl celOverloadDecl; + + Arithmetic(CelOverloadDecl overloadDecl) { + this.celOverloadDecl = overloadDecl; + } + + @Override + public CelOverloadDecl celOverloadDecl() { + return this.celOverloadDecl; + } + } + + /** Overloads for indexing a list or a map. */ + public enum Index implements StandardOverload { + INDEX_LIST( + CelOverloadDecl.newGlobalOverload( + "index_list", "list indexing", TYPE_PARAM_A, LIST_OF_A, SimpleType.INT)), + INDEX_MAP( + CelOverloadDecl.newGlobalOverload( + "index_map", "map indexing", TYPE_PARAM_B, MAP_OF_AB, TYPE_PARAM_A)), + ; + private final CelOverloadDecl celOverloadDecl; + + Index(CelOverloadDecl overloadDecl) { + this.celOverloadDecl = overloadDecl; + } + + @Override + public CelOverloadDecl celOverloadDecl() { + return this.celOverloadDecl; + } + } + + /** Overloads for retrieving the size of a literal or a collection. */ + public enum Size implements StandardOverload { + SIZE_STRING( + CelOverloadDecl.newGlobalOverload( + "size_string", "string length", SimpleType.INT, SimpleType.STRING)), + SIZE_BYTES( + CelOverloadDecl.newGlobalOverload( + "size_bytes", "bytes length", SimpleType.INT, SimpleType.BYTES)), + SIZE_LIST( + CelOverloadDecl.newGlobalOverload("size_list", "list size", SimpleType.INT, LIST_OF_A)), + SIZE_MAP( + CelOverloadDecl.newGlobalOverload("size_map", "map size", SimpleType.INT, MAP_OF_AB)), + STRING_SIZE( + CelOverloadDecl.newMemberOverload( + "string_size", "string length", SimpleType.INT, SimpleType.STRING)), + BYTES_SIZE( + CelOverloadDecl.newMemberOverload( + "bytes_size", "bytes length", SimpleType.INT, SimpleType.BYTES)), + LIST_SIZE( + CelOverloadDecl.newMemberOverload("list_size", "list size", SimpleType.INT, LIST_OF_A)), + MAP_SIZE( + CelOverloadDecl.newMemberOverload("map_size", "map size", SimpleType.INT, MAP_OF_AB)), + ; + + private final CelOverloadDecl celOverloadDecl; + + Size(CelOverloadDecl overloadDecl) { + this.celOverloadDecl = overloadDecl; + } + + @Override + public CelOverloadDecl celOverloadDecl() { + return this.celOverloadDecl; + } + } + + /** Overloads for performing type conversions. */ + public enum Conversions implements StandardOverload { + // Bool conversions + BOOL_TO_BOOL( + CelOverloadDecl.newGlobalOverload( + "bool_to_bool", "type conversion (identity)", SimpleType.BOOL, SimpleType.BOOL)), + STRING_TO_BOOL( + CelOverloadDecl.newGlobalOverload( + "string_to_bool", "type conversion", SimpleType.BOOL, SimpleType.STRING)), + + // Int conversions + INT64_TO_INT64( + CelOverloadDecl.newGlobalOverload( + "int64_to_int64", "type conversion (identity)", SimpleType.INT, SimpleType.INT)), + UINT64_TO_INT64( + CelOverloadDecl.newGlobalOverload( + "uint64_to_int64", "type conversion", SimpleType.INT, SimpleType.UINT)), + DOUBLE_TO_INT64( + CelOverloadDecl.newGlobalOverload( + "double_to_int64", "type conversion", SimpleType.INT, SimpleType.DOUBLE)), + STRING_TO_INT64( + CelOverloadDecl.newGlobalOverload( + "string_to_int64", "type conversion", SimpleType.INT, SimpleType.STRING)), + TIMESTAMP_TO_INT64( + CelOverloadDecl.newGlobalOverload( + "timestamp_to_int64", + "Convert timestamp to int64 in seconds since Unix epoch.", + SimpleType.INT, + SimpleType.TIMESTAMP)), + // Uint conversions + UINT64_TO_UINT64( + CelOverloadDecl.newGlobalOverload( + "uint64_to_uint64", + "type conversion (identity)", + SimpleType.UINT, + SimpleType.UINT)), + INT64_TO_UINT64( + CelOverloadDecl.newGlobalOverload( + "int64_to_uint64", "type conversion", SimpleType.UINT, SimpleType.INT)), + DOUBLE_TO_UINT64( + CelOverloadDecl.newGlobalOverload( + "double_to_uint64", "type conversion", SimpleType.UINT, SimpleType.DOUBLE)), + STRING_TO_UINT64( + CelOverloadDecl.newGlobalOverload( + "string_to_uint64", "type conversion", SimpleType.UINT, SimpleType.STRING)), + // Double conversions + DOUBLE_TO_DOUBLE( + CelOverloadDecl.newGlobalOverload( + "double_to_double", + "type conversion (identity)", + SimpleType.DOUBLE, + SimpleType.DOUBLE)), + INT64_TO_DOUBLE( + CelOverloadDecl.newGlobalOverload( + "int64_to_double", "type conversion", SimpleType.DOUBLE, SimpleType.INT)), + UINT64_TO_DOUBLE( + CelOverloadDecl.newGlobalOverload( + "uint64_to_double", "type conversion", SimpleType.DOUBLE, SimpleType.UINT)), + STRING_TO_DOUBLE( + CelOverloadDecl.newGlobalOverload( + "string_to_double", "type conversion", SimpleType.DOUBLE, SimpleType.STRING)), + // String conversions + STRING_TO_STRING( + CelOverloadDecl.newGlobalOverload( + "string_to_string", + "type conversion (identity)", + SimpleType.STRING, + SimpleType.STRING)), + INT64_TO_STRING( + CelOverloadDecl.newGlobalOverload( + "int64_to_string", "type conversion", SimpleType.STRING, SimpleType.INT)), + UINT64_TO_STRING( + CelOverloadDecl.newGlobalOverload( + "uint64_to_string", "type conversion", SimpleType.STRING, SimpleType.UINT)), + DOUBLE_TO_STRING( + CelOverloadDecl.newGlobalOverload( + "double_to_string", "type conversion", SimpleType.STRING, SimpleType.DOUBLE)), + BOOL_TO_STRING( + CelOverloadDecl.newGlobalOverload( + "bool_to_string", "type conversion", SimpleType.STRING, SimpleType.BOOL)), + BYTES_TO_STRING( + CelOverloadDecl.newGlobalOverload( + "bytes_to_string", "type conversion", SimpleType.STRING, SimpleType.BYTES)), + TIMESTAMP_TO_STRING( + CelOverloadDecl.newGlobalOverload( + "timestamp_to_string", "type_conversion", SimpleType.STRING, SimpleType.TIMESTAMP)), + DURATION_TO_STRING( + CelOverloadDecl.newGlobalOverload( + "duration_to_string", "type_conversion", SimpleType.STRING, SimpleType.DURATION)), + // Bytes conversions + BYTES_TO_BYTES( + CelOverloadDecl.newGlobalOverload( + "bytes_to_bytes", + "type conversion (identity)", + SimpleType.BYTES, + SimpleType.BYTES)), + STRING_TO_BYTES( + CelOverloadDecl.newGlobalOverload( + "string_to_bytes", "type conversion", SimpleType.BYTES, SimpleType.STRING)), + // Duration conversions + DURATION_TO_DURATION( + CelOverloadDecl.newGlobalOverload( + "duration_to_duration", + "type conversion (identity)", + SimpleType.DURATION, + SimpleType.DURATION)), + STRING_TO_DURATION( + CelOverloadDecl.newGlobalOverload( + "string_to_duration", + "type conversion, duration should be end with \"s\", which stands for seconds", + SimpleType.DURATION, + SimpleType.STRING)), + // Timestamp conversions + STRING_TO_TIMESTAMP( + CelOverloadDecl.newGlobalOverload( + "string_to_timestamp", + "Type conversion of strings to timestamps according to RFC3339. Example:" + + " \"1972-01-01T10:00:20.021-05:00\".", + SimpleType.TIMESTAMP, + SimpleType.STRING)), + TIMESTAMP_TO_TIMESTAMP( + CelOverloadDecl.newGlobalOverload( + "timestamp_to_timestamp", + "type conversion (identity)", + SimpleType.TIMESTAMP, + SimpleType.TIMESTAMP)), + INT64_TO_TIMESTAMP( + CelOverloadDecl.newGlobalOverload( + "int64_to_timestamp", + "Type conversion of integers as Unix epoch seconds to timestamps.", + SimpleType.TIMESTAMP, + SimpleType.INT)), + + // Dyn conversions + TO_DYN( + CelOverloadDecl.newGlobalOverload( + "to_dyn", "type conversion", SimpleType.DYN, TYPE_PARAM_A)), + ; + private final CelOverloadDecl celOverloadDecl; + + Conversions(CelOverloadDecl overloadDecl) { + this.celOverloadDecl = overloadDecl; + } + + @Override + public CelOverloadDecl celOverloadDecl() { + return this.celOverloadDecl; + } + } + + /** + * Overloads for functions performing string matching, such as regular expressions or contains + * check. + */ + public enum StringMatchers implements StandardOverload { + MATCHES( + CelOverloadDecl.newGlobalOverload( + "matches", + "matches first argument against regular expression in second argument", + SimpleType.BOOL, + SimpleType.STRING, + SimpleType.STRING)), + + MATCHES_STRING( + CelOverloadDecl.newMemberOverload( + "matches_string", + "matches the self argument against regular expression in first argument", + SimpleType.BOOL, + SimpleType.STRING, + SimpleType.STRING)), + + CONTAINS_STRING( + CelOverloadDecl.newMemberOverload( + "contains_string", + "tests whether the string operand contains the substring", + SimpleType.BOOL, + SimpleType.STRING, + SimpleType.STRING)), + + ENDS_WITH_STRING( + CelOverloadDecl.newMemberOverload( + "ends_with_string", + "tests whether the string operand ends with the suffix argument", + SimpleType.BOOL, + SimpleType.STRING, + SimpleType.STRING)), + + STARTS_WITH_STRING( + CelOverloadDecl.newMemberOverload( + "starts_with_string", + "tests whether the string operand starts with the prefix argument", + SimpleType.BOOL, + SimpleType.STRING, + SimpleType.STRING)), + ; + private final CelOverloadDecl celOverloadDecl; + + StringMatchers(CelOverloadDecl overloadDecl) { + this.celOverloadDecl = overloadDecl; + } + + @Override + public CelOverloadDecl celOverloadDecl() { + return this.celOverloadDecl; + } + } + + /** Overloads for functions performing date/time operations. */ + public enum DateTime implements StandardOverload { + TIMESTAMP_TO_YEAR( + CelOverloadDecl.newMemberOverload( + "timestamp_to_year", + "get year from the date in UTC", + SimpleType.INT, + SimpleType.TIMESTAMP)), + TIMESTAMP_TO_YEAR_WITH_TZ( + CelOverloadDecl.newMemberOverload( + "timestamp_to_year_with_tz", + "get year from the date with timezone", + SimpleType.INT, + SimpleType.TIMESTAMP, + SimpleType.STRING)), + + TIMESTAMP_TO_MONTH( + CelOverloadDecl.newMemberOverload( + "timestamp_to_month", + "get month from the date in UTC, 0-11", + SimpleType.INT, + SimpleType.TIMESTAMP)), + + TIMESTAMP_TO_MONTH_WITH_TZ( + CelOverloadDecl.newMemberOverload( + "timestamp_to_month_with_tz", + "get month from the date with timezone, 0-11", + SimpleType.INT, + SimpleType.TIMESTAMP, + SimpleType.STRING)), + + TIMESTAMP_TO_DAY_OF_YEAR( + CelOverloadDecl.newMemberOverload( + "timestamp_to_day_of_year", + "get day of year from the date in UTC, zero-based indexing", + SimpleType.INT, + SimpleType.TIMESTAMP)), + + TIMESTAMP_TO_DAY_OF_YEAR_WITH_TZ( + CelOverloadDecl.newMemberOverload( + "timestamp_to_day_of_year_with_tz", + "get day of year from the date with timezone, zero-based indexing", + SimpleType.INT, + SimpleType.TIMESTAMP, + SimpleType.STRING)), + + TIMESTAMP_TO_DAY_OF_MONTH( + CelOverloadDecl.newMemberOverload( + "timestamp_to_day_of_month", + "get day of month from the date in UTC, zero-based indexing", + SimpleType.INT, + SimpleType.TIMESTAMP)), + + TIMESTAMP_TO_DAY_OF_MONTH_WITH_TZ( + CelOverloadDecl.newMemberOverload( + "timestamp_to_day_of_month_with_tz", + "get day of month from the date with timezone, zero-based indexing", + SimpleType.INT, + SimpleType.TIMESTAMP, + SimpleType.STRING)), + + TIMESTAMP_TO_DAY_OF_MONTH_1_BASED( + CelOverloadDecl.newMemberOverload( + "timestamp_to_day_of_month_1_based", + "get day of month from the date in UTC, one-based indexing", + SimpleType.INT, + SimpleType.TIMESTAMP)), + TIMESTAMP_TO_DAY_OF_MONTH_1_BASED_WITH_TZ( + CelOverloadDecl.newMemberOverload( + "timestamp_to_day_of_month_1_based_with_tz", + "get day of month from the date with timezone, one-based indexing", + SimpleType.INT, + SimpleType.TIMESTAMP, + SimpleType.STRING)), + + TIMESTAMP_TO_DAY_OF_WEEK( + CelOverloadDecl.newMemberOverload( + "timestamp_to_day_of_week", + "get day of week from the date in UTC, zero-based, zero for Sunday", + SimpleType.INT, + SimpleType.TIMESTAMP)), + + TIMESTAMP_TO_DAY_OF_WEEK_WITH_TZ( + CelOverloadDecl.newMemberOverload( + "timestamp_to_day_of_week_with_tz", + "get day of week from the date with timezone, zero-based, zero for Sunday", + SimpleType.INT, + SimpleType.TIMESTAMP, + SimpleType.STRING)), + + TIMESTAMP_TO_HOURS( + CelOverloadDecl.newMemberOverload( + "timestamp_to_hours", + "get hours from the date in UTC, 0-23", + SimpleType.INT, + SimpleType.TIMESTAMP)), + + TIMESTAMP_TO_HOURS_WITH_TZ( + CelOverloadDecl.newMemberOverload( + "timestamp_to_hours_with_tz", + "get hours from the date with timezone, 0-23", + SimpleType.INT, + SimpleType.TIMESTAMP, + SimpleType.STRING)), + + DURATION_TO_HOURS( + CelOverloadDecl.newMemberOverload( + "duration_to_hours", + "get hours from duration", + SimpleType.INT, + SimpleType.DURATION)), + TIMESTAMP_TO_MINUTES( + CelOverloadDecl.newMemberOverload( + "timestamp_to_minutes", + "get minutes from the date in UTC, 0-59", + SimpleType.INT, + SimpleType.TIMESTAMP)), + + TIMESTAMP_TO_MINUTES_WITH_TZ( + CelOverloadDecl.newMemberOverload( + "timestamp_to_minutes_with_tz", + "get minutes from the date with timezone, 0-59", + SimpleType.INT, + SimpleType.TIMESTAMP, + SimpleType.STRING)), + + DURATION_TO_MINUTES( + CelOverloadDecl.newMemberOverload( + "duration_to_minutes", + "get minutes from duration", + SimpleType.INT, + SimpleType.DURATION)), + TIMESTAMP_TO_SECONDS( + CelOverloadDecl.newMemberOverload( + "timestamp_to_seconds", + "get seconds from the date in UTC, 0-59", + SimpleType.INT, + SimpleType.TIMESTAMP)), + + TIMESTAMP_TO_SECONDS_WITH_TZ( + CelOverloadDecl.newMemberOverload( + "timestamp_to_seconds_with_tz", + "get seconds from the date with timezone, 0-59", + SimpleType.INT, + SimpleType.TIMESTAMP, + SimpleType.STRING)), + + DURATION_TO_SECONDS( + CelOverloadDecl.newMemberOverload( + "duration_to_seconds", + "get seconds from duration", + SimpleType.INT, + SimpleType.DURATION)), + TIMESTAMP_TO_MILLISECONDS( + CelOverloadDecl.newMemberOverload( + "timestamp_to_milliseconds", + "get milliseconds from the date in UTC, 0-999", + SimpleType.INT, + SimpleType.TIMESTAMP)), + + TIMESTAMP_TO_MILLISECONDS_WITH_TZ( + CelOverloadDecl.newMemberOverload( + "timestamp_to_milliseconds_with_tz", + "get milliseconds from the date with timezone, 0-999", + SimpleType.INT, + SimpleType.TIMESTAMP, + SimpleType.STRING)), + + DURATION_TO_MILLISECONDS( + CelOverloadDecl.newMemberOverload( + "duration_to_milliseconds", + "milliseconds from duration, 0-999", + SimpleType.INT, + SimpleType.DURATION)), + ; + private final CelOverloadDecl celOverloadDecl; + + DateTime(CelOverloadDecl overloadDecl) { + this.celOverloadDecl = overloadDecl; + } + + @Override + public CelOverloadDecl celOverloadDecl() { + return this.celOverloadDecl; + } + } + + /** Overloads for performing numeric comparisons. */ + public enum Comparison implements StandardOverload { + // Less + LESS_BOOL( + CelOverloadDecl.newGlobalOverload( + "less_bool", "ordering", SimpleType.BOOL, SimpleType.BOOL, SimpleType.BOOL), + false), + LESS_INT64( + CelOverloadDecl.newGlobalOverload( + "less_int64", "ordering", SimpleType.BOOL, SimpleType.INT, SimpleType.INT), + false), + LESS_UINT64( + CelOverloadDecl.newGlobalOverload( + "less_uint64", "ordering", SimpleType.BOOL, SimpleType.UINT, SimpleType.UINT), + false), + LESS_DOUBLE( + CelOverloadDecl.newGlobalOverload( + "less_double", "ordering", SimpleType.BOOL, SimpleType.DOUBLE, SimpleType.DOUBLE), + false), + LESS_STRING( + CelOverloadDecl.newGlobalOverload( + "less_string", "ordering", SimpleType.BOOL, SimpleType.STRING, SimpleType.STRING), + false), + LESS_BYTES( + CelOverloadDecl.newGlobalOverload( + "less_bytes", "ordering", SimpleType.BOOL, SimpleType.BYTES, SimpleType.BYTES), + false), + LESS_TIMESTAMP( + CelOverloadDecl.newGlobalOverload( + "less_timestamp", + "ordering", + SimpleType.BOOL, + SimpleType.TIMESTAMP, + SimpleType.TIMESTAMP), + false), + LESS_DURATION( + CelOverloadDecl.newGlobalOverload( + "less_duration", + "ordering", + SimpleType.BOOL, + SimpleType.DURATION, + SimpleType.DURATION), + false), + LESS_INT64_UINT64( + CelOverloadDecl.newGlobalOverload( + "less_int64_uint64", + "Compare a signed integer value to an unsigned integer value", + SimpleType.BOOL, + SimpleType.INT, + SimpleType.UINT), + true), + LESS_UINT64_INT64( + CelOverloadDecl.newGlobalOverload( + "less_uint64_int64", + "Compare an unsigned integer value to a signed integer value", + SimpleType.BOOL, + SimpleType.UINT, + SimpleType.INT), + true), + LESS_INT64_DOUBLE( + CelOverloadDecl.newGlobalOverload( + "less_int64_double", + "Compare a signed integer value to a double value, coalesces the integer to a" + + " double", + SimpleType.BOOL, + SimpleType.INT, + SimpleType.DOUBLE), + true), + LESS_DOUBLE_INT64( + CelOverloadDecl.newGlobalOverload( + "less_double_int64", + "Compare a double value to a signed integer value, coalesces the integer to a" + + " double", + SimpleType.BOOL, + SimpleType.DOUBLE, + SimpleType.INT), + true), + LESS_UINT64_DOUBLE( + CelOverloadDecl.newGlobalOverload( + "less_uint64_double", + "Compare an unsigned integer value to a double value, coalesces the unsigned" + + " integer to a double", + SimpleType.BOOL, + SimpleType.UINT, + SimpleType.DOUBLE), + true), + LESS_DOUBLE_UINT64( + CelOverloadDecl.newGlobalOverload( + "less_double_uint64", + "Compare a double value to an unsigned integer value, coalesces the unsigned" + + " integer to a double", + SimpleType.BOOL, + SimpleType.DOUBLE, + SimpleType.UINT), + true), + // Less Equals + LESS_EQUALS_BOOL( + Comparison.LESS_BOOL.celOverloadDecl.toBuilder() + .setOverloadId( + Comparison.LESS_BOOL + .celOverloadDecl + .overloadId() + .replace("less", "less_equals")) + .build(), + false), + LESS_EQUALS_INT64( + Comparison.LESS_INT64.celOverloadDecl.toBuilder() + .setOverloadId( + Comparison.LESS_INT64 + .celOverloadDecl + .overloadId() + .replace("less", "less_equals")) + .build(), + false), + LESS_EQUALS_UINT64( + Comparison.LESS_UINT64.celOverloadDecl.toBuilder() + .setOverloadId( + Comparison.LESS_UINT64 + .celOverloadDecl + .overloadId() + .replace("less", "less_equals")) + .build(), + false), + LESS_EQUALS_DOUBLE( + Comparison.LESS_DOUBLE.celOverloadDecl.toBuilder() + .setOverloadId( + Comparison.LESS_DOUBLE + .celOverloadDecl + .overloadId() + .replace("less", "less_equals")) + .build(), + false), + LESS_EQUALS_STRING( + Comparison.LESS_STRING.celOverloadDecl.toBuilder() + .setOverloadId( + Comparison.LESS_STRING + .celOverloadDecl + .overloadId() + .replace("less", "less_equals")) + .build(), + false), + LESS_EQUALS_BYTES( + Comparison.LESS_BYTES.celOverloadDecl.toBuilder() + .setOverloadId( + Comparison.LESS_BYTES + .celOverloadDecl + .overloadId() + .replace("less", "less_equals")) + .build(), + false), + LESS_EQUALS_TIMESTAMP( + Comparison.LESS_TIMESTAMP.celOverloadDecl.toBuilder() + .setOverloadId( + Comparison.LESS_TIMESTAMP + .celOverloadDecl + .overloadId() + .replace("less", "less_equals")) + .build(), + false), + LESS_EQUALS_DURATION( + Comparison.LESS_DURATION.celOverloadDecl.toBuilder() + .setOverloadId( + Comparison.LESS_DURATION + .celOverloadDecl + .overloadId() + .replace("less", "less_equals")) + .build(), + false), + LESS_EQUALS_INT64_UINT64( + Comparison.LESS_INT64_UINT64.celOverloadDecl.toBuilder() + .setOverloadId( + Comparison.LESS_INT64_UINT64 + .celOverloadDecl + .overloadId() + .replace("less", "less_equals")) + .build(), + true), + LESS_EQUALS_UINT64_INT64( + Comparison.LESS_UINT64_INT64.celOverloadDecl.toBuilder() + .setOverloadId( + Comparison.LESS_UINT64_INT64 + .celOverloadDecl + .overloadId() + .replace("less", "less_equals")) + .build(), + true), + LESS_EQUALS_INT64_DOUBLE( + Comparison.LESS_INT64_DOUBLE.celOverloadDecl.toBuilder() + .setOverloadId( + Comparison.LESS_INT64_DOUBLE + .celOverloadDecl + .overloadId() + .replace("less", "less_equals")) + .build(), + true), + LESS_EQUALS_DOUBLE_INT64( + Comparison.LESS_DOUBLE_INT64.celOverloadDecl.toBuilder() + .setOverloadId( + Comparison.LESS_DOUBLE_INT64 + .celOverloadDecl + .overloadId() + .replace("less", "less_equals")) + .build(), + true), + LESS_EQUALS_UINT64_DOUBLE( + Comparison.LESS_UINT64_DOUBLE.celOverloadDecl.toBuilder() + .setOverloadId( + Comparison.LESS_UINT64_DOUBLE + .celOverloadDecl + .overloadId() + .replace("less", "less_equals")) + .build(), + true), + LESS_EQUALS_DOUBLE_UINT64( + Comparison.LESS_DOUBLE_UINT64.celOverloadDecl.toBuilder() + .setOverloadId( + Comparison.LESS_DOUBLE_UINT64 + .celOverloadDecl + .overloadId() + .replace("less", "less_equals")) + .build(), + true), + + // Greater + GREATER_BOOL( + CelOverloadDecl.newGlobalOverload( + "greater_bool", "ordering", SimpleType.BOOL, SimpleType.BOOL, SimpleType.BOOL), + false), + GREATER_INT64( + CelOverloadDecl.newGlobalOverload( + "greater_int64", "ordering", SimpleType.BOOL, SimpleType.INT, SimpleType.INT), + false), + GREATER_UINT64( + CelOverloadDecl.newGlobalOverload( + "greater_uint64", "ordering", SimpleType.BOOL, SimpleType.UINT, SimpleType.UINT), + false), + GREATER_DOUBLE( + CelOverloadDecl.newGlobalOverload( + "greater_double", + "ordering", + SimpleType.BOOL, + SimpleType.DOUBLE, + SimpleType.DOUBLE), + false), + GREATER_STRING( + CelOverloadDecl.newGlobalOverload( + "greater_string", + "ordering", + SimpleType.BOOL, + SimpleType.STRING, + SimpleType.STRING), + false), + GREATER_BYTES( + CelOverloadDecl.newGlobalOverload( + "greater_bytes", "ordering", SimpleType.BOOL, SimpleType.BYTES, SimpleType.BYTES), + false), + GREATER_TIMESTAMP( + CelOverloadDecl.newGlobalOverload( + "greater_timestamp", + "ordering", + SimpleType.BOOL, + SimpleType.TIMESTAMP, + SimpleType.TIMESTAMP), + false), + GREATER_DURATION( + CelOverloadDecl.newGlobalOverload( + "greater_duration", + "ordering", + SimpleType.BOOL, + SimpleType.DURATION, + SimpleType.DURATION), + false), + GREATER_INT64_UINT64( + CelOverloadDecl.newGlobalOverload( + "greater_int64_uint64", + "Compare a signed integer value to an unsigned integer value", + SimpleType.BOOL, + SimpleType.INT, + SimpleType.UINT), + true), + GREATER_UINT64_INT64( + CelOverloadDecl.newGlobalOverload( + "greater_uint64_int64", + "Compare an unsigned integer value to a signed integer value", + SimpleType.BOOL, + SimpleType.UINT, + SimpleType.INT), + true), + GREATER_INT64_DOUBLE( + CelOverloadDecl.newGlobalOverload( + "greater_int64_double", + "Compare a signed integer value to a double value, coalesces the integer to a" + + " double", + SimpleType.BOOL, + SimpleType.INT, + SimpleType.DOUBLE), + true), + GREATER_DOUBLE_INT64( + CelOverloadDecl.newGlobalOverload( + "greater_double_int64", + "Compare a double value to a signed integer value, coalesces the integer to a" + + " double", + SimpleType.BOOL, + SimpleType.DOUBLE, + SimpleType.INT), + true), + GREATER_UINT64_DOUBLE( + CelOverloadDecl.newGlobalOverload( + "greater_uint64_double", + "Compare an unsigned integer value to a double value, coalesces the unsigned" + + " integer to a double", + SimpleType.BOOL, + SimpleType.UINT, + SimpleType.DOUBLE), + true), + GREATER_DOUBLE_UINT64( + CelOverloadDecl.newGlobalOverload( + "greater_double_uint64", + "Compare a double value to an unsigned integer value, coalesces the unsigned" + + " integer to a double", + SimpleType.BOOL, + SimpleType.DOUBLE, + SimpleType.UINT), + true), + + // Greater Equals + GREATER_EQUALS_BOOL( + Comparison.LESS_BOOL.celOverloadDecl.toBuilder() + .setOverloadId( + Comparison.LESS_BOOL + .celOverloadDecl + .overloadId() + .replace("less", "greater_equals")) + .build(), + false), + GREATER_EQUALS_INT64( + Comparison.LESS_INT64.celOverloadDecl.toBuilder() + .setOverloadId( + Comparison.LESS_INT64 + .celOverloadDecl + .overloadId() + .replace("less", "greater_equals")) + .build(), + false), + GREATER_EQUALS_UINT64( + Comparison.LESS_UINT64.celOverloadDecl.toBuilder() + .setOverloadId( + Comparison.LESS_UINT64 + .celOverloadDecl + .overloadId() + .replace("less", "greater_equals")) + .build(), + false), + GREATER_EQUALS_DOUBLE( + Comparison.LESS_DOUBLE.celOverloadDecl.toBuilder() + .setOverloadId( + Comparison.LESS_DOUBLE + .celOverloadDecl + .overloadId() + .replace("less", "greater_equals")) + .build(), + false), + GREATER_EQUALS_STRING( + Comparison.LESS_STRING.celOverloadDecl.toBuilder() + .setOverloadId( + Comparison.LESS_STRING + .celOverloadDecl + .overloadId() + .replace("less", "greater_equals")) + .build(), + false), + GREATER_EQUALS_BYTES( + Comparison.LESS_BYTES.celOverloadDecl.toBuilder() + .setOverloadId( + Comparison.LESS_BYTES + .celOverloadDecl + .overloadId() + .replace("less", "greater_equals")) + .build(), + false), + GREATER_EQUALS_TIMESTAMP( + Comparison.LESS_TIMESTAMP.celOverloadDecl.toBuilder() + .setOverloadId( + Comparison.LESS_TIMESTAMP + .celOverloadDecl + .overloadId() + .replace("less", "greater_equals")) + .build(), + false), + GREATER_EQUALS_DURATION( + Comparison.LESS_DURATION.celOverloadDecl.toBuilder() + .setOverloadId( + Comparison.LESS_DURATION + .celOverloadDecl + .overloadId() + .replace("less", "greater_equals")) + .build(), + false), + GREATER_EQUALS_INT64_UINT64( + Comparison.LESS_INT64_UINT64.celOverloadDecl.toBuilder() + .setOverloadId( + Comparison.LESS_INT64_UINT64 + .celOverloadDecl + .overloadId() + .replace("less", "greater_equals")) + .build(), + true), + GREATER_EQUALS_UINT64_INT64( + Comparison.LESS_UINT64_INT64.celOverloadDecl.toBuilder() + .setOverloadId( + Comparison.LESS_UINT64_INT64 + .celOverloadDecl + .overloadId() + .replace("less", "greater_equals")) + .build(), + true), + GREATER_EQUALS_INT64_DOUBLE( + Comparison.LESS_INT64_DOUBLE.celOverloadDecl.toBuilder() + .setOverloadId( + Comparison.LESS_INT64_DOUBLE + .celOverloadDecl + .overloadId() + .replace("less", "greater_equals")) + .build(), + true), + GREATER_EQUALS_DOUBLE_INT64( + Comparison.LESS_DOUBLE_INT64.celOverloadDecl.toBuilder() + .setOverloadId( + Comparison.LESS_DOUBLE_INT64 + .celOverloadDecl + .overloadId() + .replace("less", "greater_equals")) + .build(), + true), + GREATER_EQUALS_UINT64_DOUBLE( + Comparison.LESS_UINT64_DOUBLE.celOverloadDecl.toBuilder() + .setOverloadId( + Comparison.LESS_UINT64_DOUBLE + .celOverloadDecl + .overloadId() + .replace("less", "greater_equals")) + .build(), + true), + GREATER_EQUALS_DOUBLE_UINT64( + Comparison.LESS_DOUBLE_UINT64.celOverloadDecl.toBuilder() + .setOverloadId( + Comparison.LESS_DOUBLE_UINT64 + .celOverloadDecl + .overloadId() + .replace("less", "greater_equals")) + .build(), + true), + ; + + private final CelOverloadDecl celOverloadDecl; + private final boolean isHeterogeneousComparison; + + Comparison(CelOverloadDecl overloadDecl, boolean isHeterogeneousComparison) { + this.celOverloadDecl = overloadDecl; + this.isHeterogeneousComparison = isHeterogeneousComparison; + } + + public boolean isHeterogeneousComparison() { + return isHeterogeneousComparison; + } + + @Override + 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() {} + } + + /** Gets the declaration for this standard function. */ + private CelFunctionDecl withOverloads(Iterable overloads) { + return newCelFunctionDecl(functionName, ImmutableSet.copyOf(overloads)); + } + + public CelFunctionDecl functionDecl() { + return celFunctionDecl; + } + + public String functionName() { + return functionName; + } + + boolean isDeprecated() { + return isDeprecated; + } + + StandardFunction(Operator operator, StandardOverload... overloads) { + this(false, operator.getFunction(), overloads); + } + + StandardFunction(String functionName, StandardOverload... overloads) { + this(false, functionName, overloads); + } + + StandardFunction(boolean isDeprecated, String functionName, StandardOverload... overloads) { + this.isDeprecated = isDeprecated; + this.functionName = functionName; + this.standardOverloads = ImmutableSet.copyOf(overloads); + this.celFunctionDecl = newCelFunctionDecl(functionName, this.standardOverloads); + } + + private static CelFunctionDecl newCelFunctionDecl( + String functionName, ImmutableSet overloads) { + return CelFunctionDecl.newFunctionDeclaration( + functionName, + overloads.stream().map(StandardOverload::celOverloadDecl).collect(toImmutableSet())); + } + } + + /** Standard CEL identifiers. */ + public enum StandardIdentifier { + INT(newStandardIdentDecl(SimpleType.INT)), + UINT(newStandardIdentDecl(SimpleType.UINT)), + BOOL(newStandardIdentDecl(SimpleType.BOOL)), + DOUBLE(newStandardIdentDecl(SimpleType.DOUBLE)), + BYTES(newStandardIdentDecl(SimpleType.BYTES)), + STRING(newStandardIdentDecl(SimpleType.STRING)), + DYN(newStandardIdentDecl(SimpleType.DYN)), + TYPE(newStandardIdentDecl("type", SimpleType.DYN)), + NULL_TYPE(newStandardIdentDecl("null_type", SimpleType.NULL_TYPE)), + LIST(newStandardIdentDecl("list", ListType.create(SimpleType.DYN))), + MAP(newStandardIdentDecl("map", MapType.create(SimpleType.DYN, SimpleType.DYN))), + ; + + private static CelIdentDecl newStandardIdentDecl(CelType celType) { + return newStandardIdentDecl(CelTypes.format(celType), celType); + } + + private static CelIdentDecl newStandardIdentDecl(String identName, CelType celType) { + return CelIdentDecl.newBuilder() + .setName(identName) + .setType(TypeType.create(celType)) + .setDoc("type denotation") + .build(); + } + + private final CelIdentDecl identDecl; + + public CelIdentDecl identDecl() { + return identDecl; + } + + StandardIdentifier(CelIdentDecl identDecl) { + this.identDecl = identDecl; + } + } + + /** General interface for defining a standard function overload. */ + @Immutable + public interface StandardOverload { + CelOverloadDecl celOverloadDecl(); + } + + /** Set of all standard function names. */ + public static ImmutableSet getAllFunctionNames() { + return stream(StandardFunction.values()) + .filter(f -> !f.isDeprecated) + .map(f -> f.functionName) + .collect(toImmutableSet()); + } + + /** Builder for constructing the set of standard function/identifiers. */ + public static final class Builder { + + private ImmutableSet includeFunctions; + private ImmutableSet excludeFunctions; + private FunctionFilter functionFilter; + + private ImmutableSet includeIdentifiers; + private ImmutableSet excludeIdentifiers; + private IdentifierFilter identifierFilter; + + @CanIgnoreReturnValue + public Builder excludeFunctions(StandardFunction... functions) { + return excludeFunctions(ImmutableSet.copyOf(functions)); + } + + @CanIgnoreReturnValue + public Builder excludeFunctions(Iterable functions) { + this.excludeFunctions = checkNotNull(ImmutableSet.copyOf(functions)); + return this; + } + + @CanIgnoreReturnValue + public Builder includeFunctions(StandardFunction... functions) { + return includeFunctions(ImmutableSet.copyOf(functions)); + } + + @CanIgnoreReturnValue + public Builder includeFunctions(Iterable functions) { + this.includeFunctions = checkNotNull(ImmutableSet.copyOf(functions)); + return this; + } + + @CanIgnoreReturnValue + public Builder filterFunctions(FunctionFilter functionFilter) { + this.functionFilter = functionFilter; + return this; + } + + @CanIgnoreReturnValue + public Builder excludeIdentifiers(StandardIdentifier... identifiers) { + return excludeIdentifiers(ImmutableSet.copyOf(identifiers)); + } + + @CanIgnoreReturnValue + public Builder excludeIdentifiers(Iterable identifiers) { + this.excludeIdentifiers = checkNotNull(ImmutableSet.copyOf(identifiers)); + return this; + } + + @CanIgnoreReturnValue + public Builder includeIdentifiers(StandardIdentifier... identifiers) { + return includeIdentifiers(ImmutableSet.copyOf(identifiers)); + } + + @CanIgnoreReturnValue + public Builder includeIdentifiers(Iterable identifiers) { + this.includeIdentifiers = checkNotNull(ImmutableSet.copyOf(identifiers)); + return this; + } + + @CanIgnoreReturnValue + public Builder filterIdentifiers(IdentifierFilter identifierFilter) { + this.identifierFilter = identifierFilter; + return this; + } + + private static void assertOneSettingIsSet( + boolean a, boolean b, boolean c, String errorMessage) { + int count = 0; + if (a) { + count++; + } + if (b) { + count++; + } + if (c) { + count++; + } + + if (count > 1) { + throw new IllegalArgumentException(errorMessage); + } + } + + public CelStandardDeclarations build() { + boolean hasIncludeFunctions = !this.includeFunctions.isEmpty(); + boolean hasExcludeFunctions = !this.excludeFunctions.isEmpty(); + boolean hasFilterFunction = this.functionFilter != null; + assertOneSettingIsSet( + hasIncludeFunctions, + hasExcludeFunctions, + hasFilterFunction, + "You may only populate one of the following builder methods: includeFunctions," + + " excludeFunctions or filterFunctions"); + boolean hasIncludeIdentifiers = !this.includeIdentifiers.isEmpty(); + boolean hasExcludeIdentifiers = !this.excludeIdentifiers.isEmpty(); + boolean hasIdentifierFilter = this.identifierFilter != null; + assertOneSettingIsSet( + hasIncludeIdentifiers, + hasExcludeIdentifiers, + hasIdentifierFilter, + "You may only populate one of the following builder methods: includeIdentifiers," + + " excludeIdentifiers or filterIdentifiers"); + + ImmutableSet.Builder functionDeclBuilder = ImmutableSet.builder(); + for (StandardFunction standardFunction : StandardFunction.values()) { + if (hasIncludeFunctions) { + if (this.includeFunctions.contains(standardFunction)) { + functionDeclBuilder.add(standardFunction.celFunctionDecl); + } + continue; + } + if (hasExcludeFunctions) { + if (!this.excludeFunctions.contains(standardFunction)) { + functionDeclBuilder.add(standardFunction.celFunctionDecl); + } + continue; + } + if (hasFilterFunction) { + ImmutableSet.Builder overloadBuilder = ImmutableSet.builder(); + for (StandardOverload standardOverload : standardFunction.standardOverloads) { + boolean includeOverload = functionFilter.include(standardFunction, standardOverload); + if (includeOverload) { + overloadBuilder.add(standardOverload); + } + } + + ImmutableSet overloads = overloadBuilder.build(); + if (!overloads.isEmpty()) { + functionDeclBuilder.add(standardFunction.withOverloads(overloadBuilder.build())); + } + continue; + } + + functionDeclBuilder.add(standardFunction.celFunctionDecl); + } + + ImmutableSet.Builder identBuilder = ImmutableSet.builder(); + for (StandardIdentifier standardIdentifier : StandardIdentifier.values()) { + if (hasIncludeIdentifiers) { + if (this.includeIdentifiers.contains(standardIdentifier)) { + identBuilder.add(standardIdentifier.identDecl); + } + continue; + } + + if (hasExcludeIdentifiers) { + if (!this.excludeIdentifiers.contains(standardIdentifier)) { + identBuilder.add(standardIdentifier.identDecl); + } + continue; + } + + if (hasIdentifierFilter) { + boolean includeIdent = identifierFilter.include(standardIdentifier); + if (includeIdent) { + identBuilder.add(standardIdentifier.identDecl); + } + continue; + } + + identBuilder.add(standardIdentifier.identDecl); + } + + return new CelStandardDeclarations(functionDeclBuilder.build(), identBuilder.build()); + } + + private Builder() { + this.includeFunctions = ImmutableSet.of(); + this.excludeFunctions = ImmutableSet.of(); + this.includeIdentifiers = ImmutableSet.of(); + this.excludeIdentifiers = ImmutableSet.of(); + } + + /** + * Functional interface for filtering standard functions. Returning true in the callback will + * include the function in the environment. + */ + @FunctionalInterface + public interface FunctionFilter { + boolean include(StandardFunction standardFunction, StandardOverload standardOverload); + } + + /** + * Functional interface for filtering standard identifiers. Returning true in the callback will + * include the identifier in the environment. + */ + @FunctionalInterface + public interface IdentifierFilter { + boolean include(StandardIdentifier standardIdentifier); + } + } + + public static Builder newBuilder() { + return new Builder(); + } + + ImmutableSet functionDecls() { + return celFunctionDecls; + } + + ImmutableSet identifierDecls() { + return celIdentDecls; + } + + private CelStandardDeclarations( + ImmutableSet celFunctionDecls, ImmutableSet celIdentDecls) { + this.celFunctionDecls = celFunctionDecls; + this.celIdentDecls = celIdentDecls; + } +} diff --git a/checker/src/main/java/dev/cel/checker/DescriptorTypeProvider.java b/checker/src/main/java/dev/cel/checker/DescriptorTypeProvider.java index e2fe17351..b5f849d50 100644 --- a/checker/src/main/java/dev/cel/checker/DescriptorTypeProvider.java +++ b/checker/src/main/java/dev/cel/checker/DescriptorTypeProvider.java @@ -41,7 +41,7 @@ import java.util.List; import java.util.Map; import java.util.Set; -import org.jspecify.nullness.Nullable; +import org.jspecify.annotations.Nullable; /** * The {@code DescriptorTypeProvider} provides type information for one or more {@link Descriptor} diff --git a/checker/src/main/java/dev/cel/checker/Env.java b/checker/src/main/java/dev/cel/checker/Env.java index 8ce80f488..75ed7cf1f 100644 --- a/checker/src/main/java/dev/cel/checker/Env.java +++ b/checker/src/main/java/dev/cel/checker/Env.java @@ -19,6 +19,7 @@ import dev.cel.expr.Decl.FunctionDecl.Overload; import dev.cel.expr.Expr; import dev.cel.expr.Type; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -27,17 +28,21 @@ import com.google.common.collect.Lists; import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.CheckReturnValue; +import dev.cel.checker.CelStandardDeclarations.StandardFunction.Overload.Comparison; +import dev.cel.checker.CelStandardDeclarations.StandardFunction.Overload.Conversions; +import dev.cel.common.CelContainer; import dev.cel.common.CelFunctionDecl; import dev.cel.common.CelOptions; import dev.cel.common.CelOverloadDecl; -import dev.cel.common.ExprFeatures; import dev.cel.common.annotations.Internal; import dev.cel.common.ast.CelConstant; import dev.cel.common.ast.CelExpr; import dev.cel.common.ast.CelExprConverter; +import dev.cel.common.ast.CelMutableExpr; import dev.cel.common.ast.CelReference; import dev.cel.common.internal.Errors; 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.common.types.SimpleType; @@ -48,7 +53,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import org.jspecify.nullness.Nullable; +import org.jspecify.annotations.Nullable; /** * Environment used during checking of expressions. Provides name resolution and error reporting. @@ -94,6 +99,12 @@ public class Env { /** CEL Feature flags. */ private final CelOptions celOptions; + private static final CelOptions LEGACY_TYPE_CHECKER_OPTIONS = + CelOptions.newBuilder() + .disableCelStandardEquality(false) + .enableNamespacedDeclarations(false) + .build(); + private Env( Errors errors, TypeProvider typeProvider, DeclGroup declGroup, CelOptions celOptions) { this.celOptions = celOptions; @@ -103,125 +114,112 @@ private Env( } /** - * Creates an unconfigured {@code Env} value without the standard CEL types, functions, and - * operators with a reference to the feature flags enabled in the environment. - * - * @deprecated use {@code unconfigured} with {@code CelOptions} instead. - */ - @Deprecated - public static Env unconfigured(Errors errors, ExprFeatures... exprFeatures) { - return unconfigured(errors, new DescriptorTypeProvider(), ImmutableSet.copyOf(exprFeatures)); - } - - /** - * Creates an unconfigured {@code Env} value without the standard CEL types, functions, and - * operators using a custom {@code typeProvider}. - * - * @deprecated use {@code unconfigured} with {@code CelOptions} instead. + * @deprecated Do not use. This exists for compatibility reasons. Migrate to CEL-Java fluent APIs. + * See {@code CelCompilerFactory}. */ @Deprecated - public static Env unconfigured( - Errors errors, TypeProvider typeProvider, ExprFeatures... exprFeatures) { - return unconfigured(errors, typeProvider, ImmutableSet.copyOf(exprFeatures)); - } - - /** - * Creates an unconfigured {@code Env} value without the standard CEL types, functions, and - * operators using a custom {@code typeProvider}. The set of enabled {@code exprFeatures} is also - * provided. - * - * @deprecated use {@code unconfigured} with {@code CelOptions} instead. - */ - @Deprecated - public static Env unconfigured( - Errors errors, TypeProvider typeProvider, ImmutableSet exprFeatures) { - return unconfigured(errors, typeProvider, CelOptions.fromExprFeatures(exprFeatures)); + public static Env unconfigured(Errors errors) { + return unconfigured(errors, LEGACY_TYPE_CHECKER_OPTIONS); } /** * Creates an unconfigured {@code Env} value without the standard CEL types, functions, and * operators with a reference to the configured {@code celOptions}. */ - public static Env unconfigured(Errors errors, CelOptions celOptions) { + @VisibleForTesting + static Env unconfigured(Errors errors, CelOptions celOptions) { return unconfigured(errors, new DescriptorTypeProvider(), celOptions); } /** * Creates an unconfigured {@code Env} value without the standard CEL types, functions, and - * operators using a custom {@code typeProvider}. The {@code CelOptions} are provided as well. + * operators using a custom {@code typeProvider}. + * + * @deprecated Do not use. This exists for compatibility reasons. Migrate to CEL-Java fluent APIs. + * See {@code CelCompilerFactory}. */ + @Deprecated public static Env unconfigured(Errors errors, TypeProvider typeProvider, CelOptions celOptions) { return new Env(errors, typeProvider, new DeclGroup(), celOptions); } /** - * Creates an {@code Env} value configured with the standard types, functions, and operators with - * a reference to the set of {@code exprFeatures} enabled in the environment. - * - *

Note: standard declarations are configured in an isolated scope, and may be shadowed by - * subsequent declarations. - * - * @deprecated use {@code standard} with {@code CelOptions} instead. + * @deprecated Do not use. This exists for compatibility reasons. Migrate to CEL-Java fluent APIs. + * See {@code CelCompilerFactory}. */ @Deprecated - public static Env standard(Errors errors, ExprFeatures... exprFeatures) { - return standard(errors, new DescriptorTypeProvider(), exprFeatures); + public static Env standard(Errors errors) { + return standard(errors, new DescriptorTypeProvider()); } /** - * Creates an {@code Env} value configured with the standard types, functions, and operators, - * configured with a custom {@code typeProvider}. - * - *

Note: standard declarations are configured in an isolated scope, and may be shadowed by - * subsequent declarations with the same signature. - * - * @deprecated use {@code standard} with {@code CelOptions} instead. + * @deprecated Do not use. This exists for compatibility reasons. Migrate to CEL-Java fluent APIs. + * See {@code CelCompilerFactory}. */ @Deprecated - public static Env standard( - Errors errors, TypeProvider typeProvider, ExprFeatures... exprFeatures) { - return standard(errors, typeProvider, ImmutableSet.copyOf(exprFeatures)); + public static Env standard(Errors errors, TypeProvider typeProvider) { + return standard(errors, typeProvider, LEGACY_TYPE_CHECKER_OPTIONS); } /** * Creates an {@code Env} value configured with the standard types, functions, and operators, - * configured with a custom {@code typeProvider} and a reference to the set of {@code - * exprFeatures} enabled in the environment. + * configured with a custom {@code typeProvider} and a reference to the {@code celOptions} to use + * within the environment. * *

Note: standard declarations are configured in an isolated scope, and may be shadowed by * subsequent declarations with the same signature. * - * @deprecated use {@code standard} with {@code CelOptions} instead. + * @deprecated Do not use. This exists for compatibility reasons. Migrate to CEL-Java fluent APIs. + * See {@code CelCompilerFactory}. */ @Deprecated - public static Env standard( - Errors errors, TypeProvider typeProvider, ImmutableSet exprFeatures) { - return standard(errors, typeProvider, CelOptions.fromExprFeatures(exprFeatures)); - } - - /** - * Creates an {@code Env} value configured with the standard types, functions, and operators and a - * reference to the configured {@code celOptions}. - * - *

Note: standard declarations are configured in an isolated scope, and may be shadowed by - * subsequent declarations with the same signature. - */ - public static Env standard(Errors errors, CelOptions celOptions) { - return standard(errors, new DescriptorTypeProvider(), celOptions); + public static Env standard(Errors errors, TypeProvider typeProvider, CelOptions celOptions) { + CelStandardDeclarations celStandardDeclaration = + CelStandardDeclarations.newBuilder() + .filterFunctions( + (function, overload) -> { + switch (function) { + case INT: + if (!celOptions.enableUnsignedLongs() + && overload.equals(Conversions.INT64_TO_INT64)) { + return false; + } + break; + case TIMESTAMP: + // TODO: Remove this flag guard once the feature has been + // auto-enabled. + if (!celOptions.enableTimestampEpoch() + && overload.equals(Conversions.INT64_TO_TIMESTAMP)) { + return false; + } + break; + default: + if (!celOptions.enableHeterogeneousNumericComparisons() + && overload instanceof Comparison) { + Comparison comparison = (Comparison) overload; + if (comparison.isHeterogeneousComparison()) { + return false; + } + } + break; + } + return true; + }) + .build(); + + return standard(celStandardDeclaration, errors, typeProvider, celOptions); } - /** - * Creates an {@code Env} value configured with the standard types, functions, and operators, - * configured with a custom {@code typeProvider} and a reference to the {@code celOptions} to use - * within the environment. - * - *

Note: standard declarations are configured in an isolated scope, and may be shadowed by - * subsequent declarations with the same signature. - */ - public static Env standard(Errors errors, TypeProvider typeProvider, CelOptions celOptions) { + public static Env standard( + CelStandardDeclarations celStandardDeclaration, + Errors errors, + TypeProvider typeProvider, + CelOptions celOptions) { Env env = Env.unconfigured(errors, typeProvider, celOptions); // Isolate the standard declarations into their own scope for forward compatibility. - Standard.add(env); + celStandardDeclaration.functionDecls().forEach(env::add); + celStandardDeclaration.identifierDecls().forEach(env::add); + env.enterScope(); return env; } @@ -291,28 +289,31 @@ public Map getTypeMap() { * Returns the type associated with an expression by expression id. It's an error to call this * method if the type is not present. * - * @deprecated Use {@link #getType(CelExpr)} instead. + * @deprecated Do not use. Migrate to CEL-Java fluent APIs. */ @Deprecated public Type getType(Expr expr) { Preconditions.checkNotNull(expr); - return CelTypes.celTypeToType(getType(CelExprConverter.fromExpr(expr))); + CelExpr celExpr = CelExprConverter.fromExpr(expr); + CelType celType = + Preconditions.checkNotNull(typeMap.get(celExpr.id()), "expression has no type"); + return CelProtoTypes.celTypeToType(celType); } /** - * Returns the type associated with an expression by expression id. It's an error to call this - * method if the type is not present. + * Returns the type associated with a mutable expression by expression id. It's an error to call + * this method if the type is not present. */ - public CelType getType(CelExpr expr) { + CelType getType(CelMutableExpr expr) { return Preconditions.checkNotNull(typeMap.get(expr.id()), "expression has no type"); } /** - * Sets the type associated with an expression by id. It's an error if the type is already set and - * is different than the provided one. Returns the expression parameter. + * Sets the type associated with a mutable expression by id. It's an error if the type is already + * set and is different than the provided one. Returns the expression parameter. */ @CanIgnoreReturnValue - public CelExpr setType(CelExpr expr, CelType type) { + CelMutableExpr setType(CelMutableExpr expr, CelType type) { CelType oldType = typeMap.put(expr.id(), type); Preconditions.checkState( oldType == null || oldType.equals(type), @@ -323,10 +324,10 @@ public CelExpr setType(CelExpr expr, CelType type) { } /** - * Sets the reference associated with an expression. It's an error if the reference is already set - * and is different. + * Sets the reference associated with a mutable expression. It's an error if the reference is + * already set and is different. */ - public void setRef(CelExpr expr, CelReference reference) { + void setRef(CelMutableExpr expr, CelReference reference) { CelReference oldReference = referenceMap.put(expr.id(), reference); Preconditions.checkState( oldReference == null || oldReference.equals(reference), @@ -349,7 +350,7 @@ public Env add(Decl decl) { CelIdentDecl.Builder identBuilder = CelIdentDecl.newBuilder() .setName(decl.getName()) - .setType(CelTypes.typeToCelType(decl.getIdent().getType())) + .setType(CelProtoTypes.typeToCelType(decl.getIdent().getType())) // Note: Setting doc and constant value exists for compatibility reason. This should // not be set by the users. .setDoc(decl.getIdent().getDoc()); @@ -394,15 +395,17 @@ public Env add(CelIdentDecl celIdentDecl) { @CanIgnoreReturnValue @Deprecated public Env add(String name, Type type) { - return add(CelIdentDecl.newIdentDeclaration(name, CelTypes.typeToCelType(type))); + return add(CelIdentDecl.newIdentDeclaration(name, CelProtoTypes.typeToCelType(type))); } /** + * Note: Used by legacy type-checker users + * * @deprecated Use {@link #tryLookupCelFunction} instead. */ @Deprecated public @Nullable Decl tryLookupFunction(String container, String name) { - CelFunctionDecl decl = tryLookupCelFunction(container, name); + CelFunctionDecl decl = tryLookupCelFunction(CelContainer.ofName(container), name); if (decl == null) { return null; } @@ -420,8 +423,8 @@ public Env add(String name, Type type) { * *

Returns {@code null} if the function cannot be found. */ - public @Nullable CelFunctionDecl tryLookupCelFunction(String container, String name) { - for (String cand : qualifiedTypeNameCandidates(container, name)) { + public @Nullable CelFunctionDecl tryLookupCelFunction(CelContainer container, String name) { + for (String cand : container.resolveCandidateNames(name)) { // First determine whether we know this name already. CelFunctionDecl decl = findFunctionDecl(cand); if (decl != null) { @@ -435,7 +438,7 @@ public Env add(String name, Type type) { * @deprecated Use {@link #tryLookupCelIdent} instead. */ @Deprecated - public @Nullable Decl tryLookupIdent(String container, String name) { + public @Nullable Decl tryLookupIdent(CelContainer container, String name) { CelIdentDecl decl = tryLookupCelIdent(container, name); if (decl == null) { return null; @@ -452,38 +455,87 @@ public Env add(String name, Type type) { * until the root package is reached. If {@code container} starts with {@code .}, the resolution * is in the root container only. * - *

Returns {@code null} if the function cannot be found. + *

Returns {@code null} if the ident cannot be found. */ - public @Nullable CelIdentDecl tryLookupCelIdent(String container, String name) { - for (String cand : qualifiedTypeNameCandidates(container, name)) { - // First determine whether we know this name already. - CelIdentDecl decl = findIdentDecl(cand); + public @Nullable CelIdentDecl tryLookupCelIdent(CelContainer container, String name) { + // A name with a leading '.' always resolves in the root scope, bypassing local scopes. + if (!name.startsWith(".")) { + // Check if this is a qualified ident, or a field selection. + String simpleName = name; + int dotIndex = name.indexOf('.'); + if (dotIndex > 0) { + simpleName = name.substring(0, dotIndex); + } + + // Attempt to find the decl with just the ident name to account for shadowed variables. + CelIdentDecl decl = tryLookupCelIdentFromLocalScopes(simpleName); if (decl != null) { - return decl; + // Appears to be a field selection on a local. + // Return null instead of attempting to resolve qualified name at the root scope + return dotIndex > 0 ? null : decl; } + } - // Next try to import the name as a reference to a message type. - // This is done via the type provider. - Optional type = typeProvider.lookupCelType(cand); - if (type.isPresent()) { - decl = CelIdentDecl.newIdentDeclaration(cand, type.get()); - decls.get(0).putIdent(decl); + for (String cand : container.resolveCandidateNames(name)) { + CelIdentDecl decl = tryLookupCelIdent(cand); + if (decl != null) { return decl; } + } - // Next try to import this as an enum value by splitting the name in a type prefix and - // the enum inside. - Integer enumValue = typeProvider.lookupEnumValue(cand); - if (enumValue != null) { - decl = - CelIdentDecl.newBuilder() - .setName(cand) - .setType(SimpleType.INT) - .setConstant(CelConstant.ofValue(enumValue)) - .build(); + return null; + } - decls.get(0).putIdent(decl); - return decl; + private @Nullable CelIdentDecl tryLookupCelIdent(String cand) { + // First determine whether we know this name already. + CelIdentDecl decl = findIdentDecl(cand); + if (decl != null) { + return decl; + } + + // Next try to import the name as a reference to a message type. + // This is done via the type provider. + Optional type = typeProvider.lookupCelType(cand); + if (type.isPresent()) { + decl = CelIdentDecl.newIdentDeclaration(cand, type.get()); + decls.get(0).putIdent(decl); + return decl; + } + + // Next try to import this as an enum value by splitting the name in a type prefix and + // the enum inside. + Integer enumValue = typeProvider.lookupEnumValue(cand); + if (enumValue != null) { + decl = + CelIdentDecl.newBuilder() + .setName(cand) + .setType(SimpleType.INT) + .setConstant(CelConstant.ofValue(enumValue)) + .build(); + + decls.get(0).putIdent(decl); + return decl; + } + return null; + } + + /** + * Lookup a local identifier by name. This searches only comprehension scopes, bypassing standard + * environment or user-defined environment. + * + *

Returns {@code null} if not found in local scopes. + */ + @Nullable CelIdentDecl tryLookupCelIdentFromLocalScopes(String name) { + int firstUserSpaceScope = 2; + // Iterate from the top of the stack down to the first local scope. + // Note that: + // Scope 0: Standard environment + // Scope 1: User defined environment + // Scope 2 and onwards: comprehension scopes + for (int i = decls.size() - 1; i >= firstUserSpaceScope; i--) { + CelIdentDecl ident = decls.get(i).getIdent(name); + if (ident != null) { + return ident; } } return null; @@ -493,10 +545,15 @@ public Env add(String name, Type type) { * Lookup a name like {@link #tryLookupCelIdent}, but report an error if the name is not found and * return the {@link #ERROR_IDENT_DECL}. */ - public CelIdentDecl lookupIdent(int position, String inContainer, String name) { - CelIdentDecl result = tryLookupCelIdent(inContainer, name); + public CelIdentDecl lookupIdent(long exprId, int position, CelContainer container, String name) { + CelIdentDecl result = tryLookupCelIdent(container, name); if (result == null) { - reportError(position, "undeclared reference to '%s' (in container '%s')", name, inContainer); + reportError( + exprId, + position, + "undeclared reference to '%s' (in container '%s')", + name, + container.name()); return ERROR_IDENT_DECL; } return result; @@ -506,18 +563,34 @@ public CelIdentDecl lookupIdent(int position, String inContainer, String name) { * Lookup a name like {@link #tryLookupCelFunction} but report an error if the name is not found * and return the {@link #ERROR_FUNCTION_DECL}. */ - public CelFunctionDecl lookupFunction(int position, String inContainer, String name) { - CelFunctionDecl result = tryLookupCelFunction(inContainer, name); + public CelFunctionDecl lookupFunction( + long exprId, int position, CelContainer container, String name) { + CelFunctionDecl result = tryLookupCelFunction(container, name); if (result == null) { - reportError(position, "undeclared reference to '%s' (in container '%s')", name, inContainer); + reportError( + exprId, + position, + "undeclared reference to '%s' (in container '%s')", + name, + container.name()); return ERROR_FUNCTION_DECL; } return result; } - /** Reports an error. */ + /** + * Note: Used by legacy type-checker users + * + * @deprecated Use {@link #reportError(long, int, String, Object...) instead.} + */ + @Deprecated public void reportError(int position, String message, Object... args) { - errors.reportError(position, message, args); + reportError(0L, position, message, args); + } + + /** Reports an error. */ + public void reportError(long exprId, int position, String message, Object... args) { + errors.reportError(exprId, position, message, args); } boolean enableCompileTimeOverloadResolution() { @@ -532,14 +605,6 @@ boolean enableNamespacedDeclarations() { return celOptions.enableNamespacedDeclarations(); } - boolean enableHeterogeneousNumericComparisons() { - return celOptions.enableHeterogeneousNumericComparisons(); - } - - boolean enableTimestampEpoch() { - return celOptions.enableTimestampEpoch(); - } - /** Add an identifier {@code decl} to the environment. */ @CanIgnoreReturnValue private Env addIdent(CelIdentDecl celIdentDecl) { @@ -548,7 +613,8 @@ private Env addIdent(CelIdentDecl celIdentDecl) { getDeclGroup().putIdent(celIdentDecl); } else { reportError( - 0, + /* exprId= */ 0, + /* position= */ 0, "overlapping declaration name '%s' (type '%s' cannot be distinguished from '%s')", celIdentDecl.name(), CelTypes.format(current.type()), @@ -594,7 +660,8 @@ private void addOverload(CelFunctionDecl.Builder builder, CelOverloadDecl overlo || Types.isAssignable(emptySubs, existingTypeErased, overloadTypeErased) != null; if (overlap && existing.isInstanceFunction() == overload.isInstanceFunction()) { reportError( - 0, + /* exprId= */ 0, + /* position= */ 0, "overlapping overload for name '%s' (type '%s' cannot be distinguished from '%s')", builder.name(), CelTypes.format(existingFunction), @@ -609,7 +676,8 @@ private void addOverload(CelFunctionDecl.Builder builder, CelOverloadDecl overlo && macro.getDefinition().isReceiverStyle() == overload.isInstanceFunction() && macro.getDefinition().getArgumentCount() == overload.parameterTypes().size()) { reportError( - 0, + /* exprId= */ 0, + /* position= */ 0, "overload for name '%s' with %s argument(s) overlaps with predefined macro", builder.name(), macro.getDefinition().getArgumentCount()); @@ -674,30 +742,6 @@ private void addOverload(CelFunctionDecl.Builder builder, CelOverloadDecl overlo .build(); } - /** - * Returns the candidates for name resolution of a name within a container(e.g. package, message, - * enum, service elements) context following PB (== C++) conventions. Iterates those names which - * shadow other names first; recognizes and removes a leading '.' for overriding shadowing. Given - * a container name {@code a.b.c.M.N} and a type name {@code R.s}, this will deliver in order - * {@code a.b.c.M.N.R.s, a.b.c.M.R.s, a.b.c.R.s, a.b.R.s, a.R.s, R.s}. - */ - private static ImmutableList qualifiedTypeNameCandidates( - String container, String typeName) { - // This function is a copy of //j/c/g/api/tools/model/SymbolTable#nameCandidates. - if (typeName.startsWith(".")) { - return ImmutableList.of(typeName.substring(1)); - } - if (container.isEmpty()) { - return ImmutableList.of(typeName); - } else { - int i = container.lastIndexOf('.'); - return ImmutableList.builder() - .add(container + "." + typeName) - .addAll(qualifiedTypeNameCandidates(i >= 0 ? container.substring(0, i) : "", typeName)) - .build(); - } - } - /** * A helper class for constructing identifier declarations. * @@ -716,7 +760,7 @@ public IdentBuilder(String name) { @CanIgnoreReturnValue public IdentBuilder type(Type value) { Preconditions.checkNotNull(value); - builder.setType(CelTypes.typeToCelType(Preconditions.checkNotNull(value))); + builder.setType(CelProtoTypes.typeToCelType(Preconditions.checkNotNull(value))); return this; } @@ -798,12 +842,12 @@ public FunctionBuilder add(String id, Type resultType, Type... argTypes) { public FunctionBuilder add(String id, Type resultType, Iterable argTypes) { ImmutableList.Builder argumentBuilder = new ImmutableList.Builder<>(); for (Type type : argTypes) { - argumentBuilder.add(CelTypes.typeToCelType(type)); + argumentBuilder.add(CelProtoTypes.typeToCelType(type)); } this.overloads.add( CelOverloadDecl.newBuilder() .setOverloadId(id) - .setResultType(CelTypes.typeToCelType(resultType)) + .setResultType(CelProtoTypes.typeToCelType(resultType)) .addParameterTypes(argumentBuilder.build()) .setIsInstanceFunction(isInstance) .build()); @@ -823,12 +867,12 @@ public FunctionBuilder add( String id, List typeParams, Type resultType, Iterable argTypes) { ImmutableList.Builder argumentBuilder = new ImmutableList.Builder<>(); for (Type type : argTypes) { - argumentBuilder.add(CelTypes.typeToCelType(type)); + argumentBuilder.add(CelProtoTypes.typeToCelType(type)); } this.overloads.add( CelOverloadDecl.newBuilder() .setOverloadId(id) - .setResultType(CelTypes.typeToCelType(resultType)) + .setResultType(CelProtoTypes.typeToCelType(resultType)) .addParameterTypes(argumentBuilder.build()) .setIsInstanceFunction(isInstance) .build()); @@ -966,7 +1010,7 @@ private static CelFunctionDecl sanitizeFunction(CelFunctionDecl func) { overloadBuilder.setResultType(getWellKnownType(resultType)); } - ImmutableSet.Builder parameterTypeBuilder = ImmutableSet.builder(); + ImmutableList.Builder parameterTypeBuilder = ImmutableList.builder(); for (CelType paramType : overloadBuilder.parameterTypes()) { if (isWellKnownType(paramType)) { parameterTypeBuilder.add(getWellKnownType(paramType)); diff --git a/checker/src/main/java/dev/cel/checker/ExprChecker.java b/checker/src/main/java/dev/cel/checker/ExprChecker.java index 5c0c21ffd..e3ce99e67 100644 --- a/checker/src/main/java/dev/cel/checker/ExprChecker.java +++ b/checker/src/main/java/dev/cel/checker/ExprChecker.java @@ -22,32 +22,46 @@ import com.google.auto.value.AutoValue; import com.google.common.base.Joiner; import com.google.common.base.Optional; -import com.google.common.base.Preconditions; +import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.Maps; import com.google.errorprone.annotations.CheckReturnValue; import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelContainer; import dev.cel.common.CelFunctionDecl; +import dev.cel.common.CelMutableAst; import dev.cel.common.CelOverloadDecl; import dev.cel.common.CelProtoAbstractSyntaxTree; +import dev.cel.common.CelSource; +import dev.cel.common.Operator; import dev.cel.common.annotations.Internal; import dev.cel.common.ast.CelConstant; import dev.cel.common.ast.CelExpr; +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.CelMutableIdent; +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.ast.CelMutableExpr.CelMutableStruct; import dev.cel.common.ast.CelReference; 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.common.types.ListType; import dev.cel.common.types.MapType; import dev.cel.common.types.OptionalType; +import dev.cel.common.types.ProtoMessageType; import dev.cel.common.types.SimpleType; import dev.cel.common.types.TypeType; -import dev.cel.parser.Operator; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; -import org.jspecify.nullness.Nullable; +import java.util.Set; +import org.jspecify.annotations.Nullable; /** * The expression type checker. @@ -59,10 +73,14 @@ @Internal @Deprecated public final class ExprChecker { + private static final CelSource.Extension JSON_NAME_EXTENSION = + CelSource.Extension.create( + "json_name", + CelSource.Extension.Version.of(1, 1), + CelSource.Extension.Component.COMPONENT_RUNTIME); /** - * Checks the parsed expression within the given environment and returns a checked expression. - * Conditions for type checking and the result are described in checked.proto. + * Deprecated type-check API. * * @deprecated Do not use. CEL-Java users should leverage the Fluent APIs instead. See {@code * CelCompilerFactory}. @@ -74,10 +92,7 @@ public static CheckedExpr check(Env env, String inContainer, ParsedExpr parsedEx } /** - * Type-checks the parsed expression within the given environment and returns a checked - * expression. If an expected result type was given, then it verifies that that type matches the - * actual result type. Conditions for type checking and the constructed {@code CheckedExpr} are - * described in checked.proto. + * Deprecated type-check API. * * @deprecated Do not use. CEL-Java users should leverage the Fluent APIs instead. See {@code * CelCompilerFactory}. @@ -88,11 +103,14 @@ public static CheckedExpr typecheck( Env env, String inContainer, ParsedExpr parsedExpr, Optional expectedResultType) { Optional type = expectedResultType.isPresent() - ? Optional.of(CelTypes.typeToCelType(expectedResultType.get())) + ? Optional.of(CelProtoTypes.typeToCelType(expectedResultType.get())) : Optional.absent(); CelAbstractSyntaxTree ast = typecheck( - env, inContainer, CelProtoAbstractSyntaxTree.fromParsedExpr(parsedExpr).getAst(), type); + env, + CelContainer.ofName(inContainer), + CelProtoAbstractSyntaxTree.fromParsedExpr(parsedExpr).getAst(), + type); if (ast.isChecked()) { return CelProtoAbstractSyntaxTree.fromCelAst(ast).toCheckedExpr(); @@ -116,85 +134,100 @@ public static CheckedExpr typecheck( @Internal public static CelAbstractSyntaxTree typecheck( Env env, - String inContainer, + CelContainer container, CelAbstractSyntaxTree ast, Optional expectedResultType) { env.resetTypeAndRefMaps(); final ExprChecker checker = new ExprChecker( env, - inContainer, + container, ast.getSource().getPositionsMap(), new InferenceContext(), env.enableCompileTimeOverloadResolution(), env.enableHomogeneousLiterals(), env.enableNamespacedDeclarations()); - CelExpr expr = checker.visit(ast.getExpr()); + + CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + checker.visit(mutableAst.expr()); if (expectedResultType.isPresent()) { - checker.assertType(expr, expectedResultType.get()); + checker.assertType(mutableAst.expr(), expectedResultType.get()); } // Walk over the final type map substituting any type parameters either by their bound value or // by DYN. Map typeMap = Maps.transformValues(env.getTypeMap(), checker.inferenceContext::finalize); - return CelAbstractSyntaxTree.newCheckedAst(expr, ast.getSource(), env.getRefMap(), typeMap); + CelAbstractSyntaxTree parsedAst = mutableAst.toParsedAst(/* retainSourcePositions= */ true); + return CelAbstractSyntaxTree.newCheckedAst( + parsedAst.getExpr(), + parsedAst.getSource().toBuilder().addAllExtensions(checker.extensions).build(), + env.getRefMap(), + typeMap); } private final Env env; private final TypeProvider typeProvider; - private final String inContainer; + private final CelContainer container; private final Map positionMap; private final InferenceContext inferenceContext; private final boolean compileTimeOverloadResolution; private final boolean homogeneousLiterals; private final boolean namespacedDeclarations; + private final Set extensions; private ExprChecker( Env env, - String inContainer, + CelContainer container, Map positionMap, InferenceContext inferenceContext, boolean compileTimeOverloadResolution, boolean homogeneousLiterals, boolean namespacedDeclarations) { - this.env = Preconditions.checkNotNull(env); + this.env = checkNotNull(env); this.typeProvider = env.getTypeProvider(); - this.positionMap = Preconditions.checkNotNull(positionMap); - this.inContainer = Preconditions.checkNotNull(inContainer); - this.inferenceContext = Preconditions.checkNotNull(inferenceContext); + this.positionMap = checkNotNull(positionMap); + this.container = checkNotNull(container); + this.inferenceContext = checkNotNull(inferenceContext); this.compileTimeOverloadResolution = compileTimeOverloadResolution; this.homogeneousLiterals = homogeneousLiterals; this.namespacedDeclarations = namespacedDeclarations; + this.extensions = new HashSet<>(); } /** Visit the {@code expr} value, routing to overloads based on the kind of expression. */ - @CheckReturnValue - public CelExpr visit(CelExpr expr) { - switch (expr.exprKind().getKind()) { + public void visit(CelMutableExpr expr) { + switch (expr.getKind()) { case CONSTANT: - return visit(expr, expr.constant()); + visit(expr, expr.constant()); + break; case IDENT: - return visit(expr, expr.ident()); + visit(expr, expr.ident()); + break; case SELECT: - return visit(expr, expr.select()); + visit(expr, expr.select()); + break; case CALL: - return visit(expr, expr.call()); - case CREATE_LIST: - return visit(expr, expr.createList()); - case CREATE_STRUCT: - return visit(expr, expr.createStruct()); - case CREATE_MAP: - return visit(expr, expr.createMap()); + visit(expr, expr.call()); + break; + case LIST: + visit(expr, expr.list()); + break; + case STRUCT: + visit(expr, expr.struct()); + break; + case MAP: + visit(expr, expr.map()); + break; case COMPREHENSION: - return visit(expr, expr.comprehension()); + visit(expr, expr.comprehension()); + break; default: throw new IllegalArgumentException("unexpected expr kind"); } } - @CheckReturnValue - private CelExpr visit(CelExpr expr, CelConstant constant) { + private void visit(CelMutableExpr expr, CelConstant constant) { switch (constant.getKind()) { case INT64_VALUE: env.setType(expr, SimpleType.INT); @@ -226,80 +259,71 @@ private CelExpr visit(CelExpr expr, CelConstant constant) { default: throw new IllegalArgumentException("unexpected constant case: " + constant.getKind()); } - return expr; } - @CheckReturnValue - private CelExpr visit(CelExpr expr, CelExpr.CelIdent ident) { - CelIdentDecl decl = env.lookupIdent(getPosition(expr), inContainer, ident.name()); + private void visit(CelMutableExpr expr, CelMutableIdent ident) { + CelIdentDecl decl = env.lookupIdent(expr.id(), getPosition(expr), container, ident.name()); checkNotNull(decl); if (decl.equals(Env.ERROR_IDENT_DECL)) { // error reported env.setType(expr, SimpleType.ERROR); - env.setRef(expr, makeReference(decl)); - return expr; + env.setRef(expr, makeReference(decl.name(), decl)); + return; } - if (!decl.name().equals(ident.name())) { + + String refName = maybeDisambiguate(ident.name(), decl.name()); + + if (!refName.equals(ident.name())) { // Overwrite the identifier with its fully qualified name. - expr = replaceIdentSubtree(expr, decl.name()); + expr.setIdent(CelMutableIdent.create(refName)); } env.setType(expr, decl.type()); - env.setRef(expr, makeReference(decl)); - return expr; + env.setRef(expr, makeReference(refName, decl)); } - @CheckReturnValue - private CelExpr visit(CelExpr expr, CelExpr.CelSelect select) { + private void visit(CelMutableExpr expr, CelMutableSelect select) { // Before traversing down the tree, try to interpret as qualified name. String qname = asQualifiedName(expr); if (qname != null) { - CelIdentDecl decl = env.tryLookupCelIdent(inContainer, qname); + CelIdentDecl decl = env.tryLookupCelIdent(container, qname); if (decl != null) { if (select.testOnly()) { - env.reportError(getPosition(expr), "expression does not select a field"); + env.reportError(expr.id(), getPosition(expr), "expression does not select a field"); env.setType(expr, SimpleType.BOOL); } else { + String refName = maybeDisambiguate(qname, decl.name()); + if (namespacedDeclarations) { // Rewrite the node to be a variable reference to the resolved fully-qualified // variable name. - expr = replaceIdentSubtree(expr, decl.name()); + expr.setIdent(CelMutableIdent.create(refName)); } env.setType(expr, decl.type()); - env.setRef(expr, makeReference(decl)); + env.setRef(expr, makeReference(refName, decl)); } - return expr; + return; } } // Interpret as field selection, first traversing down the operand. - CelExpr visitedOperand = visit(select.operand()); - if (namespacedDeclarations && !select.operand().equals(visitedOperand)) { - // Subtree has been rewritten. Replace the operand. - expr = replaceSelectOperandSubtree(expr, visitedOperand); - } - CelType resultType = visitSelectField(expr, visitedOperand, select.field(), false); + visit(select.operand()); + + CelType resultType = visitSelectField(expr, select.operand(), select.field(), false); if (select.testOnly()) { resultType = SimpleType.BOOL; } env.setType(expr, resultType); - return expr; } - @CheckReturnValue - private CelExpr visit(CelExpr expr, CelExpr.CelCall call) { + private void visit(CelMutableExpr expr, CelMutableCall call) { String functionName = call.function(); if (Operator.OPTIONAL_SELECT.getFunction().equals(functionName)) { - return visitOptionalCall(expr, call); + visitOptionalCall(expr, call); + return; } // Traverse arguments. - ImmutableList argsList = call.args(); - for (int i = 0; i < argsList.size(); i++) { - CelExpr arg = argsList.get(i); - CelExpr visitedArg = visit(arg); - if (namespacedDeclarations && !visitedArg.equals(arg)) { - // Argument has been overwritten. - expr = replaceCallArgumentSubtree(expr, visitedArg, i); - } + for (CelMutableExpr arg : call.args()) { + visit(arg); } int position = getPosition(expr); @@ -307,42 +331,38 @@ private CelExpr visit(CelExpr expr, CelExpr.CelCall call) { if (!call.target().isPresent()) { // Regular static call with simple name. - CelFunctionDecl decl = env.lookupFunction(position, inContainer, call.function()); - resolution = resolveOverload(position, decl, null, call.args()); + CelFunctionDecl decl = env.lookupFunction(expr.id(), position, container, call.function()); + resolution = resolveOverload(expr.id(), position, decl, null, call.args()); if (!decl.name().equals(call.function())) { if (namespacedDeclarations) { // Overwrite the function name with its fully qualified resolved name. - expr = replaceCallSubtree(expr, decl.name()); + expr.setCall(CelMutableCall.create(decl.name(), call.args())); } } } else { // Check whether the target is actually a qualified name for a static function. String qualifiedName = asQualifiedName(call.target().get()); CelFunctionDecl decl = - env.tryLookupCelFunction(inContainer, qualifiedName + "." + call.function()); + env.tryLookupCelFunction(container, qualifiedName + "." + call.function()); if (decl != null) { - resolution = resolveOverload(position, decl, null, call.args()); + resolution = resolveOverload(expr.id(), position, decl, null, call.args()); if (namespacedDeclarations) { // The function name is namespaced and so preserving the target operand would // be an inaccurate representation of the desired evaluation behavior. // Overwrite with fully-qualified resolved function name sans receiver target. - expr = replaceCallSubtree(expr, decl.name()); + expr.setCall(CelMutableCall.create(decl.name(), call.args())); } } else { // Regular instance call. - CelExpr target = call.target().get(); - CelExpr visitedTargetExpr = visit(target); - if (namespacedDeclarations && !visitedTargetExpr.equals(target)) { - // Visiting target contained a namespaced function. Rewrite the call expression here by - // setting the target to the new subtree. - expr = replaceCallSubtree(expr, visitedTargetExpr); - } + CelMutableExpr target = call.target().get(); + visit(target); resolution = resolveOverload( + expr.id(), position, - env.lookupFunction(getPosition(expr), inContainer, call.function()), + env.lookupFunction(expr.id(), getPosition(expr), container, call.function()), target, call.args()); } @@ -350,26 +370,31 @@ private CelExpr visit(CelExpr expr, CelExpr.CelCall call) { env.setType(expr, resolution.type()); env.setRef(expr, resolution.reference()); - - return expr; } - @CheckReturnValue - private CelExpr visit(CelExpr expr, CelExpr.CelCreateStruct createStruct) { + private void visit(CelMutableExpr expr, CelMutableStruct struct) { // Determine the type of the message. CelType messageType = SimpleType.ERROR; - CelIdentDecl decl = env.lookupIdent(getPosition(expr), inContainer, createStruct.messageName()); + CelIdentDecl decl = + env.lookupIdent(expr.id(), getPosition(expr), container, struct.messageName()); + if (!struct.messageName().equals(decl.name())) { + struct.setMessageName(decl.name()); + } + env.setRef(expr, CelReference.newBuilder().setName(decl.name()).build()); CelType type = decl.type(); - if (type.kind() != CelKind.ERROR) { - if (type.kind() != CelKind.TYPE) { + if (!type.kind().equals(CelKind.ERROR)) { + if (!type.kind().equals(CelKind.TYPE)) { // expected type of types - env.reportError(getPosition(expr), "'%s' is not a type", CelTypes.format(type)); + env.reportError(expr.id(), getPosition(expr), "'%s' is not a type", CelTypes.format(type)); } else { messageType = ((TypeType) type).type(); - if (messageType.kind() != CelKind.STRUCT) { + if (!messageType.kind().equals(CelKind.STRUCT)) { env.reportError( - getPosition(expr), "'%s' is not a message type", CelTypes.format(messageType)); + expr.id(), + getPosition(expr), + "'%s' is not a message type", + CelTypes.format(messageType)); messageType = SimpleType.ERROR; } } @@ -383,26 +408,25 @@ private CelExpr visit(CelExpr expr, CelExpr.CelCreateStruct createStruct) { } // Check the field initializers. - ImmutableList entriesList = createStruct.entries(); - for (int i = 0; i < entriesList.size(); i++) { - CelExpr.CelCreateStruct.Entry entry = entriesList.get(i); - CelExpr visitedValueExpr = visit(entry.value()); - if (namespacedDeclarations && !visitedValueExpr.equals(entry.value())) { - // Subtree has been rewritten. Replace the struct value. - expr = replaceStructEntryValueSubtree(expr, visitedValueExpr, i); - } - CelType fieldType = getFieldType(getPosition(entry), messageType, entry.fieldKey()).celType(); - CelType valueType = env.getType(visitedValueExpr); + List entriesList = struct.entries(); + for (CelMutableStruct.Entry entry : entriesList) { + CelMutableExpr value = entry.value(); + visit(value); + + CelType fieldType = + getFieldType(entry.id(), getPosition(entry), messageType, entry.fieldKey()).celType(); + CelType valueType = env.getType(value); if (entry.optionalEntry()) { if (valueType instanceof OptionalType) { valueType = unwrapOptional(valueType); } else { assertIsAssignable( - getPosition(visitedValueExpr), valueType, OptionalType.create(valueType)); + value.id(), getPosition(value), valueType, OptionalType.create(valueType)); } } if (!inferenceContext.isAssignable(fieldType, valueType)) { env.reportError( + expr.id(), getPosition(entry), "expected type of field '%s' is '%s' but provided type is '%s'", entry.fieldKey(), @@ -410,40 +434,32 @@ private CelExpr visit(CelExpr expr, CelExpr.CelCreateStruct createStruct) { CelTypes.format(valueType)); } } - return expr; } - @CheckReturnValue - private CelExpr visit(CelExpr expr, CelExpr.CelCreateMap createMap) { + private void visit(CelMutableExpr expr, CelMutableMap map) { CelType mapKeyType = null; CelType mapValueType = null; - ImmutableList entriesList = createMap.entries(); - for (int i = 0; i < entriesList.size(); i++) { - CelExpr.CelCreateMap.Entry entry = entriesList.get(i); - CelExpr visitedMapKeyExpr = visit(entry.key()); - if (namespacedDeclarations && !visitedMapKeyExpr.equals(entry.key())) { - // Subtree has been rewritten. Replace the map key. - expr = replaceMapEntryKeySubtree(expr, visitedMapKeyExpr, i); - } - mapKeyType = - joinTypes(getPosition(visitedMapKeyExpr), mapKeyType, env.getType(visitedMapKeyExpr)); + List entriesList = map.entries(); + for (CelMutableMap.Entry entry : entriesList) { + CelMutableExpr key = entry.key(); + visit(key); - CelExpr visitedValueExpr = visit(entry.value()); - if (namespacedDeclarations && !visitedValueExpr.equals(entry.value())) { - // Subtree has been rewritten. Replace the map value. - expr = replaceMapEntryValueSubtree(expr, visitedValueExpr, i); - } - CelType valueType = env.getType(visitedValueExpr); + mapKeyType = joinTypes(key.id(), getPosition(key), mapKeyType, env.getType(key)); + + CelMutableExpr value = entry.value(); + visit(value); + + CelType valueType = env.getType(value); if (entry.optionalEntry()) { if (valueType instanceof OptionalType) { valueType = unwrapOptional(valueType); } else { assertIsAssignable( - getPosition(visitedValueExpr), valueType, OptionalType.create(valueType)); + value.id(), getPosition(value), valueType, OptionalType.create(valueType)); } } - mapValueType = joinTypes(getPosition(visitedValueExpr), mapValueType, valueType); + mapValueType = joinTypes(value.id(), getPosition(value), mapValueType, valueType); } if (mapKeyType == null) { // If the map is empty, assign free type variables to key and value type. @@ -451,60 +467,60 @@ private CelExpr visit(CelExpr expr, CelExpr.CelCreateMap createMap) { mapValueType = inferenceContext.newTypeVar("value"); } env.setType(expr, MapType.create(mapKeyType, mapValueType)); - return expr; } - @CheckReturnValue - private CelExpr visit(CelExpr expr, CelExpr.CelCreateList createList) { + private void visit(CelMutableExpr expr, CelMutableList list) { CelType elemsType = null; - ImmutableList elementsList = createList.elements(); - HashSet optionalIndices = new HashSet<>(createList.optionalIndices()); + List elementsList = list.elements(); + HashSet optionalIndices = new HashSet<>(list.optionalIndices()); for (int i = 0; i < elementsList.size(); i++) { - CelExpr visitedElem = visit(elementsList.get(i)); - if (namespacedDeclarations && !visitedElem.equals(elementsList.get(i))) { - // Subtree has been rewritten. Replace the list element - expr = replaceListElementSubtree(expr, visitedElem, i); - } - CelType elemType = env.getType(visitedElem); + CelMutableExpr elem = elementsList.get(i); + visit(elem); + + CelType elemType = env.getType(elem); if (optionalIndices.contains(i)) { if (elemType instanceof OptionalType) { elemType = unwrapOptional(elemType); } else { - assertIsAssignable(getPosition(visitedElem), elemType, OptionalType.create(elemType)); + assertIsAssignable(elem.id(), getPosition(elem), elemType, OptionalType.create(elemType)); } } - elemsType = joinTypes(getPosition(visitedElem), elemsType, elemType); + elemsType = joinTypes(elem.id(), getPosition(elem), elemsType, elemType); } if (elemsType == null) { // If the list is empty, assign free type var to elem type. elemsType = inferenceContext.newTypeVar("elem"); } env.setType(expr, ListType.create(elemsType)); - return expr; } - @CheckReturnValue - private CelExpr visit(CelExpr expr, CelExpr.CelComprehension compre) { - CelExpr visitedRange = visit(compre.iterRange()); - if (namespacedDeclarations && !visitedRange.equals(compre.iterRange())) { - expr = replaceComprehensionRangeSubtree(expr, visitedRange); - } - CelExpr init = visit(compre.accuInit()); - CelType accuType = env.getType(init); - CelType rangeType = inferenceContext.specialize(env.getType(visitedRange)); + private void visit(CelMutableExpr expr, CelMutableComprehension compre) { + visit(compre.iterRange()); + visit(compre.accuInit()); + CelType accuType = env.getType(compre.accuInit()); + CelType rangeType = inferenceContext.specialize(env.getType(compre.iterRange())); CelType varType; + CelType varType2 = null; switch (rangeType.kind()) { case LIST: varType = ((ListType) rangeType).elemType(); + if (!Strings.isNullOrEmpty(compre.iterVar2())) { + varType2 = varType; + varType = SimpleType.INT; + } break; case MAP: // Ranges over the keys. varType = ((MapType) rangeType).keyType(); + if (!Strings.isNullOrEmpty(compre.iterVar2())) { + varType2 = ((MapType) rangeType).valueType(); + } break; case DYN: case ERROR: varType = SimpleType.DYN; + varType2 = SimpleType.DYN; break; case TYPE_PARAM: // Mark the range as DYN to avoid its free variable being associated with the wrong type @@ -513,14 +529,17 @@ private CelExpr visit(CelExpr expr, CelExpr.CelComprehension compre) { inferenceContext.isAssignable(SimpleType.DYN, rangeType); // Mark the variable type as DYN. varType = SimpleType.DYN; + varType2 = SimpleType.DYN; break; default: env.reportError( - getPosition(visitedRange), + expr.id(), + getPosition(compre.iterRange()), "expression of type '%s' cannot be range of a comprehension " + "(must be list, map, or dynamic)", CelTypes.format(rangeType)); varType = SimpleType.DYN; + varType2 = SimpleType.DYN; break; } @@ -530,37 +549,53 @@ private CelExpr visit(CelExpr expr, CelExpr.CelComprehension compre) { // Declare iteration variable on inner scope. env.enterScope(); env.add(CelIdentDecl.newIdentDeclaration(compre.iterVar(), varType)); - CelExpr condition = visit(compre.loopCondition()); - assertType(condition, SimpleType.BOOL); - CelExpr visitedStep = visit(compre.loopStep()); - if (namespacedDeclarations && !visitedStep.equals(compre.loopStep())) { - expr = replaceComprehensionStepSubtree(expr, visitedStep); + if (!Strings.isNullOrEmpty(compre.iterVar2())) { + env.add(CelIdentDecl.newIdentDeclaration(compre.iterVar2(), varType2)); } - assertType(visitedStep, accuType); + visit(compre.loopCondition()); + assertType(compre.loopCondition(), SimpleType.BOOL); + visit(compre.loopStep()); + assertType(compre.loopStep(), accuType); // Forget iteration variable, as result expression must only depend on accu. env.exitScope(); - CelExpr visitedResult = visit(compre.result()); - if (namespacedDeclarations && !visitedResult.equals(compre.result())) { - expr = replaceComprehensionResultSubtree(expr, visitedResult); - } + visit(compre.result()); env.exitScope(); - env.setType(expr, inferenceContext.specialize(env.getType(visitedResult))); - return expr; + + env.setType(expr, inferenceContext.specialize(env.getType(compre.result()))); } - private CelReference makeReference(CelIdentDecl decl) { - CelReference.Builder ref = CelReference.newBuilder().setName(decl.name()); + private CelReference makeReference(String name, CelIdentDecl decl) { + CelReference.Builder ref = CelReference.newBuilder().setName(name); if (decl.constant().isPresent()) { ref.setValue(decl.constant().get()); } return ref.build(); } + /** + * Returns the reference name, prefixed with a leading dot only if disambiguation is needed. + * Disambiguation is needed when: the original name had a leading dot, and there's a local + * variable that would shadow the resolved name. + */ + private String maybeDisambiguate(String originalName, String resolvedName) { + if (!originalName.startsWith(".")) { + return resolvedName; + } + String simpleName = originalName.substring(1); + int dotIndex = simpleName.indexOf('.'); + String localName = dotIndex > 0 ? simpleName.substring(0, dotIndex) : simpleName; + if (env.tryLookupCelIdentFromLocalScopes(localName) != null) { + return "." + resolvedName; + } + return resolvedName; + } + private OverloadResolution resolveOverload( + long callExprId, int position, @Nullable CelFunctionDecl function, - @Nullable CelExpr target, - List args) { + @Nullable CelMutableExpr target, + List args) { if (function == null || function.equals(Env.ERROR_FUNCTION_DECL)) { // Error reported, just return error value. return OverloadResolution.of(CelReference.newBuilder().build(), SimpleType.ERROR); @@ -569,7 +604,7 @@ private OverloadResolution resolveOverload( if (target != null) { argTypes.add(env.getType(target)); } - for (CelExpr arg : args) { + for (CelMutableExpr arg : args) { argTypes.add(env.getType(arg)); } CelType resultType = null; // For most common result type. @@ -614,6 +649,7 @@ private OverloadResolution resolveOverload( if (compileTimeOverloadResolution) { // In compile-time overload resolution mode report this situation as an error. env.reportError( + callExprId, position, "found more than one matching overload for '%s' applied to '%s': %s and also %s", function.name(), @@ -628,6 +664,7 @@ private OverloadResolution resolveOverload( } if (resultType == null) { env.reportError( + callExprId, position, "found no matching overload for '%s' applied to '%s'%s", function.name(), @@ -643,7 +680,7 @@ private OverloadResolution resolveOverload( // Return value from visit is not needed as the subtree is not rewritten here. @SuppressWarnings("CheckReturnValue") private CelType visitSelectField( - CelExpr expr, CelExpr operand, String field, boolean isOptional) { + CelMutableExpr expr, CelMutableExpr operand, String field, boolean isOptional) { CelType operandType = inferenceContext.specialize(env.getType(operand)); CelType resultType = SimpleType.ERROR; @@ -653,13 +690,18 @@ private CelType visitSelectField( } if (!Types.isDynOrError(operandType)) { - if (operandType.kind() == CelKind.STRUCT) { - TypeProvider.FieldType fieldType = getFieldType(getPosition(expr), operandType, field); + if (operandType.kind().equals(CelKind.STRUCT)) { + TypeProvider.FieldType fieldType = + getFieldType(expr.id(), getPosition(expr), operandType, field); + ProtoMessageType protoMessageType = resolveProtoMessageType(operandType); + if (protoMessageType != null && protoMessageType.isJsonName(field)) { + extensions.add(JSON_NAME_EXTENSION); + } // Type of the field resultType = fieldType.celType(); - } else if (operandType.kind() == CelKind.MAP) { + } else if (operandType.kind().equals(CelKind.MAP)) { resultType = ((MapType) operandType).valueType(); - } else if (operandType.kind() == CelKind.TYPE_PARAM) { + } else if (operandType.kind().equals(CelKind.TYPE_PARAM)) { // Mark the operand as type DYN to avoid cases where the free type variable might take on // an incorrect type if used in multiple locations. // @@ -673,6 +715,7 @@ private CelType visitSelectField( resultType = SimpleType.DYN; } else { env.reportError( + expr.id(), getPosition(expr), "type '%s' does not support field selection", CelTypes.format(operandType)); @@ -688,25 +731,48 @@ private CelType visitSelectField( return resultType; } - private CelExpr visitOptionalCall(CelExpr expr, CelExpr.CelCall call) { - CelExpr operand = call.args().get(0); - CelExpr field = call.args().get(1); - if (!field.exprKind().getKind().equals(CelExpr.ExprKind.Kind.CONSTANT) - || field.constant().getKind() != CelConstant.Kind.STRING_VALUE) { - env.reportError(getPosition(field), "unsupported optional field selection"); - return expr; + private @Nullable ProtoMessageType resolveProtoMessageType(CelType operandType) { + if (operandType instanceof ProtoMessageType) { + return (ProtoMessageType) operandType; } - CelExpr visitedOperand = visit(operand); - if (namespacedDeclarations && !operand.equals(visitedOperand)) { - // Subtree has been rewritten. Replace the operand. - expr = replaceSelectOperandSubtree(expr, visitedOperand); + if (operandType.kind().equals(CelKind.STRUCT)) { + // This is either a StructTypeReference or just a Struct. Attempt to search for + // ProtoMessageType that may exist in in the type provider. + TypeType typeDef = + typeProvider + .lookupCelType(operandType.name()) + .filter(t -> t instanceof TypeType) + .map(TypeType.class::cast) + .orElse(null); + if (typeDef == null || typeDef.parameters().size() != 1) { + return null; + } + + CelType maybeProtoMessageType = typeDef.parameters().get(0); + if (maybeProtoMessageType instanceof ProtoMessageType) { + return (ProtoMessageType) maybeProtoMessageType; + } } + + return null; + } + + private void visitOptionalCall(CelMutableExpr expr, CelMutableCall call) { + CelMutableExpr operand = call.args().get(0); + CelMutableExpr field = call.args().get(1); + if (!field.getKind().equals(CelExpr.ExprKind.Kind.CONSTANT) + || !field.constant().getKind().equals(CelConstant.Kind.STRING_VALUE)) { + + env.reportError(expr.id(), getPosition(field), "unsupported optional field selection"); + return; + } + + visit(operand); + CelType resultType = visitSelectField(expr, operand, field.constant().stringValue(), true); env.setType(expr, resultType); env.setRef(expr, CelReference.newBuilder().addOverloadIds("select_optional_field").build()); - - return expr; } /** @@ -714,8 +780,8 @@ private CelExpr visitOptionalCall(CelExpr expr, CelExpr.CelCall call) { * expression and returns the name they constitute, or null if the expression cannot be * interpreted like this. */ - private @Nullable String asQualifiedName(CelExpr expr) { - switch (expr.exprKind().getKind()) { + private @Nullable String asQualifiedName(CelMutableExpr expr) { + switch (expr.getKind()) { case IDENT: return expr.ident().name(); case SELECT: @@ -730,7 +796,8 @@ private CelExpr visitOptionalCall(CelExpr expr, CelExpr.CelCall call) { } /** Returns the field type give a type instance and field name. */ - private TypeProvider.FieldType getFieldType(int position, CelType type, String fieldName) { + private TypeProvider.FieldType getFieldType( + long exprId, int position, CelType type, String fieldName) { String typeName = type.name(); if (typeProvider.lookupCelType(typeName).isPresent()) { TypeProvider.FieldType fieldType = typeProvider.lookupFieldType(type, fieldName); @@ -742,36 +809,39 @@ private TypeProvider.FieldType getFieldType(int position, CelType type, String f if (extensionFieldType != null) { return extensionFieldType.fieldType(); } - env.reportError(position, "undefined field '%s'", fieldName); + env.reportError(exprId, position, "undefined field '%s'", fieldName); } else { // Proto message was added as a variable to the environment but the descriptor was not // provided - env.reportError( - position, - "Message type resolution failure while referencing field '%s'. Ensure that the descriptor" - + " for type '%s' was added to the environment", - fieldName, - typeName); + String errorMessage = + String.format("Message type resolution failure while referencing field '%s'.", fieldName); + if (type.kind().equals(CelKind.STRUCT)) { + errorMessage += + String.format( + " Ensure that the descriptor for type '%s' was added to the environment", typeName); + } + env.reportError(exprId, position, errorMessage, fieldName, typeName); } return ERROR; } /** Checks compatibility of joined types, and returns the most general common type. */ - private CelType joinTypes(int position, CelType previousType, CelType type) { + private CelType joinTypes(long exprId, int position, CelType previousType, CelType type) { if (previousType == null) { return type; } if (homogeneousLiterals) { - assertIsAssignable(position, type, previousType); + assertIsAssignable(exprId, position, type, previousType); } else if (!inferenceContext.isAssignable(previousType, type)) { return SimpleType.DYN; } return Types.mostGeneral(previousType, type); } - private void assertIsAssignable(int position, CelType actual, CelType expected) { + private void assertIsAssignable(long exprId, int position, CelType actual, CelType expected) { if (!inferenceContext.isAssignable(expected, actual)) { env.reportError( + exprId, position, "expected type '%s' but found '%s'", CelTypes.format(expected), @@ -783,16 +853,16 @@ private CelType unwrapOptional(CelType type) { return type.parameters().get(0); } - private void assertType(CelExpr expr, CelType type) { - assertIsAssignable(getPosition(expr), env.getType(expr), type); + private void assertType(CelMutableExpr expr, CelType type) { + assertIsAssignable(expr.id(), getPosition(expr), env.getType(expr), type); } - private int getPosition(CelExpr expr) { + private int getPosition(CelMutableExpr expr) { Integer pos = positionMap.get(expr.id()); return pos == null ? 0 : pos; } - private int getPosition(CelExpr.CelCreateStruct.Entry entry) { + private int getPosition(CelMutableStruct.Entry entry) { Integer pos = positionMap.get(entry.id()); return pos == null ? 0 : pos; } @@ -815,78 +885,4 @@ public static OverloadResolution of(CelReference reference, CelType type) { /** Helper object to represent a {@link TypeProvider.FieldType} lookup failure. */ private static final TypeProvider.FieldType ERROR = TypeProvider.FieldType.of(Types.ERROR); - - private static CelExpr replaceIdentSubtree(CelExpr expr, String name) { - CelExpr.CelIdent newIdent = CelExpr.CelIdent.newBuilder().setName(name).build(); - return expr.toBuilder().setIdent(newIdent).build(); - } - - private static CelExpr replaceSelectOperandSubtree(CelExpr expr, CelExpr operand) { - CelExpr.CelSelect newSelect = expr.select().toBuilder().setOperand(operand).build(); - return expr.toBuilder().setSelect(newSelect).build(); - } - - private static CelExpr replaceCallArgumentSubtree(CelExpr expr, CelExpr newArg, int index) { - CelExpr.CelCall newCall = expr.call().toBuilder().setArg(index, newArg).build(); - return expr.toBuilder().setCall(newCall).build(); - } - - private static CelExpr replaceCallSubtree(CelExpr expr, String functionName) { - CelExpr.CelCall newCall = - expr.call().toBuilder().setFunction(functionName).clearTarget().build(); - return expr.toBuilder().setCall(newCall).build(); - } - - private static CelExpr replaceCallSubtree(CelExpr expr, CelExpr target) { - CelExpr.CelCall newCall = expr.call().toBuilder().setTarget(target).build(); - return expr.toBuilder().setCall(newCall).build(); - } - - private static CelExpr replaceListElementSubtree(CelExpr expr, CelExpr element, int index) { - CelExpr.CelCreateList newList = - expr.createList().toBuilder().setElement(index, element).build(); - return expr.toBuilder().setCreateList(newList).build(); - } - - private static CelExpr replaceStructEntryValueSubtree(CelExpr expr, CelExpr newValue, int index) { - CelExpr.CelCreateStruct createStruct = expr.createStruct(); - CelExpr.CelCreateStruct.Entry newEntry = - createStruct.entries().get(index).toBuilder().setValue(newValue).build(); - createStruct = createStruct.toBuilder().setEntry(index, newEntry).build(); - return expr.toBuilder().setCreateStruct(createStruct).build(); - } - - private static CelExpr replaceMapEntryKeySubtree(CelExpr expr, CelExpr newKey, int index) { - CelExpr.CelCreateMap createMap = expr.createMap(); - CelExpr.CelCreateMap.Entry newEntry = - createMap.entries().get(index).toBuilder().setKey(newKey).build(); - createMap = createMap.toBuilder().setEntry(index, newEntry).build(); - return expr.toBuilder().setCreateMap(createMap).build(); - } - - private static CelExpr replaceMapEntryValueSubtree(CelExpr expr, CelExpr newValue, int index) { - CelExpr.CelCreateMap createMap = expr.createMap(); - CelExpr.CelCreateMap.Entry newEntry = - createMap.entries().get(index).toBuilder().setValue(newValue).build(); - createMap = createMap.toBuilder().setEntry(index, newEntry).build(); - return expr.toBuilder().setCreateMap(createMap).build(); - } - - private static CelExpr replaceComprehensionRangeSubtree(CelExpr expr, CelExpr newRange) { - CelExpr.CelComprehension newComprehension = - expr.comprehension().toBuilder().setIterRange(newRange).build(); - return expr.toBuilder().setComprehension(newComprehension).build(); - } - - private static CelExpr replaceComprehensionStepSubtree(CelExpr expr, CelExpr newStep) { - CelExpr.CelComprehension newComprehension = - expr.comprehension().toBuilder().setLoopStep(newStep).build(); - return expr.toBuilder().setComprehension(newComprehension).build(); - } - - private static CelExpr replaceComprehensionResultSubtree(CelExpr expr, CelExpr newResult) { - CelExpr.CelComprehension newComprehension = - expr.comprehension().toBuilder().setResult(newResult).build(); - return expr.toBuilder().setComprehension(newComprehension).build(); - } } diff --git a/checker/src/main/java/dev/cel/checker/ProtoTypeMask.java b/checker/src/main/java/dev/cel/checker/ProtoTypeMask.java index 1a129a91f..ec1c31840 100644 --- a/checker/src/main/java/dev/cel/checker/ProtoTypeMask.java +++ b/checker/src/main/java/dev/cel/checker/ProtoTypeMask.java @@ -38,6 +38,13 @@ public abstract class ProtoTypeMask { /** WILDCARD_FIELD indicates that all fields within the proto type are visible. */ static final String WILDCARD_FIELD = "*"; + /** HIDDEN_FIELD indicates that all fields within the proto type are not visible. */ + static final String HIDDEN_FIELD = "!"; + + private static final FieldMask HIDDEN_FIELD_MASK = + FieldMask.newBuilder().addPaths(HIDDEN_FIELD).build(); + + private static final FieldPath HIDDEN_FIELD_PATH = FieldPath.of(HIDDEN_FIELD); private static final FieldMask WILDCARD_FIELD_MASK = FieldMask.newBuilder().addPaths(WILDCARD_FIELD).build(); private static final FieldPath WILDCARD_FIELD_PATH = FieldPath.of(WILDCARD_FIELD); @@ -52,6 +59,10 @@ boolean areAllFieldPathsExposed() { return getFieldPathsExposed().stream().allMatch(fp -> fp.equals(WILDCARD_FIELD_PATH)); } + boolean areAllFieldPathsHidden() { + return getFieldPathsExposed().stream().allMatch(fp -> fp.equals(HIDDEN_FIELD_PATH)); + } + public ProtoTypeMask withFieldsAsVariableDeclarations() { return new AutoValue_ProtoTypeMask(getTypeName(), getFieldPathsExposed(), true); } @@ -64,12 +75,12 @@ public ProtoTypeMask withFieldsAsVariableDeclarations() { * treated as variable identifiers bound to the protobuf field name and its associated field type. * *

A {@code FieldMask} contains one or more {@code paths} which contain identifier characters - * that have been dot delimited, e.g.resource.name, request.auth.claims. Here are a few things to + * that have been dot delimited, e.g. resource.name, request.auth.claims. Here are a few things to * keep in mind: * *

@@ -88,7 +99,7 @@ public static ProtoTypeMask of(String typeName, FieldMask fieldMask) { * Construct a new {@code ProtoTypeMask} which exposes all fields in the given {@code typeName} * for use within CEL expressions. * - *

The {@code typeName} should be a fully-qualified path, e.g., {@code + *

The {@code typeName} should be a fully-qualified path, e.g. {@code * "google.rpc.context.AttributeContext"}. * *

All top-level fields in the given {@code typeName} should be treated as variable identifiers @@ -98,6 +109,17 @@ public static ProtoTypeMask ofAllFields(String fullyQualifiedTypeName) { return of(fullyQualifiedTypeName, WILDCARD_FIELD_MASK); } + /** + * Construct a new {@code ProtoTypeMask} which hides all fields in the given {@code typeName} for + * use within CEL expressions. + * + *

The {@code typeName} should be a fully-qualified path, e.g. {@code + * "google.rpc.context.AttributeContext"}. + */ + public static ProtoTypeMask ofAllFieldsHidden(String fullyQualifiedTypeName) { + return of(fullyQualifiedTypeName, HIDDEN_FIELD_MASK); + } + /** * FieldPath is the equivalent of a field selection represented within a {@link FieldMask#path}. */ diff --git a/checker/src/main/java/dev/cel/checker/ProtoTypeMaskTypeProvider.java b/checker/src/main/java/dev/cel/checker/ProtoTypeMaskTypeProvider.java index 61511c38d..025dfaf1a 100644 --- a/checker/src/main/java/dev/cel/checker/ProtoTypeMaskTypeProvider.java +++ b/checker/src/main/java/dev/cel/checker/ProtoTypeMaskTypeProvider.java @@ -92,11 +92,16 @@ private static ImmutableMap computeVisibleFieldsMap( CelTypeProvider delegateProvider, ImmutableSet protoTypeMasks) { Map> fieldMap = new HashMap<>(); for (ProtoTypeMask typeMask : protoTypeMasks) { - Optional rootType = delegateProvider.findType(typeMask.getTypeName()); - checkArgument(rootType.isPresent(), "message not registered: %s", typeMask.getTypeName()); + String typeName = typeMask.getTypeName(); + Optional rootType = delegateProvider.findType(typeName); + checkArgument(rootType.isPresent(), "message not registered: %s", typeName); if (typeMask.areAllFieldPathsExposed()) { continue; } + if (typeMask.areAllFieldPathsHidden()) { + fieldMap.put(typeName, ImmutableSet.of()); + continue; + } // Unroll the type(messageType) to just messageType. CelType type = rootType.get(); checkArgument(type instanceof ProtoMessageType, "type is not a protobuf: %s", type.name()); diff --git a/checker/src/main/java/dev/cel/checker/Standard.java b/checker/src/main/java/dev/cel/checker/Standard.java deleted file mode 100644 index f595dde9a..000000000 --- a/checker/src/main/java/dev/cel/checker/Standard.java +++ /dev/null @@ -1,824 +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.checker; - -import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableList; -import com.google.errorprone.annotations.CanIgnoreReturnValue; -import dev.cel.common.CelFunctionDecl; -import dev.cel.common.CelOverloadDecl; -import dev.cel.common.annotations.Internal; -import dev.cel.common.types.CelType; -import dev.cel.common.types.CelTypes; -import dev.cel.common.types.ListType; -import dev.cel.common.types.MapType; -import dev.cel.common.types.SimpleType; -import dev.cel.common.types.TypeParamType; -import dev.cel.common.types.TypeType; -import dev.cel.parser.Operator; - -/** - * Standard declarations for CEL. - * - *

CEL Library Internals. Do Not Use. - */ -@Internal -public final class Standard { - - private static final ImmutableList CORE_FUNCTION_DECLARATIONS = - coreFunctionDeclarations(); - private static final ImmutableList CORE_IDENT_DECLARATIONS = - coreIdentDeclarations(); - - /** Enumeration of Standard Functions that are not present in {@link Operator}). */ - public enum Function { - BOOL("bool"), - BYTES("bytes"), - CONTAINS("contains"), - DOUBLE("double"), - DURATION("duration"), - DYN("dyn"), - ENDS_WITH("endsWith"), - GET_DATE("getDate"), - GET_DAY_OF_MONTH("getDayOfMonth"), - GET_DAY_OF_WEEK("getDayOfWeek"), - GET_DAY_OF_YEAR("getDayOfYear"), - GET_FULL_YEAR("getFullYear"), - GET_HOURS("getHours"), - GET_MILLISECONDS("getMilliseconds"), - GET_MINUTES("getMinutes"), - GET_MONTH("getMonth"), - GET_SECONDS("getSeconds"), - INT("int"), - LIST("list"), - MAP("map"), - MATCHES("matches"), - NULL_TYPE("null_type"), - SIZE("size"), - STARTS_WITH("startsWith"), - STRING("string"), - TIMESTAMP("timestamp"), - TYPE("type"), - UINT("uint"); - - private final String functionName; - - public String getFunction() { - return functionName; - } - - Function(String functionName) { - this.functionName = functionName; - } - } - - /** - * Adds the standard declarations of CEL to the environment. - * - *

Note: Standard declarations should be provided in their own scope to avoid collisions with - * custom declarations. The {@link Env#standard} helper method does this by default. - */ - @CanIgnoreReturnValue - public static Env add(Env env) { - CORE_FUNCTION_DECLARATIONS.forEach(env::add); - CORE_IDENT_DECLARATIONS.forEach(env::add); - - // TODO: Remove this flag guard once the feature has been auto-enabled. - timestampConversionDeclarations(env.enableTimestampEpoch()).forEach(env::add); - numericComparisonDeclarations(env.enableHeterogeneousNumericComparisons()).forEach(env::add); - - return env; - } - - /** Do the expensive work of setting up all the objects in the environment. */ - private static ImmutableList coreIdentDeclarations() { - ImmutableList.Builder identDeclBuilder = ImmutableList.builder(); - - // Type Denotations - for (CelType type : - ImmutableList.of( - SimpleType.INT, - SimpleType.UINT, - SimpleType.BOOL, - SimpleType.DOUBLE, - SimpleType.BYTES, - SimpleType.STRING, - SimpleType.DYN)) { - identDeclBuilder.add( - CelIdentDecl.newBuilder() - .setName(CelTypes.format(type)) - .setType(TypeType.create(type)) - .setDoc("type denotation") - .build()); - } - identDeclBuilder.add( - CelIdentDecl.newBuilder() - .setName("type") - .setType(TypeType.create(SimpleType.DYN)) - .setDoc("type denotation") - .build()); - identDeclBuilder.add( - CelIdentDecl.newBuilder() - .setName("null_type") - .setType(TypeType.create(SimpleType.NULL_TYPE)) - .setDoc("type denotation") - .build()); - identDeclBuilder.add( - CelIdentDecl.newBuilder() - .setName("list") - .setType(TypeType.create(ListType.create(SimpleType.DYN))) - .setDoc("type denotation") - .build()); - identDeclBuilder.add( - CelIdentDecl.newBuilder() - .setName("map") - .setType(TypeType.create(MapType.create(SimpleType.DYN, SimpleType.DYN))) - .setDoc("type denotation") - .build()); - - return identDeclBuilder.build(); - } - - /** Do the expensive work of setting up all the objects in the environment. */ - private static ImmutableList coreFunctionDeclarations() { - // Some shortcuts we use when building declarations. - TypeParamType typeParamA = TypeParamType.create("A"); - ListType listOfA = ListType.create(typeParamA); - TypeParamType typeParamB = TypeParamType.create("B"); - MapType mapOfAb = MapType.create(typeParamA, typeParamB); - - ImmutableList.Builder celFunctionDeclBuilder = ImmutableList.builder(); - - // Booleans - celFunctionDeclBuilder.add( - CelFunctionDecl.newFunctionDeclaration( - Operator.CONDITIONAL.getFunction(), - CelOverloadDecl.newGlobalOverload( - "conditional", - "conditional", - typeParamA, - SimpleType.BOOL, - typeParamA, - typeParamA))); - celFunctionDeclBuilder.add( - CelFunctionDecl.newFunctionDeclaration( - Operator.LOGICAL_AND.getFunction(), - CelOverloadDecl.newGlobalOverload( - "logical_and", "logical_and", SimpleType.BOOL, SimpleType.BOOL, SimpleType.BOOL))); - celFunctionDeclBuilder.add( - CelFunctionDecl.newFunctionDeclaration( - Operator.LOGICAL_OR.getFunction(), - CelOverloadDecl.newGlobalOverload( - "logical_or", "logical or", SimpleType.BOOL, SimpleType.BOOL, SimpleType.BOOL))); - celFunctionDeclBuilder.add( - CelFunctionDecl.newFunctionDeclaration( - Operator.LOGICAL_NOT.getFunction(), - CelOverloadDecl.newGlobalOverload( - "logical_not", "logical not", SimpleType.BOOL, SimpleType.BOOL))); - CelFunctionDecl notStrictlyFalse = - CelFunctionDecl.newFunctionDeclaration( - Operator.OLD_NOT_STRICTLY_FALSE.getFunction(), - CelOverloadDecl.newGlobalOverload( - "not_strictly_false", - "false if argument is false, true otherwise (including errors and unknowns)", - SimpleType.BOOL, - SimpleType.BOOL)); - celFunctionDeclBuilder.add(notStrictlyFalse); - celFunctionDeclBuilder.add( - CelFunctionDecl.newBuilder() - .setName(Operator.NOT_STRICTLY_FALSE.getFunction()) - .addOverloads(sameAs(notStrictlyFalse, "", "")) - .build()); - - // Relations - celFunctionDeclBuilder.add( - CelFunctionDecl.newFunctionDeclaration( - Operator.EQUALS.getFunction(), - CelOverloadDecl.newGlobalOverload( - "equals", "equality", SimpleType.BOOL, typeParamA, typeParamA))); - celFunctionDeclBuilder.add( - CelFunctionDecl.newFunctionDeclaration( - Operator.NOT_EQUALS.getFunction(), - CelOverloadDecl.newGlobalOverload( - "not_equals", "inequality", SimpleType.BOOL, typeParamA, typeParamA))); - - // Algebra - CelFunctionDecl commonArithmetic = - CelFunctionDecl.newFunctionDeclaration( - Operator.SUBTRACT.getFunction(), - CelOverloadDecl.newGlobalOverload( - "common_int64", "arithmetic", SimpleType.INT, SimpleType.INT, SimpleType.INT), - CelOverloadDecl.newGlobalOverload( - "common_uint64", "arithmetic", SimpleType.UINT, SimpleType.UINT, SimpleType.UINT), - CelOverloadDecl.newGlobalOverload( - "common_double", - "arithmetic", - SimpleType.DOUBLE, - SimpleType.DOUBLE, - SimpleType.DOUBLE)); - CelFunctionDecl subtract = - CelFunctionDecl.newBuilder() - .setName(Operator.SUBTRACT.getFunction()) - .addOverloads(sameAs(commonArithmetic, "common", "subtract")) - .addOverloads( - CelOverloadDecl.newGlobalOverload( - "subtract_timestamp_timestamp", - "arithmetic", - SimpleType.DURATION, - SimpleType.TIMESTAMP, - SimpleType.TIMESTAMP), - CelOverloadDecl.newGlobalOverload( - "subtract_timestamp_duration", - "arithmetic", - SimpleType.TIMESTAMP, - SimpleType.TIMESTAMP, - SimpleType.DURATION), - CelOverloadDecl.newGlobalOverload( - "subtract_duration_duration", - "arithmetic", - SimpleType.DURATION, - SimpleType.DURATION, - SimpleType.DURATION)) - .build(); - celFunctionDeclBuilder.add(subtract); - celFunctionDeclBuilder.add( - CelFunctionDecl.newBuilder() - .setName(Operator.MULTIPLY.getFunction()) - .addOverloads(sameAs(commonArithmetic, "common", "multiply")) - .build()); - celFunctionDeclBuilder.add( - CelFunctionDecl.newBuilder() - .setName(Operator.DIVIDE.getFunction()) - .addOverloads(sameAs(commonArithmetic, "common", "divide")) - .build()); - celFunctionDeclBuilder.add( - CelFunctionDecl.newFunctionDeclaration( - Operator.MODULO.getFunction(), - CelOverloadDecl.newGlobalOverload( - "modulo_int64", "arithmetic", SimpleType.INT, SimpleType.INT, SimpleType.INT), - CelOverloadDecl.newGlobalOverload( - "modulo_uint64", "arithmetic", SimpleType.UINT, SimpleType.UINT, SimpleType.UINT))); - celFunctionDeclBuilder.add( - CelFunctionDecl.newBuilder() - .setName(Operator.ADD.getFunction()) - .addOverloads(sameAs(commonArithmetic, "common", "add")) - .addOverloads( - CelOverloadDecl.newGlobalOverload( - "add_string", - "string concatenation", - SimpleType.STRING, - SimpleType.STRING, - SimpleType.STRING), - CelOverloadDecl.newGlobalOverload( - "add_bytes", - "bytes concatenation", - SimpleType.BYTES, - SimpleType.BYTES, - SimpleType.BYTES), - CelOverloadDecl.newGlobalOverload( - "add_list", "list concatenation", listOfA, listOfA, listOfA), - CelOverloadDecl.newGlobalOverload( - "add_timestamp_duration", - "arithmetic", - SimpleType.TIMESTAMP, - SimpleType.TIMESTAMP, - SimpleType.DURATION), - CelOverloadDecl.newGlobalOverload( - "add_duration_timestamp", - "arithmetic", - SimpleType.TIMESTAMP, - SimpleType.DURATION, - SimpleType.TIMESTAMP), - CelOverloadDecl.newGlobalOverload( - "add_duration_duration", - "arithmetic", - SimpleType.DURATION, - SimpleType.DURATION, - SimpleType.DURATION)) - .build()); - celFunctionDeclBuilder.add( - CelFunctionDecl.newFunctionDeclaration( - Operator.NEGATE.getFunction(), - CelOverloadDecl.newGlobalOverload( - "negate_int64", "negation", SimpleType.INT, SimpleType.INT), - CelOverloadDecl.newGlobalOverload( - "negate_double", "negation", SimpleType.DOUBLE, SimpleType.DOUBLE))); - - // Index - celFunctionDeclBuilder.add( - CelFunctionDecl.newFunctionDeclaration( - Operator.INDEX.getFunction(), - CelOverloadDecl.newGlobalOverload( - "index_list", "list indexing", typeParamA, listOfA, SimpleType.INT), - CelOverloadDecl.newGlobalOverload( - "index_map", "map indexing", typeParamB, mapOfAb, typeParamA))); - - // Collections - celFunctionDeclBuilder.add( - CelFunctionDecl.newFunctionDeclaration( - "size", - CelOverloadDecl.newGlobalOverload( - "size_string", "string length", SimpleType.INT, SimpleType.STRING), - CelOverloadDecl.newGlobalOverload( - "size_bytes", "bytes length", SimpleType.INT, SimpleType.BYTES), - CelOverloadDecl.newGlobalOverload("size_list", "list size", SimpleType.INT, listOfA), - CelOverloadDecl.newGlobalOverload("size_map", "map size", SimpleType.INT, mapOfAb), - CelOverloadDecl.newMemberOverload( - "string_size", "string length", SimpleType.INT, SimpleType.STRING), - CelOverloadDecl.newMemberOverload( - "bytes_size", "bytes length", SimpleType.INT, SimpleType.BYTES), - CelOverloadDecl.newMemberOverload("list_size", "list size", SimpleType.INT, listOfA), - CelOverloadDecl.newMemberOverload("map_size", "map size", SimpleType.INT, mapOfAb))); - - // Set membership 'in' operator. - CelFunctionDecl inOperator = - CelFunctionDecl.newFunctionDeclaration( - Operator.OLD_IN.getFunction(), - CelOverloadDecl.newGlobalOverload( - "in_list", "list membership", SimpleType.BOOL, typeParamA, listOfA), - CelOverloadDecl.newGlobalOverload( - "in_map", "map key membership", SimpleType.BOOL, typeParamA, mapOfAb)); - celFunctionDeclBuilder.add(inOperator); - celFunctionDeclBuilder.add( - CelFunctionDecl.newBuilder() - .setName(Operator.IN.getFunction()) - .addOverloads(sameAs(inOperator, "", "")) - .build()); - - // Conversions to type - celFunctionDeclBuilder.add( - CelFunctionDecl.newFunctionDeclaration( - Function.TYPE.getFunction(), - CelOverloadDecl.newGlobalOverload( - "type", "returns type of value", TypeType.create(typeParamA), typeParamA))); - - // Conversions to int - celFunctionDeclBuilder.add( - CelFunctionDecl.newFunctionDeclaration( - Function.INT.getFunction(), - CelOverloadDecl.newGlobalOverload( - "uint64_to_int64", "type conversion", SimpleType.INT, SimpleType.UINT), - CelOverloadDecl.newGlobalOverload( - "double_to_int64", "type conversion", SimpleType.INT, SimpleType.DOUBLE), - CelOverloadDecl.newGlobalOverload( - "string_to_int64", "type conversion", SimpleType.INT, SimpleType.STRING), - CelOverloadDecl.newGlobalOverload( - "timestamp_to_int64", - "Convert timestamp to int64 in seconds since Unix epoch.", - SimpleType.INT, - SimpleType.TIMESTAMP))); - - // Conversions to uint - celFunctionDeclBuilder.add( - CelFunctionDecl.newFunctionDeclaration( - Function.UINT.getFunction(), - CelOverloadDecl.newGlobalOverload( - "int64_to_uint64", "type conversion", SimpleType.UINT, SimpleType.INT), - CelOverloadDecl.newGlobalOverload( - "double_to_uint64", "type conversion", SimpleType.UINT, SimpleType.DOUBLE), - CelOverloadDecl.newGlobalOverload( - "string_to_uint64", "type conversion", SimpleType.UINT, SimpleType.STRING))); - - // Conversions to double - celFunctionDeclBuilder.add( - CelFunctionDecl.newFunctionDeclaration( - Function.DOUBLE.getFunction(), - CelOverloadDecl.newGlobalOverload( - "int64_to_double", "type conversion", SimpleType.DOUBLE, SimpleType.INT), - CelOverloadDecl.newGlobalOverload( - "uint64_to_double", "type conversion", SimpleType.DOUBLE, SimpleType.UINT), - CelOverloadDecl.newGlobalOverload( - "string_to_double", "type conversion", SimpleType.DOUBLE, SimpleType.STRING))); - - // Conversions to string - celFunctionDeclBuilder.add( - CelFunctionDecl.newFunctionDeclaration( - Function.STRING.getFunction(), - CelOverloadDecl.newGlobalOverload( - "int64_to_string", "type conversion", SimpleType.STRING, SimpleType.INT), - CelOverloadDecl.newGlobalOverload( - "uint64_to_string", "type conversion", SimpleType.STRING, SimpleType.UINT), - CelOverloadDecl.newGlobalOverload( - "double_to_string", "type conversion", SimpleType.STRING, SimpleType.DOUBLE), - CelOverloadDecl.newGlobalOverload( - "bytes_to_string", "type conversion", SimpleType.STRING, SimpleType.BYTES), - CelOverloadDecl.newGlobalOverload( - "timestamp_to_string", "type_conversion", SimpleType.STRING, SimpleType.TIMESTAMP), - CelOverloadDecl.newGlobalOverload( - "duration_to_string", "type_conversion", SimpleType.STRING, SimpleType.DURATION))); - - // Conversions to list - celFunctionDeclBuilder.add( - CelFunctionDecl.newFunctionDeclaration( - Function.LIST.getFunction(), - CelOverloadDecl.newGlobalOverload( - "to_list", "type conversion", listOfA, TypeType.create(typeParamA), listOfA))); - - // Conversions to map - celFunctionDeclBuilder.add( - CelFunctionDecl.newFunctionDeclaration( - Function.MAP.getFunction(), - CelOverloadDecl.newGlobalOverload( - "to_map", - "type conversion", - mapOfAb, - TypeType.create(typeParamA), - TypeType.create(typeParamB), - mapOfAb))); - - // Conversions to bytes - celFunctionDeclBuilder.add( - CelFunctionDecl.newFunctionDeclaration( - Function.BYTES.getFunction(), - CelOverloadDecl.newGlobalOverload( - "string_to_bytes", "type conversion", SimpleType.BYTES, SimpleType.STRING))); - - // Conversions to dyn - celFunctionDeclBuilder.add( - CelFunctionDecl.newFunctionDeclaration( - Function.DYN.getFunction(), - CelOverloadDecl.newGlobalOverload( - "to_dyn", "type conversion", SimpleType.DYN, typeParamA))); - - // Conversions to Duration - celFunctionDeclBuilder.add( - CelFunctionDecl.newFunctionDeclaration( - Function.DURATION.getFunction(), - CelOverloadDecl.newGlobalOverload( - "string_to_duration", - "type conversion, duration should be end with \"s\", which stands for seconds", - SimpleType.DURATION, - SimpleType.STRING))); - - // String functions - celFunctionDeclBuilder.add( - CelFunctionDecl.newFunctionDeclaration( - Function.MATCHES.getFunction(), - CelOverloadDecl.newGlobalOverload( - "matches", - "matches first argument against regular expression in second argument", - SimpleType.BOOL, - SimpleType.STRING, - SimpleType.STRING))); - celFunctionDeclBuilder.add( - CelFunctionDecl.newFunctionDeclaration( - Function.MATCHES.getFunction(), - CelOverloadDecl.newMemberOverload( - "matches_string", - "matches the self argument against regular expression in first argument", - SimpleType.BOOL, - SimpleType.STRING, - SimpleType.STRING))); - - celFunctionDeclBuilder.add( - CelFunctionDecl.newFunctionDeclaration( - Function.CONTAINS.getFunction(), - CelOverloadDecl.newMemberOverload( - "contains_string", - "tests whether the string operand contains the substring", - SimpleType.BOOL, - SimpleType.STRING, - SimpleType.STRING))); - celFunctionDeclBuilder.add( - CelFunctionDecl.newFunctionDeclaration( - Function.ENDS_WITH.getFunction(), - CelOverloadDecl.newMemberOverload( - "ends_with_string", - "tests whether the string operand ends with the suffix argument", - SimpleType.BOOL, - SimpleType.STRING, - SimpleType.STRING))); - celFunctionDeclBuilder.add( - CelFunctionDecl.newFunctionDeclaration( - Function.STARTS_WITH.getFunction(), - CelOverloadDecl.newMemberOverload( - "starts_with_string", - "tests whether the string operand starts with the prefix argument", - SimpleType.BOOL, - SimpleType.STRING, - SimpleType.STRING))); - - // Date/time functions - celFunctionDeclBuilder.add( - CelFunctionDecl.newFunctionDeclaration( - Function.GET_FULL_YEAR.getFunction(), - CelOverloadDecl.newMemberOverload( - "timestamp_to_year", - "get year from the date in UTC", - SimpleType.INT, - SimpleType.TIMESTAMP), - CelOverloadDecl.newMemberOverload( - "timestamp_to_year_with_tz", - "get year from the date with timezone", - SimpleType.INT, - SimpleType.TIMESTAMP, - SimpleType.STRING))); - - celFunctionDeclBuilder.add( - CelFunctionDecl.newFunctionDeclaration( - Function.GET_MONTH.getFunction(), - CelOverloadDecl.newMemberOverload( - "timestamp_to_month", - "get month from the date in UTC, 0-11", - SimpleType.INT, - SimpleType.TIMESTAMP), - CelOverloadDecl.newMemberOverload( - "timestamp_to_month_with_tz", - "get month from the date with timezone, 0-11", - SimpleType.INT, - SimpleType.TIMESTAMP, - SimpleType.STRING))); - - celFunctionDeclBuilder.add( - CelFunctionDecl.newFunctionDeclaration( - Function.GET_DAY_OF_YEAR.getFunction(), - CelOverloadDecl.newMemberOverload( - "timestamp_to_day_of_year", - "get day of year from the date in UTC, zero-based indexing", - SimpleType.INT, - SimpleType.TIMESTAMP), - CelOverloadDecl.newMemberOverload( - "timestamp_to_day_of_year_with_tz", - "get day of year from the date with timezone, zero-based indexing", - SimpleType.INT, - SimpleType.TIMESTAMP, - SimpleType.STRING))); - - celFunctionDeclBuilder.add( - CelFunctionDecl.newFunctionDeclaration( - Function.GET_DAY_OF_MONTH.getFunction(), - CelOverloadDecl.newMemberOverload( - "timestamp_to_day_of_month", - "get day of month from the date in UTC, zero-based indexing", - SimpleType.INT, - SimpleType.TIMESTAMP), - CelOverloadDecl.newMemberOverload( - "timestamp_to_day_of_month_with_tz", - "get day of month from the date with timezone, zero-based indexing", - SimpleType.INT, - SimpleType.TIMESTAMP, - SimpleType.STRING))); - celFunctionDeclBuilder.add( - CelFunctionDecl.newFunctionDeclaration( - Function.GET_DATE.getFunction(), - CelOverloadDecl.newMemberOverload( - "timestamp_to_day_of_month_1_based", - "get day of month from the date in UTC, one-based indexing", - SimpleType.INT, - SimpleType.TIMESTAMP), - CelOverloadDecl.newMemberOverload( - "timestamp_to_day_of_month_1_based_with_tz", - "get day of month from the date with timezone, one-based indexing", - SimpleType.INT, - SimpleType.TIMESTAMP, - SimpleType.STRING))); - - celFunctionDeclBuilder.add( - CelFunctionDecl.newFunctionDeclaration( - Function.GET_DAY_OF_WEEK.getFunction(), - CelOverloadDecl.newMemberOverload( - "timestamp_to_day_of_week", - "get day of week from the date in UTC, zero-based, zero for Sunday", - SimpleType.INT, - SimpleType.TIMESTAMP), - CelOverloadDecl.newMemberOverload( - "timestamp_to_day_of_week_with_tz", - "get day of week from the date with timezone, zero-based, zero for Sunday", - SimpleType.INT, - SimpleType.TIMESTAMP, - SimpleType.STRING))); - - celFunctionDeclBuilder.add( - CelFunctionDecl.newFunctionDeclaration( - Function.GET_HOURS.getFunction(), - CelOverloadDecl.newMemberOverload( - "timestamp_to_hours", - "get hours from the date in UTC, 0-23", - SimpleType.INT, - SimpleType.TIMESTAMP), - CelOverloadDecl.newMemberOverload( - "timestamp_to_hours_with_tz", - "get hours from the date with timezone, 0-23", - SimpleType.INT, - SimpleType.TIMESTAMP, - SimpleType.STRING), - CelOverloadDecl.newMemberOverload( - "duration_to_hours", - "get hours from duration", - SimpleType.INT, - SimpleType.DURATION))); - - celFunctionDeclBuilder.add( - CelFunctionDecl.newFunctionDeclaration( - Function.GET_MINUTES.getFunction(), - CelOverloadDecl.newMemberOverload( - "timestamp_to_minutes", - "get minutes from the date in UTC, 0-59", - SimpleType.INT, - SimpleType.TIMESTAMP), - CelOverloadDecl.newMemberOverload( - "timestamp_to_minutes_with_tz", - "get minutes from the date with timezone, 0-59", - SimpleType.INT, - SimpleType.TIMESTAMP, - SimpleType.STRING), - CelOverloadDecl.newMemberOverload( - "duration_to_minutes", - "get minutes from duration", - SimpleType.INT, - SimpleType.DURATION))); - - celFunctionDeclBuilder.add( - CelFunctionDecl.newFunctionDeclaration( - Function.GET_SECONDS.getFunction(), - CelOverloadDecl.newMemberOverload( - "timestamp_to_seconds", - "get seconds from the date in UTC, 0-59", - SimpleType.INT, - SimpleType.TIMESTAMP), - CelOverloadDecl.newMemberOverload( - "timestamp_to_seconds_with_tz", - "get seconds from the date with timezone, 0-59", - SimpleType.INT, - SimpleType.TIMESTAMP, - SimpleType.STRING), - CelOverloadDecl.newMemberOverload( - "duration_to_seconds", - "get seconds from duration", - SimpleType.INT, - SimpleType.DURATION))); - - celFunctionDeclBuilder.add( - CelFunctionDecl.newFunctionDeclaration( - Function.GET_MILLISECONDS.getFunction(), - CelOverloadDecl.newMemberOverload( - "timestamp_to_milliseconds", - "get milliseconds from the date in UTC, 0-999", - SimpleType.INT, - SimpleType.TIMESTAMP), - CelOverloadDecl.newMemberOverload( - "timestamp_to_milliseconds_with_tz", - "get milliseconds from the date with timezone, 0-999", - SimpleType.INT, - SimpleType.TIMESTAMP, - SimpleType.STRING), - CelOverloadDecl.newMemberOverload( - "duration_to_milliseconds", - "milliseconds from duration, 0-999", - SimpleType.INT, - SimpleType.DURATION))); - - return celFunctionDeclBuilder.build(); - } - - private static ImmutableList timestampConversionDeclarations(boolean withEpoch) { - CelFunctionDecl.Builder timestampBuilder = - CelFunctionDecl.newBuilder() - .setName("timestamp") - .addOverloads( - CelOverloadDecl.newGlobalOverload( - "string_to_timestamp", - "Type conversion of strings to timestamps according to RFC3339. Example:" - + " \"1972-01-01T10:00:20.021-05:00\".", - SimpleType.TIMESTAMP, - SimpleType.STRING)); - if (withEpoch) { - timestampBuilder.addOverloads( - CelOverloadDecl.newGlobalOverload( - "int64_to_timestamp", - "Type conversion of integers as Unix epoch seconds to timestamps.", - SimpleType.TIMESTAMP, - SimpleType.INT)); - } - return ImmutableList.of(timestampBuilder.build()); - } - - private static ImmutableList numericComparisonDeclarations( - boolean withHeterogeneousComparisons) { - CelFunctionDecl.Builder lessBuilder = - CelFunctionDecl.newBuilder() - .setName(Operator.LESS.getFunction()) - .addOverloads( - CelOverloadDecl.newGlobalOverload( - "less_bool", "ordering", SimpleType.BOOL, SimpleType.BOOL, SimpleType.BOOL), - CelOverloadDecl.newGlobalOverload( - "less_int64", "ordering", SimpleType.BOOL, SimpleType.INT, SimpleType.INT), - CelOverloadDecl.newGlobalOverload( - "less_uint64", "ordering", SimpleType.BOOL, SimpleType.UINT, SimpleType.UINT), - CelOverloadDecl.newGlobalOverload( - "less_double", - "ordering", - SimpleType.BOOL, - SimpleType.DOUBLE, - SimpleType.DOUBLE), - CelOverloadDecl.newGlobalOverload( - "less_string", - "ordering", - SimpleType.BOOL, - SimpleType.STRING, - SimpleType.STRING), - CelOverloadDecl.newGlobalOverload( - "less_bytes", "ordering", SimpleType.BOOL, SimpleType.BYTES, SimpleType.BYTES), - CelOverloadDecl.newGlobalOverload( - "less_timestamp", - "ordering", - SimpleType.BOOL, - SimpleType.TIMESTAMP, - SimpleType.TIMESTAMP), - CelOverloadDecl.newGlobalOverload( - "less_duration", - "ordering", - SimpleType.BOOL, - SimpleType.DURATION, - SimpleType.DURATION)); - - if (withHeterogeneousComparisons) { - lessBuilder.addOverloads( - CelOverloadDecl.newGlobalOverload( - "less_int64_uint64", - "Compare a signed integer value to an unsigned integer value", - SimpleType.BOOL, - SimpleType.INT, - SimpleType.UINT), - CelOverloadDecl.newGlobalOverload( - "less_uint64_int64", - "Compare an unsigned integer value to a signed integer value", - SimpleType.BOOL, - SimpleType.UINT, - SimpleType.INT), - CelOverloadDecl.newGlobalOverload( - "less_int64_double", - "Compare a signed integer value to a double value, coalesces the integer to a double", - SimpleType.BOOL, - SimpleType.INT, - SimpleType.DOUBLE), - CelOverloadDecl.newGlobalOverload( - "less_double_int64", - "Compare a double value to a signed integer value, coalesces the integer to a double", - SimpleType.BOOL, - SimpleType.DOUBLE, - SimpleType.INT), - CelOverloadDecl.newGlobalOverload( - "less_uint64_double", - "Compare an unsigned integer value to a double value, coalesces the unsigned integer" - + " to a double", - SimpleType.BOOL, - SimpleType.UINT, - SimpleType.DOUBLE), - CelOverloadDecl.newGlobalOverload( - "less_double_uint64", - "Compare a double value to an unsigned integer value, coalesces the unsigned integer" - + " to a double", - SimpleType.BOOL, - SimpleType.DOUBLE, - SimpleType.UINT)); - } - - CelFunctionDecl less = lessBuilder.build(); - return ImmutableList.of( - less, - CelFunctionDecl.newBuilder() - .setName(Operator.LESS_EQUALS.getFunction()) - .addOverloads(sameAs(less, "less", "less_equals")) - .build(), - CelFunctionDecl.newBuilder() - .setName(Operator.GREATER.getFunction()) - .addOverloads(sameAs(less, "less", "greater")) - .build(), - CelFunctionDecl.newBuilder() - .setName(Operator.GREATER_EQUALS.getFunction()) - .addOverloads(sameAs(less, "less", "greater_equals")) - .build()); - } - - /** - * Add the overloads of another function to this function, after replacing the overload id as - * specified. - */ - private static ImmutableList sameAs( - CelFunctionDecl func, String idPart, String idPartReplace) { - ImmutableList.Builder overloads = new ImmutableList.Builder<>(); - Preconditions.checkNotNull(func); - for (CelOverloadDecl overload : func.overloads()) { - overloads.add( - overload.toBuilder() - .setOverloadId(overload.overloadId().replace(idPart, idPartReplace)) - .build()); - } - return overloads.build(); - } - - private Standard() {} -} diff --git a/checker/src/main/java/dev/cel/checker/TypeFormatter.java b/checker/src/main/java/dev/cel/checker/TypeFormatter.java index 450518068..3cdd1a511 100644 --- a/checker/src/main/java/dev/cel/checker/TypeFormatter.java +++ b/checker/src/main/java/dev/cel/checker/TypeFormatter.java @@ -18,7 +18,7 @@ import dev.cel.common.annotations.Internal; import dev.cel.common.types.CelType; import dev.cel.common.types.CelTypes; -import org.jspecify.nullness.Nullable; +import org.jspecify.annotations.Nullable; /** * Class to format {@link Type} objects into {@code String} values. diff --git a/checker/src/main/java/dev/cel/checker/TypeProvider.java b/checker/src/main/java/dev/cel/checker/TypeProvider.java index 745789498..2dd5261ab 100644 --- a/checker/src/main/java/dev/cel/checker/TypeProvider.java +++ b/checker/src/main/java/dev/cel/checker/TypeProvider.java @@ -18,11 +18,11 @@ import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +import dev.cel.common.types.CelProtoTypes; import dev.cel.common.types.CelType; -import dev.cel.common.types.CelTypes; import java.util.Optional; import java.util.function.Function; -import org.jspecify.nullness.Nullable; +import org.jspecify.annotations.Nullable; /** * The {@code TypeProvider} defines methods to lookup types and enums, and resolve field types. @@ -38,7 +38,7 @@ public interface TypeProvider { /** Lookup the a {@link CelType} given a qualified {@code typeName}. Returns null if not found. */ default Optional lookupCelType(String typeName) { Type type = lookupType(typeName); - return Optional.ofNullable(type).map(CelTypes::typeToCelType); + return Optional.ofNullable(type).map(CelProtoTypes::typeToCelType); } /** Lookup the {@code Integer} enum value given an {@code enumName}. Returns null if not found. */ @@ -61,7 +61,7 @@ default Optional lookupCelType(String typeName) { * check is supported via the ('has') macro. */ default @Nullable FieldType lookupFieldType(CelType type, String fieldName) { - return lookupFieldType(CelTypes.celTypeToType(type), fieldName); + return lookupFieldType(CelProtoTypes.celTypeToType(type), fieldName); } /** @@ -89,7 +89,7 @@ public abstract class FieldType { public abstract Type type(); public CelType celType() { - return CelTypes.typeToCelType(type()); + return CelProtoTypes.typeToCelType(type()); } /** Create a new {@code FieldType} instance from the provided {@code type}. */ diff --git a/checker/src/main/java/dev/cel/checker/TypeProviderLegacyImpl.java b/checker/src/main/java/dev/cel/checker/TypeProviderLegacyImpl.java index 497d15698..b2ac51d95 100644 --- a/checker/src/main/java/dev/cel/checker/TypeProviderLegacyImpl.java +++ b/checker/src/main/java/dev/cel/checker/TypeProviderLegacyImpl.java @@ -18,15 +18,15 @@ import com.google.common.collect.ImmutableSet; import com.google.errorprone.annotations.CheckReturnValue; import dev.cel.common.annotations.Internal; +import dev.cel.common.types.CelProtoTypes; import dev.cel.common.types.CelType; import dev.cel.common.types.CelTypeProvider; -import dev.cel.common.types.CelTypes; import dev.cel.common.types.EnumType; import dev.cel.common.types.ProtoMessageType; import dev.cel.common.types.StructType; import dev.cel.common.types.TypeType; import java.util.Optional; -import org.jspecify.nullness.Nullable; +import org.jspecify.annotations.Nullable; /** * The {@code TypeProviderLegacyImpl} acts as a bridge between the old and new type provider APIs @@ -45,7 +45,7 @@ final class TypeProviderLegacyImpl implements TypeProvider { @Override public @Nullable Type lookupType(String typeName) { - return lookupCelType(typeName).map(CelTypes::celTypeToType).orElse(null); + return lookupCelType(typeName).map(CelProtoTypes::celTypeToType).orElse(null); } @Override @@ -65,13 +65,13 @@ public Optional lookupCelType(String typeName) { return structType .findField(fieldName) - .map(f -> FieldType.of(CelTypes.celTypeToType(f.type()))) + .map(f -> FieldType.of(CelProtoTypes.celTypeToType(f.type()))) .orElse(null); } @Override public @Nullable FieldType lookupFieldType(Type type, String fieldName) { - return lookupFieldType(CelTypes.typeToCelType(type), fieldName); + return lookupFieldType(CelProtoTypes.typeToCelType(type), fieldName); } @Override @@ -114,7 +114,8 @@ public Optional lookupCelType(String typeName) { .map( et -> ExtensionFieldType.of( - CelTypes.celTypeToType(et.type()), CelTypes.celTypeToType(et.messageType()))) + CelProtoTypes.celTypeToType(et.type()), + CelProtoTypes.celTypeToType(et.messageType()))) .orElse(null); } } diff --git a/checker/src/main/java/dev/cel/checker/Types.java b/checker/src/main/java/dev/cel/checker/Types.java index 5e54dfd6d..4cc502cdf 100644 --- a/checker/src/main/java/dev/cel/checker/Types.java +++ b/checker/src/main/java/dev/cel/checker/Types.java @@ -26,8 +26,8 @@ import com.google.protobuf.NullValue; import dev.cel.common.annotations.Internal; 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.common.types.ListType; import dev.cel.common.types.MapType; import dev.cel.common.types.NullableType; @@ -39,7 +39,7 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; -import org.jspecify.nullness.Nullable; +import org.jspecify.annotations.Nullable; /** * Utilities for dealing with the {@link Type} proto. @@ -83,7 +83,7 @@ public final class Types { public static final Type DURATION = create(WellKnownType.DURATION); /** Map of well-known proto messages and their CEL {@code Type} equivalents. */ - static final ImmutableMap WELL_KNOWN_TYPE_MAP = + public static final ImmutableMap WELL_KNOWN_TYPE_MAP = ImmutableMap.builder() .put(DOUBLE_WRAPPER_MESSAGE, Types.createWrapper(Types.DOUBLE)) .put(FLOAT_WRAPPER_MESSAGE, Types.createWrapper(Types.DOUBLE)) @@ -103,7 +103,7 @@ public final class Types { .buildOrThrow(); /** Map of primitive proto types and their CEL {@code Type} equivalents. */ - static final ImmutableMap PRIMITIVE_TYPE_MAP = + public static final ImmutableMap PRIMITIVE_TYPE_MAP = ImmutableMap.builder() .put(FieldDescriptorProto.Type.TYPE_DOUBLE, Types.DOUBLE) .put(FieldDescriptorProto.Type.TYPE_FLOAT, Types.DOUBLE) @@ -177,7 +177,7 @@ public static Type createWrapper(Type type) { */ @Deprecated public static boolean isDynOrError(Type type) { - return isDynOrError(CelTypes.typeToCelType(type)); + return isDynOrError(CelProtoTypes.typeToCelType(type)); } /** Tests whether the type has error or dyn kind. Both have the property to match any type. */ @@ -238,18 +238,18 @@ public static CelType mostGeneral(CelType type1, CelType type2) { subs.entrySet().stream() .collect( Collectors.toMap( - k -> CelTypes.typeToCelType(k.getKey()), - v -> CelTypes.typeToCelType(v.getValue()), + k -> CelProtoTypes.typeToCelType(k.getKey()), + v -> CelProtoTypes.typeToCelType(v.getValue()), (prev, next) -> next, HashMap::new)); if (internalIsAssignable( - subsCopy, CelTypes.typeToCelType(type1), CelTypes.typeToCelType(type2))) { + subsCopy, CelProtoTypes.typeToCelType(type1), CelProtoTypes.typeToCelType(type2))) { return subsCopy.entrySet().stream() .collect( Collectors.toMap( - k -> CelTypes.celTypeToType(k.getKey()), - v -> CelTypes.celTypeToType(v.getValue()), + k -> CelProtoTypes.celTypeToType(k.getKey()), + v -> CelProtoTypes.celTypeToType(v.getValue()), (prev, next) -> next, HashMap::new)); } @@ -384,7 +384,8 @@ private static boolean isAssignableFromNull(CelType targetType) { */ @Deprecated public static boolean isEqualOrLessSpecific(Type type1, Type type2) { - return isEqualOrLessSpecific(CelTypes.typeToCelType(type1), CelTypes.typeToCelType(type2)); + return isEqualOrLessSpecific( + CelProtoTypes.typeToCelType(type1), CelProtoTypes.typeToCelType(type2)); } /** @@ -426,7 +427,7 @@ public static boolean isEqualOrLessSpecific(CelType type1, CelType type2) { TypeType typeType2 = (TypeType) type2; return isEqualOrLessSpecific(typeType1.type(), typeType2.type()); - // Message, primitive, well-known, and wrapper type names must be equal to be equivalent. + // Message, primitive, well-known, and wrapper type names must be equal to be equivalent. default: return type1.equals(type2); } @@ -493,10 +494,11 @@ private static boolean notReferencedIn( public static Type substitute(Map subs, Type type, boolean typeParamToDyn) { ImmutableMap.Builder subsMap = ImmutableMap.builder(); for (Map.Entry sub : subs.entrySet()) { - subsMap.put(CelTypes.typeToCelType(sub.getKey()), CelTypes.typeToCelType(sub.getValue())); + subsMap.put( + CelProtoTypes.typeToCelType(sub.getKey()), CelProtoTypes.typeToCelType(sub.getValue())); } - return CelTypes.celTypeToType( - substitute(subsMap.buildOrThrow(), CelTypes.typeToCelType(type), typeParamToDyn)); + return CelProtoTypes.celTypeToType( + substitute(subsMap.buildOrThrow(), CelProtoTypes.typeToCelType(type), typeParamToDyn)); } /** diff --git a/checker/src/test/java/dev/cel/checker/BUILD.bazel b/checker/src/test/java/dev/cel/checker/BUILD.bazel index 338dcbb10..22b70210d 100644 --- a/checker/src/test/java/dev/cel/checker/BUILD.bazel +++ b/checker/src/test/java/dev/cel/checker/BUILD.bazel @@ -1,8 +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", @@ -10,45 +13,49 @@ 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", "//checker:checker_legacy_environment", "//checker:proto_expr_visitor", "//checker:proto_type_mask", + "//checker:standard_decl", "//checker:type_inferencer", "//checker:type_provider_legacy_impl", - "//common", + "//common:cel_ast", + "//common:cel_source", "//common:compiler_common", + "//common:container", + "//common:mutable_ast", + "//common:operator", + "//common:options", "//common:proto_ast", + "//common:source_location", "//common/ast", "//common/internal:env_visitor", "//common/internal:errors", - "//common/resources/testdata/proto2:messages_extensions_proto2_java_proto", - "//common/resources/testdata/proto2:messages_proto2_java_proto", - "//common/resources/testdata/proto2:test_all_types_java_proto", "//common/resources/testdata/proto3:standalone_global_enum_java_proto", - "//common/resources/testdata/proto3:test_all_types_java_proto", "//common/types", - "//common/types:cel_types", + "//common/types:cel_proto_types", "//common/types:json", "//common/types:message_type_provider", "//common/types:type_providers", "//compiler", "//compiler:compiler_builder", + # "//java/com/google/testing/testsize:annotations", "//parser:macro", - "//parser:operator", "//testing:adorner", "//testing:cel_baseline_test_case", - "@maven//:com_google_errorprone_error_prone_annotations", - "@maven//:org_jspecify_jspecify", + "//:auto_value", "@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:expr_java_proto", - "@maven//:com_google_api_grpc_proto_google_common_protos", + "@cel_spec//proto/cel/expr:checked_java_proto", + "@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", + "@com_google_googleapis//google/rpc/context:attribute_context_java_proto", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", ], diff --git a/checker/src/test/java/dev/cel/checker/CelCheckerLegacyImplTest.java b/checker/src/test/java/dev/cel/checker/CelCheckerLegacyImplTest.java index 1f96367f3..92a70c2d6 100644 --- a/checker/src/test/java/dev/cel/checker/CelCheckerLegacyImplTest.java +++ b/checker/src/test/java/dev/cel/checker/CelCheckerLegacyImplTest.java @@ -16,12 +16,19 @@ import static com.google.common.truth.Truth.assertThat; +import com.google.common.collect.ImmutableList; +import dev.cel.checker.CelStandardDeclarations.StandardFunction; +import dev.cel.common.CelContainer; import dev.cel.common.CelFunctionDecl; +import dev.cel.common.CelOptions; import dev.cel.common.CelOverloadDecl; import dev.cel.common.CelVarDecl; +import dev.cel.common.types.CelType; +import dev.cel.common.types.CelTypeProvider; import dev.cel.common.types.SimpleType; import dev.cel.compiler.CelCompilerFactory; -import dev.cel.testing.testdata.proto2.TestAllTypesProto.TestAllTypes; +import dev.cel.expr.conformance.proto3.TestAllTypes; +import java.util.Optional; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -49,7 +56,45 @@ public void toCheckerBuilder_isImmutable() { CelCheckerLegacyImpl.Builder newCheckerBuilder = (CelCheckerLegacyImpl.Builder) celChecker.toCheckerBuilder(); - assertThat(newCheckerBuilder.getCheckerLibraries().build()).isEmpty(); + assertThat(newCheckerBuilder.checkerLibraries().build()).isEmpty(); + } + + @Test + public void toCheckerBuilder_singularFields_copied() { + CelStandardDeclarations subsetDecls = + CelStandardDeclarations.newBuilder().includeFunctions(StandardFunction.BOOL).build(); + CelOptions celOptions = CelOptions.current().build(); + CelContainer celContainer = CelContainer.ofName("foo"); + CelType expectedResultType = SimpleType.BOOL; + CelTypeProvider customTypeProvider = + new CelTypeProvider() { + @Override + public ImmutableList types() { + return ImmutableList.of(); + } + + @Override + public Optional findType(String typeName) { + return Optional.empty(); + } + }; + CelCheckerBuilder celCheckerBuilder = + CelCompilerFactory.standardCelCheckerBuilder() + .setOptions(celOptions) + .setContainer(celContainer) + .setResultType(expectedResultType) + .setTypeProvider(customTypeProvider) + .setStandardEnvironmentEnabled(false) + .setStandardDeclarations(subsetDecls); + CelCheckerLegacyImpl celChecker = (CelCheckerLegacyImpl) celCheckerBuilder.build(); + + CelCheckerLegacyImpl.Builder newCheckerBuilder = + (CelCheckerLegacyImpl.Builder) celChecker.toCheckerBuilder(); + + assertThat(newCheckerBuilder.standardDeclarations()).isEqualTo(subsetDecls); + assertThat(newCheckerBuilder.options()).isEqualTo(celOptions); + assertThat(newCheckerBuilder.container()).isEqualTo(celContainer); + assertThat(newCheckerBuilder.celTypeProvider()).isEqualTo(customTypeProvider); } @Test @@ -63,19 +108,19 @@ public void toCheckerBuilder_collectionProperties_copied() { .addMessageTypes(TestAllTypes.getDescriptor()) .addFileTypes(TestAllTypes.getDescriptor().getFile()) .addProtoTypeMasks( - ProtoTypeMask.ofAllFields("dev.cel.testing.testdata.proto2.TestAllTypes")) + ProtoTypeMask.ofAllFields("cel.expr.conformance.proto3.TestAllTypes")) .addLibraries(new CelCheckerLibrary() {}); CelCheckerLegacyImpl celChecker = (CelCheckerLegacyImpl) celCheckerBuilder.build(); CelCheckerLegacyImpl.Builder newCheckerBuilder = (CelCheckerLegacyImpl.Builder) celChecker.toCheckerBuilder(); - assertThat(newCheckerBuilder.getFunctionDecls().build()).hasSize(1); - assertThat(newCheckerBuilder.getIdentDecls().build()).hasSize(1); - assertThat(newCheckerBuilder.getProtoTypeMasks().build()).hasSize(1); - assertThat(newCheckerBuilder.getMessageTypes().build()).hasSize(1); - assertThat(newCheckerBuilder.getFileTypes().build()).hasSize(1); - assertThat(newCheckerBuilder.getCheckerLibraries().build()).hasSize(1); + assertThat(newCheckerBuilder.functionDecls().build()).hasSize(1); + assertThat(newCheckerBuilder.identDecls().build()).hasSize(1); + assertThat(newCheckerBuilder.protoTypeMasks().build()).hasSize(1); + assertThat(newCheckerBuilder.fileTypes().build()) + .hasSize(1); // MessageTypes and FileTypes deduped into the same file descriptor + assertThat(newCheckerBuilder.checkerLibraries().build()).hasSize(1); } @Test @@ -93,14 +138,14 @@ public void toCheckerBuilder_collectionProperties_areImmutable() { celCheckerBuilder.addMessageTypes(TestAllTypes.getDescriptor()); celCheckerBuilder.addFileTypes(TestAllTypes.getDescriptor().getFile()); celCheckerBuilder.addProtoTypeMasks( - ProtoTypeMask.ofAllFields("dev.cel.testing.testdata.proto2.TestAllTypes")); + ProtoTypeMask.ofAllFields("cel.expr.conformance.proto3.TestAllTypes")); celCheckerBuilder.addLibraries(new CelCheckerLibrary() {}); - assertThat(newCheckerBuilder.getFunctionDecls().build()).isEmpty(); - assertThat(newCheckerBuilder.getIdentDecls().build()).isEmpty(); - assertThat(newCheckerBuilder.getProtoTypeMasks().build()).isEmpty(); - assertThat(newCheckerBuilder.getMessageTypes().build()).isEmpty(); - assertThat(newCheckerBuilder.getFileTypes().build()).isEmpty(); - assertThat(newCheckerBuilder.getCheckerLibraries().build()).isEmpty(); + assertThat(newCheckerBuilder.functionDecls().build()).isEmpty(); + assertThat(newCheckerBuilder.identDecls().build()).isEmpty(); + assertThat(newCheckerBuilder.protoTypeMasks().build()).isEmpty(); + assertThat(newCheckerBuilder.messageTypes().build()).isEmpty(); + assertThat(newCheckerBuilder.fileTypes().build()).isEmpty(); + assertThat(newCheckerBuilder.checkerLibraries().build()).isEmpty(); } } diff --git a/checker/src/test/java/dev/cel/checker/CelIssueTest.java b/checker/src/test/java/dev/cel/checker/CelIssueTest.java index e0d34fd0c..a430ab2db 100644 --- a/checker/src/test/java/dev/cel/checker/CelIssueTest.java +++ b/checker/src/test/java/dev/cel/checker/CelIssueTest.java @@ -16,11 +16,11 @@ import static com.google.common.truth.Truth.assertThat; -import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; -import com.google.common.collect.Iterables; import dev.cel.common.CelIssue; +import dev.cel.common.CelIssue.Severity; import dev.cel.common.CelSource; +import dev.cel.common.CelSourceLocation; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -28,7 +28,16 @@ @RunWith(JUnit4.class) public final class CelIssueTest { - private static final Joiner JOINER = Joiner.on('\n'); + @Test + public void formatError_withExprId() { + CelIssue celIssue = CelIssue.formatError(1L, CelSourceLocation.of(2, 3), "Error message"); + + assertThat(celIssue.getExprId()).isEqualTo(1L); + assertThat(celIssue.getSourceLocation().getLine()).isEqualTo(2); + assertThat(celIssue.getSourceLocation().getColumn()).isEqualTo(3); + assertThat(celIssue.getSeverity()).isEqualTo(Severity.ERROR); + assertThat(celIssue.getMessage()).isEqualTo("Error message"); + } @Test public void toDisplayString_narrow() throws Exception { @@ -38,7 +47,7 @@ public void toDisplayString_narrow() throws Exception { ImmutableList.of( CelIssue.formatError(1, 1, "No such field"), CelIssue.formatError(2, 20, "Syntax error, missing paren")); - assertThat(JOINER.join(Iterables.transform(issues, error -> error.toDisplayString(source)))) + assertThat(CelIssue.toDisplayString(issues, source)) .isEqualTo( "ERROR: issues-test:1:2: No such field\n" + " | a.b\n" @@ -53,7 +62,7 @@ public void toDisplayString_wideAndNarrow() throws Exception { CelSource source = CelSource.newBuilder("你好吗\n我b很好\n").setDescription("issues-test").build(); ImmutableList issues = ImmutableList.of(CelIssue.formatError(2, 3, "Unexpected character '好'")); - assertThat(JOINER.join(Iterables.transform(issues, error -> error.toDisplayString(source)))) + assertThat(CelIssue.toDisplayString(issues, source)) .isEqualTo("ERROR: issues-test:2:4: Unexpected character '好'\n" + " | 我b很好\n" + " | ...^"); } @@ -73,7 +82,8 @@ public void toDisplayString_emojis() throws Exception { + " IDENTIFIER}"), CelIssue.formatError(1, 35, "Syntax error: token recognition error at: '😁'"), CelIssue.formatError(1, 36, "Syntax error: missing IDENTIFIER at ''")); - assertThat(JOINER.join(Iterables.transform(issues, error -> error.toDisplayString(source)))) + + assertThat(CelIssue.toDisplayString(issues, source)) .isEqualTo( "ERROR: issues-test:1:33: Syntax error: extraneous input 'in' expecting {'[', '{'," + " '(', '.', '-', '!', 'true', 'false', 'null', NUM_FLOAT, NUM_INT, NUM_UINT," diff --git a/checker/src/test/java/dev/cel/checker/CelOverloadDeclTest.java b/checker/src/test/java/dev/cel/checker/CelOverloadDeclTest.java index 41a11d045..0dd7d83df 100644 --- a/checker/src/test/java/dev/cel/checker/CelOverloadDeclTest.java +++ b/checker/src/test/java/dev/cel/checker/CelOverloadDeclTest.java @@ -20,8 +20,9 @@ import static dev.cel.common.CelOverloadDecl.newMemberOverload; import dev.cel.expr.Decl.FunctionDecl.Overload; +import com.google.common.collect.ImmutableList; import dev.cel.common.CelOverloadDecl; -import dev.cel.common.types.CelTypes; +import dev.cel.common.types.CelProtoTypes; import dev.cel.common.types.SimpleType; import dev.cel.common.types.TypeParamType; import org.junit.Test; @@ -80,9 +81,27 @@ public void toProtoOverload_withTypeParams() { Overload protoOverload = CelOverloadDecl.celOverloadToOverload(celOverloadDecl); assertThat(protoOverload.getOverloadId()).isEqualTo("overloadId"); assertThat(protoOverload.getIsInstanceFunction()).isTrue(); - assertThat(protoOverload.getResultType()).isEqualTo(CelTypes.createTypeParam("A")); + assertThat(protoOverload.getResultType()).isEqualTo(CelProtoTypes.createTypeParam("A")); assertThat(protoOverload.getParamsList()) - .containsExactly(CelTypes.STRING, CelTypes.DOUBLE, CelTypes.createTypeParam("B")); + .containsExactly( + CelProtoTypes.STRING, CelProtoTypes.DOUBLE, CelProtoTypes.createTypeParam("B")); assertThat(protoOverload.getTypeParamsList()).containsExactly("A", "B"); } + + @Test + public void setParameterTypes_doesNotDedupe() { + CelOverloadDecl overloadDecl = + CelOverloadDecl.newBuilder() + .setParameterTypes( + ImmutableList.of( + SimpleType.STRING, SimpleType.STRING, SimpleType.STRING, SimpleType.INT)) + .setOverloadId("overload_id") + .setIsInstanceFunction(true) + .setResultType(SimpleType.DYN) + .build(); + + assertThat(overloadDecl.parameterTypes()) + .containsExactly(SimpleType.STRING, SimpleType.STRING, SimpleType.STRING, SimpleType.INT) + .inOrder(); + } } diff --git a/checker/src/test/java/dev/cel/checker/CelProtoExprVisitorTest.java b/checker/src/test/java/dev/cel/checker/CelProtoExprVisitorTest.java index 54972b931..3cab304ac 100644 --- a/checker/src/test/java/dev/cel/checker/CelProtoExprVisitorTest.java +++ b/checker/src/test/java/dev/cel/checker/CelProtoExprVisitorTest.java @@ -21,12 +21,13 @@ import dev.cel.expr.Expr; import com.google.auto.value.AutoValue; import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelContainer; +import dev.cel.common.Operator; 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.parser.CelStandardMacro; -import dev.cel.parser.Operator; -import dev.cel.testing.testdata.proto3.TestAllTypesProto.TestAllTypes; import java.util.Optional; import org.junit.Before; import org.junit.Test; @@ -163,7 +164,7 @@ public void visitSelect() throws Exception { CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder() .addMessageTypes(TestAllTypes.getDescriptor()) - .setContainer(TestAllTypes.getDescriptor().getFullName()) + .setContainer(CelContainer.ofName(TestAllTypes.getDescriptor().getFullName())) .build(); CelAbstractSyntaxTree ast = celCompiler.compile("TestAllTypes{}.single_int64").getAst(); @@ -174,7 +175,9 @@ public void visitSelect() throws Exception { .isEqualTo( VisitedReference.newBuilder() .setCreateStruct( - Expr.CreateStruct.newBuilder().setMessageName("TestAllTypes").build()) + Expr.CreateStruct.newBuilder() + .setMessageName("cel.expr.conformance.proto3.TestAllTypes") + .build()) .setSelect( Expr.Select.newBuilder() .setOperand( @@ -182,7 +185,7 @@ public void visitSelect() throws Exception { .setId(1) .setStructExpr( Expr.CreateStruct.newBuilder() - .setMessageName("TestAllTypes") + .setMessageName("cel.expr.conformance.proto3.TestAllTypes") .build())) .setField("single_int64") .build()) @@ -215,7 +218,7 @@ public void visitCreateStruct() throws Exception { CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder() .addMessageTypes(TestAllTypes.getDescriptor()) - .setContainer(TestAllTypes.getDescriptor().getFullName()) + .setContainer(CelContainer.ofName(TestAllTypes.getDescriptor().getFullName())) .build(); CelAbstractSyntaxTree ast = celCompiler.compile("TestAllTypes{}").getAst(); @@ -226,7 +229,9 @@ public void visitCreateStruct() throws Exception { .isEqualTo( VisitedReference.newBuilder() .setCreateStruct( - Expr.CreateStruct.newBuilder().setMessageName("TestAllTypes").build()) + Expr.CreateStruct.newBuilder() + .setMessageName("cel.expr.conformance.proto3.TestAllTypes") + .build()) .build()); } diff --git a/checker/src/test/java/dev/cel/checker/CelStandardDeclarationsTest.java b/checker/src/test/java/dev/cel/checker/CelStandardDeclarationsTest.java new file mode 100644 index 000000000..17a7212a1 --- /dev/null +++ b/checker/src/test/java/dev/cel/checker/CelStandardDeclarationsTest.java @@ -0,0 +1,275 @@ +// 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. + +package dev.cel.checker; + +import static com.google.common.truth.Truth.assertThat; +import static dev.cel.common.CelFunctionDecl.newFunctionDeclaration; +import static org.junit.Assert.assertThrows; + +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import com.google.testing.junit.testparameterinjector.TestParameters; +import dev.cel.checker.CelStandardDeclarations.StandardFunction; +import dev.cel.checker.CelStandardDeclarations.StandardFunction.Overload.Arithmetic; +import dev.cel.checker.CelStandardDeclarations.StandardIdentifier; +import dev.cel.common.CelOptions; +import dev.cel.common.CelValidationException; +import dev.cel.compiler.CelCompiler; +import dev.cel.compiler.CelCompilerFactory; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public class CelStandardDeclarationsTest { + + @Test + @TestParameters("{includeFunction: true, excludeFunction: true, filterFunction: true}") + @TestParameters("{includeFunction: true, excludeFunction: true, filterFunction: false}") + @TestParameters("{includeFunction: true, excludeFunction: false, filterFunction: true}") + @TestParameters("{includeFunction: false, excludeFunction: true, filterFunction: true}") + public void standardDeclaration_moreThanOneFunctionFilterSet_throws( + boolean includeFunction, boolean excludeFunction, boolean filterFunction) { + CelStandardDeclarations.Builder builder = CelStandardDeclarations.newBuilder(); + if (includeFunction) { + builder.includeFunctions(StandardFunction.ADD); + } + if (excludeFunction) { + builder.excludeFunctions(StandardFunction.SUBTRACT); + } + if (filterFunction) { + builder.filterFunctions((func, over) -> true); + } + + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, builder::build); + assertThat(e) + .hasMessageThat() + .contains( + "You may only populate one of the following builder methods: includeFunctions," + + " excludeFunctions or filterFunctions"); + } + + @Test + @TestParameters("{includeIdentifier: true, excludeIdentifier: true, filterIdentifier: true}") + @TestParameters("{includeIdentifier: true, excludeIdentifier: true, filterIdentifier: false}") + @TestParameters("{includeIdentifier: true, excludeIdentifier: false, filterIdentifier: true}") + @TestParameters("{includeIdentifier: false, excludeIdentifier: true, filterIdentifier: true}") + public void standardDeclaration_moreThanOneIdentifierFilterSet_throws( + boolean includeIdentifier, boolean excludeIdentifier, boolean filterIdentifier) { + CelStandardDeclarations.Builder builder = CelStandardDeclarations.newBuilder(); + if (includeIdentifier) { + builder.includeIdentifiers(StandardIdentifier.MAP); + } + if (excludeIdentifier) { + builder.excludeIdentifiers(StandardIdentifier.BOOL); + } + if (filterIdentifier) { + builder.filterIdentifiers((ident) -> true); + } + + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, builder::build); + assertThat(e) + .hasMessageThat() + .contains( + "You may only populate one of the following builder methods: includeIdentifiers," + + " excludeIdentifiers or filterIdentifiers"); + } + + @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()); + + assertThat(e) + .hasMessageThat() + .contains( + "setStandardEnvironmentEnabled must be set to false to override standard" + + " declarations."); + } + + @Test + public void standardDeclarations_includeFunctions() { + CelStandardDeclarations celStandardDeclaration = + CelStandardDeclarations.newBuilder() + .includeFunctions(StandardFunction.ADD, StandardFunction.SUBTRACT) + .build(); + + assertThat(celStandardDeclaration.functionDecls()) + .containsExactly( + StandardFunction.ADD.functionDecl(), StandardFunction.SUBTRACT.functionDecl()); + } + + @Test + public void standardDeclarations_excludeFunctions() { + CelStandardDeclarations celStandardDeclaration = + CelStandardDeclarations.newBuilder() + .excludeFunctions(StandardFunction.ADD, StandardFunction.SUBTRACT) + .build(); + + assertThat(celStandardDeclaration.functionDecls()) + .doesNotContain(StandardFunction.ADD.functionDecl()); + assertThat(celStandardDeclaration.functionDecls()) + .doesNotContain(StandardFunction.SUBTRACT.functionDecl()); + } + + @Test + public void standardDeclarations_filterFunctions() { + CelStandardDeclarations celStandardDeclaration = + CelStandardDeclarations.newBuilder() + .filterFunctions( + (func, over) -> { + if (func.equals(StandardFunction.ADD) && over.equals(Arithmetic.ADD_INT64)) { + return true; + } + + if (func.equals(StandardFunction.SUBTRACT) + && over.equals(Arithmetic.SUBTRACT_INT64)) { + return true; + } + + return false; + }) + .build(); + + assertThat(celStandardDeclaration.functionDecls()) + .containsExactly( + newFunctionDeclaration( + StandardFunction.ADD.functionName(), Arithmetic.ADD_INT64.celOverloadDecl()), + newFunctionDeclaration( + StandardFunction.SUBTRACT.functionName(), + Arithmetic.SUBTRACT_INT64.celOverloadDecl())); + } + + @Test + public void standardDeclarations_includeIdentifiers() { + CelStandardDeclarations celStandardDeclaration = + CelStandardDeclarations.newBuilder() + .includeIdentifiers(StandardIdentifier.INT, StandardIdentifier.UINT) + .build(); + + assertThat(celStandardDeclaration.identifierDecls()) + .containsExactly(StandardIdentifier.INT.identDecl(), StandardIdentifier.UINT.identDecl()); + } + + @Test + public void standardDeclarations_excludeIdentifiers() { + CelStandardDeclarations celStandardDeclaration = + CelStandardDeclarations.newBuilder() + .excludeIdentifiers(StandardIdentifier.INT, StandardIdentifier.UINT) + .build(); + + assertThat(celStandardDeclaration.identifierDecls()) + .doesNotContain(StandardIdentifier.INT.identDecl()); + assertThat(celStandardDeclaration.identifierDecls()) + .doesNotContain(StandardIdentifier.UINT.identDecl()); + } + + @Test + public void standardDeclarations_filterIdentifiers() { + CelStandardDeclarations celStandardDeclaration = + CelStandardDeclarations.newBuilder() + .filterIdentifiers(ident -> ident.equals(StandardIdentifier.MAP)) + .build(); + + assertThat(celStandardDeclaration.identifierDecls()) + .containsExactly(StandardIdentifier.MAP.identDecl()); + } + + @Test + public void standardEnvironment_subsetEnvironment() throws Exception { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .setStandardEnvironmentEnabled(false) + .setStandardDeclarations( + CelStandardDeclarations.newBuilder() + .includeFunctions(StandardFunction.ADD, StandardFunction.SUBTRACT) + .build()) + .build(); + + assertThat(celCompiler.compile("1 + 2 - 3").getAst()).isNotNull(); + CelValidationException e = + assertThrows(CelValidationException.class, () -> celCompiler.compile("1 * 2 / 3").getAst()); + assertThat(e).hasMessageThat().contains("undeclared reference to '_*_'"); + assertThat(e).hasMessageThat().contains("undeclared reference to '_/_'"); + } + + @Test + @TestParameters("{expression: '1 > 2.0'}") + @TestParameters("{expression: '2.0 > 1'}") + @TestParameters("{expression: '1 > 2u'}") + @TestParameters("{expression: '2u > 1'}") + @TestParameters("{expression: '2u > 1.0'}") + @TestParameters("{expression: '1.0 > 2u'}") + @TestParameters("{expression: '1 >= 2.0'}") + @TestParameters("{expression: '2.0 >= 1'}") + @TestParameters("{expression: '1 >= 2u'}") + @TestParameters("{expression: '2u >= 1'}") + @TestParameters("{expression: '2u >= 1.0'}") + @TestParameters("{expression: '1.0 >= 2u'}") + @TestParameters("{expression: '1 < 2.0'}") + @TestParameters("{expression: '2.0 < 1'}") + @TestParameters("{expression: '1 < 2u'}") + @TestParameters("{expression: '2u < 1'}") + @TestParameters("{expression: '2u < 1.0'}") + @TestParameters("{expression: '1.0 < 2u'}") + @TestParameters("{expression: '1 <= 2.0'}") + @TestParameters("{expression: '2.0 <= 1'}") + @TestParameters("{expression: '1 <= 2u'}") + @TestParameters("{expression: '2u <= 1'}") + @TestParameters("{expression: '2u <= 1.0'}") + @TestParameters("{expression: '1.0 <= 2u'}") + public void heterogeneousEqualityDisabled_mixedTypeComparisons_throws(String expression) { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .setOptions(CelOptions.current().enableHeterogeneousNumericComparisons(false).build()) + .build(); + + CelValidationException e = + assertThrows(CelValidationException.class, () -> celCompiler.compile(expression).getAst()); + assertThat(e).hasMessageThat().contains("found no matching overload for"); + } + + @Test + public void unsignedLongsDisabled_int64Identity_throws() { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .setOptions(CelOptions.current().enableUnsignedLongs(false).build()) + .build(); + + CelValidationException e = + assertThrows(CelValidationException.class, () -> celCompiler.compile("int(1)").getAst()); + assertThat(e).hasMessageThat().contains("found no matching overload for"); + } + + @Test + public void timestampEpochDisabled_int64Identity_throws() { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .setOptions(CelOptions.current().enableTimestampEpoch(false).build()) + .build(); + + CelValidationException e = + assertThrows( + CelValidationException.class, () -> celCompiler.compile("timestamp(10000)").getAst()); + assertThat(e).hasMessageThat().contains("found no matching overload for"); + } +} diff --git a/checker/src/test/java/dev/cel/checker/DescriptorTypeProviderTest.java b/checker/src/test/java/dev/cel/checker/DescriptorTypeProviderTest.java index d7fc84202..11366a68b 100644 --- a/checker/src/test/java/dev/cel/checker/DescriptorTypeProviderTest.java +++ b/checker/src/test/java/dev/cel/checker/DescriptorTypeProviderTest.java @@ -21,9 +21,9 @@ import com.google.rpc.context.AttributeContext; import dev.cel.checker.TypeProvider.CombinedTypeProvider; import dev.cel.checker.TypeProvider.ExtensionFieldType; -import dev.cel.common.types.CelTypes; -import dev.cel.testing.testdata.proto2.MessagesProto2Extensions; -import dev.cel.testing.testdata.proto2.Proto2Message; +import dev.cel.common.types.CelProtoTypes; +import dev.cel.expr.conformance.proto2.TestAllTypes; +import dev.cel.expr.conformance.proto2.TestAllTypesExtensions; import java.util.Arrays; import org.junit.Assert; import org.junit.Test; @@ -36,7 +36,7 @@ public final class DescriptorTypeProviderTest { @Test public void lookupFieldNames_nonMessageType() { TypeProvider typeProvider = new DescriptorTypeProvider(); - assertThat(typeProvider.lookupFieldNames(CelTypes.STRING)).isNull(); + assertThat(typeProvider.lookupFieldNames(CelProtoTypes.STRING)).isNull(); } @Test @@ -44,29 +44,21 @@ public void lookupFieldNames_undeclaredMessageType() { TypeProvider typeProvider = new DescriptorTypeProvider(); assertThat( typeProvider.lookupFieldNames( - CelTypes.createMessage("google.rpc.context.AttributeContext"))) + CelProtoTypes.createMessage("google.rpc.context.AttributeContext"))) .isNull(); } @Test public void lookupFieldNames_groupTypeField() throws Exception { Type proto2MessageType = - CelTypes.createMessage("dev.cel.testing.testdata.proto2.Proto2Message"); + CelProtoTypes.createMessage("cel.expr.conformance.proto2.TestAllTypes"); TypeProvider typeProvider = new DescriptorTypeProvider( ImmutableList.of( - Proto2Message.getDescriptor().getFile(), MessagesProto2Extensions.getDescriptor())); - assertThat(typeProvider.lookupFieldNames(proto2MessageType)) - .containsExactly( - "single_nested_test_all_types", - "single_enum", - "nestedgroup", - "single_int32", - "single_fixed32", - "single_fixed64"); + TestAllTypes.getDescriptor().getFile(), TestAllTypesExtensions.getDescriptor())); assertThat(typeProvider.lookupFieldType(proto2MessageType, "nestedgroup").type()) .isEqualTo( - CelTypes.createMessage("dev.cel.testing.testdata.proto2.Proto2Message.NestedGroup")); + CelProtoTypes.createMessage("cel.expr.conformance.proto2.TestAllTypes.NestedGroup")); } @Test @@ -103,43 +95,42 @@ public void lookupExtensionType_combinedProvider() { final TypeProvider configuredProvider = new DescriptorTypeProvider( ImmutableList.of( - Proto2Message.getDescriptor().getFile(), MessagesProto2Extensions.getDescriptor())); + TestAllTypes.getDescriptor().getFile(), TestAllTypesExtensions.getDescriptor())); final TypeProvider partialProvider = // The partial provider has no extension lookup. makePartialTypeProvider(configuredProvider); final TypeProvider typeProvider = new CombinedTypeProvider(ImmutableList.of(partialProvider, configuredProvider)); final Type messageType = - CelTypes.createMessage("dev.cel.testing.testdata.proto2.Proto2Message"); + CelProtoTypes.createMessage("cel.expr.conformance.proto2.TestAllTypes"); assertThat(typeProvider.lookupExtensionType("non.existent")).isNull(); ExtensionFieldType nestedExt = - typeProvider.lookupExtensionType("dev.cel.testing.testdata.proto2.nested_ext"); + typeProvider.lookupExtensionType("cel.expr.conformance.proto2.nested_ext"); assertThat(nestedExt).isNotNull(); assertThat(nestedExt.fieldType().type()).isEqualTo(messageType); assertThat(nestedExt.messageType()).isEqualTo(messageType); ExtensionFieldType int32Ext = - typeProvider.lookupExtensionType("dev.cel.testing.testdata.proto2.int32_ext"); + typeProvider.lookupExtensionType("cel.expr.conformance.proto2.int32_ext"); assertThat(int32Ext).isNotNull(); - assertThat(int32Ext.fieldType().type()).isEqualTo(CelTypes.INT64); + assertThat(int32Ext.fieldType().type()).isEqualTo(CelProtoTypes.INT64); assertThat(int32Ext.messageType()).isEqualTo(messageType); ExtensionFieldType repeatedExt = - typeProvider.lookupExtensionType( - "dev.cel.testing.testdata.proto2.repeated_string_holder_ext"); + typeProvider.lookupExtensionType("cel.expr.conformance.proto2.repeated_test_all_types"); assertThat(repeatedExt).isNotNull(); assertThat(repeatedExt.fieldType().type()) .isEqualTo( - CelTypes.createList( - CelTypes.createMessage("dev.cel.testing.testdata.proto2.StringHolder"))); + CelProtoTypes.createList( + CelProtoTypes.createMessage("cel.expr.conformance.proto2.TestAllTypes"))); assertThat(repeatedExt.messageType()).isEqualTo(messageType); // With leading dot '.'. assertThat( typeProvider.lookupExtensionType( - ".dev.cel.testing.testdata.proto2.repeated_string_holder_ext")) + ".cel.expr.conformance.proto2.repeated_test_all_types")) .isNotNull(); } diff --git a/checker/src/test/java/dev/cel/checker/ExprCheckerTest.java b/checker/src/test/java/dev/cel/checker/ExprCheckerTest.java index 9217942c1..846201d32 100644 --- a/checker/src/test/java/dev/cel/checker/ExprCheckerTest.java +++ b/checker/src/test/java/dev/cel/checker/ExprCheckerTest.java @@ -14,67 +14,68 @@ package dev.cel.checker; -import static dev.cel.common.types.CelTypes.createList; -import static dev.cel.common.types.CelTypes.createMap; -import static dev.cel.common.types.CelTypes.createMessage; -import static dev.cel.common.types.CelTypes.createOptionalType; -import static dev.cel.common.types.CelTypes.createTypeParam; -import static dev.cel.common.types.CelTypes.createWrapper; -import static dev.cel.common.types.CelTypes.format; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.collect.ImmutableList.toImmutableList; +import static dev.cel.common.types.CelProtoTypes.format; import dev.cel.expr.CheckedExpr; -import dev.cel.expr.Constant; +import dev.cel.expr.Decl; import dev.cel.expr.Expr.CreateStruct.EntryOrBuilder; import dev.cel.expr.ExprOrBuilder; -import dev.cel.expr.ParsedExpr; import dev.cel.expr.Reference; -import dev.cel.expr.Type; -import dev.cel.expr.Type.AbstractType; -import dev.cel.expr.Type.PrimitiveType; import com.google.common.base.Joiner; import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; import com.google.protobuf.DescriptorProtos.FileDescriptorSet; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; // import com.google.testing.testsize.MediumTest; import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelContainer; +import dev.cel.common.CelFunctionDecl; +import dev.cel.common.CelMutableAst; +import dev.cel.common.CelOverloadDecl; import dev.cel.common.CelProtoAbstractSyntaxTree; +import dev.cel.common.CelVarDecl; +import dev.cel.common.ast.CelConstant; import dev.cel.common.internal.EnvVisitable; +import dev.cel.common.internal.EnvVisitor; import dev.cel.common.internal.Errors; -import dev.cel.common.types.CelTypes; +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.OpaqueType; +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.common.types.TypeParamType; +import dev.cel.common.types.TypeType; +import dev.cel.expr.conformance.proto3.TestAllTypes; +import dev.cel.parser.CelMacro; import dev.cel.testing.CelAdorner; import dev.cel.testing.CelBaselineTestCase; import dev.cel.testing.CelDebug; -import dev.cel.testing.testdata.proto2.Proto2Message; import dev.cel.testing.testdata.proto3.StandaloneGlobalEnum; -import dev.cel.testing.testdata.proto3.TestAllTypesProto.TestAllTypes; import java.util.Arrays; +import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; /** Tests for the CEL {@link ExprChecker}. */ // @MediumTest -@RunWith(Parameterized.class) +@RunWith(TestParameterInjector.class) public class ExprCheckerTest extends CelBaselineTestCase { - @Parameters() - public static ImmutableList evalTestCases() { - return ImmutableList.copyOf(TestCase.values()); - } - - public ExprCheckerTest(TestCase testCase) { - super(testCase.declareWithCelType); - } - /** Helper to run a test for configured instance variables. */ private void runTest() throws Exception { CelAbstractSyntaxTree ast = - prepareTest(Arrays.asList(TestAllTypes.getDescriptor(), Proto2Message.getDescriptor())); + prepareTest( + Arrays.asList( + StandaloneGlobalEnum.getDescriptor().getFile(), + TestAllTypes.getDescriptor().getFile(), + dev.cel.expr.conformance.proto2.TestAllTypes.getDescriptor().getFile())); if (ast != null) { testOutput() .println( @@ -87,10 +88,11 @@ private void runTest() throws Exception { } @SuppressWarnings("CheckReturnValue") - private void runErroneousTest(ParsedExpr parsedExpr) { + private void runErroneousTest(CelAbstractSyntaxTree parsedAst) { + checkArgument(!parsedAst.isChecked()); Errors errors = new Errors("", source); Env env = Env.unconfigured(errors, TEST_OPTIONS); - ExprChecker.typecheck(env, container, parsedExpr, Optional.absent()); + ExprChecker.typecheck(env, container, parsedAst, Optional.absent()); testOutput().println(errors.getAllErrorsAsString()); testOutput().println(); } @@ -106,7 +108,35 @@ public void standardEnvDump() throws Exception { testOutput().println("Standard environment:"); ((EnvVisitable) celCompiler) - .accept((name, decls) -> testOutput().println(formatDecl(name, decls))); + .accept( + new EnvVisitor() { + @Override + public void visitDecl(String name, List decls) { + // TODO: Remove proto to native type adaptation after changing + // interface + for (Decl decl : decls) { + if (decl.hasFunction()) { + CelFunctionDecl celFunctionDecl = + CelFunctionDecl.newFunctionDeclaration( + decl.getName(), + decl.getFunction().getOverloadsList().stream() + .map(CelOverloadDecl::overloadToCelOverload) + .collect(toImmutableList())); + testOutput().println(formatFunctionDecl(celFunctionDecl)); + } else if (decl.hasIdent()) { + CelVarDecl celVarDecl = + CelVarDecl.newVarDeclaration( + decl.getName(), CelProtoTypes.typeToCelType(decl.getIdent().getType())); + testOutput().println(formatVarDecl(celVarDecl)); + } else { + throw new IllegalArgumentException("Invalid declaration: " + decl); + } + } + } + + @Override + public void visitMacro(CelMacro macro) {} + }); } // Operators @@ -150,7 +180,7 @@ public void operatorsBytes() throws Exception { @Test public void operatorsConditional() throws Exception { - declareVariable("x", createMessage("dev.cel.testing.testdata.proto3.TestAllTypes")); + declareVariable("x", StructTypeReference.create("cel.expr.conformance.proto3.TestAllTypes")); source = "false ? x.single_timestamp : null"; runTest(); } @@ -161,21 +191,26 @@ public void operatorsConditional() throws Exception { @Test public void referenceTypeRelative() throws Exception { source = "proto3.TestAllTypes"; - container = "dev.cel.testing.testdata"; + container = CelContainer.ofName("cel.expr.conformance.TestAllTypes"); runTest(); } @Test public void referenceTypeAbsolute() throws Exception { - source = ".dev.cel.testing.testdata.proto3.TestAllTypes"; + source = ".cel.expr.conformance.proto3.TestAllTypes"; + runTest(); + + declareVariable("app.config", SimpleType.INT); + source = "[0].exists(app, .app.config == 1)"; runTest(); } @Test public void referenceValue() throws Exception { - declareVariable("container.x", createMessage("dev.cel.testing.testdata.proto3.TestAllTypes")); + declareVariable( + "container.x", StructTypeReference.create("cel.expr.conformance.proto3.TestAllTypes")); source = "x"; - container = "container"; + container = CelContainer.ofName("container"); runTest(); } @@ -190,28 +225,82 @@ public void referenceUndefinedError() throws Exception { @Test public void anyMessage() throws Exception { - declareVariable("x", CelTypes.ANY); - declareVariable("y", createWrapper(PrimitiveType.INT64)); + declareVariable("x", SimpleType.ANY); + declareVariable("y", NullableType.create(SimpleType.INT)); source = "x == google.protobuf.Any{" - + "type_url:'types.googleapis.com/dev.cel.testing.testdata.proto3.TestAllTypes'}" + + "type_url:'types.googleapis.com/cel.expr.conformance.proto3.TestAllTypes'}" + " && x.single_nested_message.bb == 43 || x ==" - + " dev.cel.testing.testdata.proto3.TestAllTypes{} || y < x|| x >= x"; + + " cel.expr.conformance.proto3.TestAllTypes{} || y < x|| x >= x"; runTest(); } @Test public void messageFieldSelect() throws Exception { - declareVariable("x", createMessage("dev.cel.testing.testdata.proto3.TestAllTypes")); + declareVariable("x", StructTypeReference.create("cel.expr.conformance.proto3.TestAllTypes")); source = "x.single_nested_message.bb == 43 && has(x.single_nested_message) && has(x.single_int32)" + " && has(x.repeated_int32) && has(x.map_int64_nested_type)"; runTest(); } + @Test + public void containers() throws Exception { + container = + CelContainer.newBuilder() + .setName("dev.cel.testing.testdata.proto3.StandaloneGlobalEnum") + .addAlias("p3_alias", "cel.expr.conformance.proto3") + .addAlias("foo_bar_alias", "foo.bar") + .addAlias("foo_bar_baz_alias", "foo.bar.baz") + .addAbbreviations("cel.expr.conformance.proto2", "cel.expr.conformance.proto3") + .build(); + source = "p3_alias.TestAllTypes{}"; + runTest(); + + source = "proto2.TestAllTypes{}"; + runTest(); + + source = "proto3.TestAllTypes{}"; + runTest(); + + source = "SGAR"; // From StandaloneGlobalEnum + runTest(); + + declareVariable("foo.bar", SimpleType.STRING); + declareFunction( + "baz", + memberOverload( + "foo_bar_baz_overload", ImmutableList.of(SimpleType.STRING), SimpleType.DYN)); + // Member call of "baz()" on "foo.bar" identifier + source = "foo_bar_alias.baz()"; + runTest(); + + declareFunction( + "foo.bar.baz.qux", + globalOverload("foo_bar_baz_qux_overload", ImmutableList.of(), SimpleType.DYN)); + // Global call of "foo.bar.baz.qux" as a fully qualified name + source = "foo_bar_baz_alias.qux()"; + runTest(); + } + + @Test + public void messageCreationError() throws Exception { + declareVariable("x", SimpleType.INT); + source = "x{foo: 1}"; + runTest(); + + declareVariable("y", TypeType.create(SimpleType.INT)); + source = "y{foo: 1}"; + runTest(); + + declareVariable("z", TypeType.create(StructTypeReference.create("msg_without_descriptor"))); + source = "z{foo: 1}"; + runTest(); + } + @Test public void messageFieldSelectError() throws Exception { - declareVariable("x", createMessage("dev.cel.testing.testdata.proto3.TestAllTypes")); + declareVariable("x", StructTypeReference.create("cel.expr.conformance.proto3.TestAllTypes")); source = "x.single_nested_message.undefined == x.undefined"; runTest(); } @@ -221,7 +310,9 @@ public void messageFieldSelectError() throws Exception { @Test public void listOperators() throws Exception { - declareVariable("x", createList(createMessage("dev.cel.testing.testdata.proto3.TestAllTypes"))); + declareVariable( + "x", + ListType.create(StructTypeReference.create("cel.expr.conformance.proto3.TestAllTypes"))); source = "(x + x)[1].single_int32 == size(x)"; runTest(); @@ -231,14 +322,16 @@ public void listOperators() throws Exception { @Test public void listRepeatedOperators() throws Exception { - declareVariable("x", createMessage("dev.cel.testing.testdata.proto3.TestAllTypes")); + declareVariable("x", StructTypeReference.create("cel.expr.conformance.proto3.TestAllTypes")); source = "x.repeated_int64[x.single_int32] == 23"; runTest(); } @Test public void listIndexTypeError() throws Exception { - declareVariable("x", createList(createMessage("dev.cel.testing.testdata.proto3.TestAllTypes"))); + declareVariable( + "x", + ListType.create(StructTypeReference.create("cel.expr.conformance.proto3.TestAllTypes"))); source = "x[1u]"; runTest(); } @@ -251,8 +344,10 @@ public void identError() throws Exception { @Test public void listElemTypeError() throws Exception { - declareVariable("x", createList(createMessage("dev.cel.testing.testdata.proto3.TestAllTypes"))); - declareVariable("y", createList(CelTypes.INT64)); + declareVariable( + "x", + ListType.create(StructTypeReference.create("cel.expr.conformance.proto3.TestAllTypes"))); + declareVariable("y", ListType.create(SimpleType.INT)); source = "x + y"; runTest(); } @@ -264,7 +359,9 @@ public void listElemTypeError() throws Exception { public void mapOperators() throws Exception { declareVariable( "x", - createMap(CelTypes.STRING, createMessage("dev.cel.testing.testdata.proto3.TestAllTypes"))); + MapType.create( + SimpleType.STRING, + StructTypeReference.create("cel.expr.conformance.proto3.TestAllTypes"))); source = "x[\"a\"].single_int32 == 23"; runTest(); @@ -276,14 +373,16 @@ public void mapOperators() throws Exception { public void mapIndexTypeError() throws Exception { declareVariable( "x", - createMap(CelTypes.STRING, createMessage("dev.cel.testing.testdata.proto3.TestAllTypes"))); + MapType.create( + SimpleType.STRING, + StructTypeReference.create("cel.expr.conformance.proto3.TestAllTypes"))); source = "x[2].single_int32 == 23"; runTest(); } @Test public void mapEmpty() throws Exception { - declareVariable("x", createMessage("dev.cel.testing.testdata.proto3.TestAllTypes")); + declareVariable("x", StructTypeReference.create("cel.expr.conformance.proto3.TestAllTypes")); source = "size(x.map_int64_nested_type) == 0"; runTest(); } @@ -293,14 +392,14 @@ public void mapEmpty() throws Exception { @Test public void wrapper() throws Exception { - declareVariable("x", createMessage("dev.cel.testing.testdata.proto3.TestAllTypes")); + declareVariable("x", StructTypeReference.create("cel.expr.conformance.proto3.TestAllTypes")); source = "x.single_int64_wrapper + 1 != 23"; runTest(); } @Test public void equalsWrapper() throws Exception { - declareVariable("x", createMessage("dev.cel.testing.testdata.proto3.TestAllTypes")); + declareVariable("x", StructTypeReference.create("cel.expr.conformance.proto3.TestAllTypes")); source = "x.single_int64_wrapper == 1 && " + "x.single_int32_wrapper != 2 && " @@ -316,18 +415,18 @@ public void equalsWrapper() throws Exception { @Test public void nullableWrapper() throws Exception { - declareVariable("x", createMessage("dev.cel.testing.testdata.proto3.TestAllTypes")); + declareVariable("x", StructTypeReference.create("cel.expr.conformance.proto3.TestAllTypes")); source = "x.single_int64_wrapper == null"; runTest(); } @Test public void nullableMessage() throws Exception { - declareVariable("x", createMessage("dev.cel.testing.testdata.proto3.TestAllTypes")); + declareVariable("x", StructTypeReference.create("cel.expr.conformance.proto3.TestAllTypes")); source = "x.single_nested_message != null"; runTest(); - container = "dev.cel.testing.testdata.proto3.TestAllTypesProto"; + container = CelContainer.ofName("cel.expr.conformance.proto3.TestAllTypesProto"); source = "null == TestAllTypes{} || TestAllTypes{} == null"; runTest(); } @@ -340,7 +439,7 @@ public void nullNull() throws Exception { @Test public void nullablePrimitiveError() throws Exception { - declareVariable("x", createMessage("dev.cel.testing.testdata.proto3.TestAllTypes")); + declareVariable("x", StructTypeReference.create("cel.expr.conformance.proto3.TestAllTypes")); source = "x.single_int64 != null"; runTest(); } @@ -350,14 +449,14 @@ public void nullablePrimitiveError() throws Exception { @Test public void dynOperators() throws Exception { - declareVariable("x", createMessage("dev.cel.testing.testdata.proto3.TestAllTypes")); + declareVariable("x", StructTypeReference.create("cel.expr.conformance.proto3.TestAllTypes")); source = "x.single_value + 1 / x.single_struct.y == 23"; runTest(); } @Test public void dynOperatorsAtRuntime() throws Exception { - declareVariable("x", createMessage("dev.cel.testing.testdata.proto3.TestAllTypes")); + declareVariable("x", StructTypeReference.create("cel.expr.conformance.proto3.TestAllTypes")); source = "x.single_value[23] + x.single_struct['y']"; runTest(); } @@ -376,17 +475,15 @@ public void flexibleTypeAdaption() throws Exception { source = "([[[1]], [[2]], [[3]]][0][0] + [2, 3, {'four': {'five': 'six'}}])[3]"; runTest(); - declareVariable("a", createTypeParam("T")); + declareVariable("a", TypeParamType.create("T")); source = "a.b + 1 == a[0]"; runTest(); - Type keyParam = createTypeParam("A"); - Type valParam = createTypeParam("B"); - Type mapType = createMap(keyParam, valParam); + CelType keyParam = TypeParamType.create("A"); + CelType valParam = TypeParamType.create("B"); + CelType mapType = MapType.create(keyParam, valParam); declareFunction( - "merge", - globalOverload( - "merge_maps", ImmutableList.of(mapType, mapType), ImmutableList.of("A", "B"), mapType)); + "merge", globalOverload("merge_maps", ImmutableList.of(mapType, mapType), mapType)); source = "merge({'hello': dyn(1)}, {'world': 2.0})"; runTest(); @@ -394,14 +491,24 @@ public void flexibleTypeAdaption() throws Exception { runTest(); } + @Test + public void userFunctionOverlappingOverloadsError() throws Exception { + declareFunction( + "func", + memberOverload("overlapping_overload_1", ImmutableList.of(SimpleType.INT), SimpleType.INT), + memberOverload("overlapping_overload_2", ImmutableList.of(SimpleType.INT), SimpleType.INT)); + source = "func(1)"; + runTest(); + } + // Json Types // ========== @Test public void jsonType() throws Exception { - declareVariable("x", createMessage("google.protobuf.Struct")); - declareVariable("y", createMessage("google.protobuf.ListValue")); - declareVariable("z", createMessage("google.protobuf.Value")); + declareVariable("x", StructTypeReference.create("google.protobuf.Struct")); + declareVariable("y", StructTypeReference.create("google.protobuf.ListValue")); + declareVariable("z", StructTypeReference.create("google.protobuf.Value")); source = "x[\"claims\"][\"groups\"][0].name == \"dummy\" " + "&& x.claims[\"exp\"] == y[1].time " @@ -410,20 +517,84 @@ 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 // ============================= @Test public void callStyle() throws Exception { - Type param = createTypeParam("A"); + CelType param = TypeParamType.create("A"); // Note, the size() function here is added in a separate scope from the standard declaration // set, but the environment ensures that the standard and custom overloads are returned together // during function resolution time. declareFunction( "size", - memberOverload( - "my_size", ImmutableList.of(createList(param)), ImmutableList.of("A"), CelTypes.INT64)); - declareVariable("x", createList(CelTypes.INT64)); + memberOverload("my_size", ImmutableList.of(ListType.create(param)), SimpleType.INT)); + declareVariable("x", ListType.create(SimpleType.INT)); source = "size(x) == x.size()"; runTest(); } @@ -434,12 +605,12 @@ public void userFunction() throws Exception { "myfun", memberOverload( "myfun_instance", - ImmutableList.of(CelTypes.INT64, CelTypes.BOOL, CelTypes.UINT64), - CelTypes.INT64), + ImmutableList.of(SimpleType.INT, SimpleType.BOOL, SimpleType.UINT), + SimpleType.INT), globalOverload( "myfun_static", - ImmutableList.of(CelTypes.INT64, CelTypes.BOOL, CelTypes.UINT64), - CelTypes.INT64)); + ImmutableList.of(SimpleType.INT, SimpleType.BOOL, SimpleType.UINT), + SimpleType.INT)); source = "myfun(1, true, 3u) + 1.myfun(false, 3u).myfun(true, 42u)"; runTest(); } @@ -448,7 +619,7 @@ public void userFunction() throws Exception { public void namespacedFunctions() throws Exception { declareFunction( "ns.func", - globalOverload("ns_func_overload", ImmutableList.of(CelTypes.STRING), CelTypes.INT64)); + globalOverload("ns_func_overload", ImmutableList.of(SimpleType.STRING), SimpleType.INT)); source = "ns.func('hello')"; runTest(); @@ -456,8 +627,8 @@ public void namespacedFunctions() throws Exception { "member", memberOverload( "ns_member_overload", - ImmutableList.of(CelTypes.INT64, CelTypes.INT64), - CelTypes.INT64)); + ImmutableList.of(SimpleType.INT, SimpleType.INT), + SimpleType.INT)); source = "ns.func('hello').member(ns.func('test'))"; runTest(); @@ -477,7 +648,7 @@ public void namespacedFunctions() throws Exception { source = "[1, 2].map(x, x * ns.func('test'))"; runTest(); - container = "ns"; + container = CelContainer.ofName("ns"); source = "func('hello')"; runTest(); @@ -487,61 +658,58 @@ public void namespacedFunctions() throws Exception { @Test public void namespacedVariables() throws Exception { - container = "ns"; - declareVariable("ns.x", CelTypes.INT64); + container = CelContainer.ofName("ns"); + declareVariable("ns.x", SimpleType.INT); source = "x"; runTest(); - container = "dev.cel.testing.testdata.proto3"; - Type messageType = createMessage("dev.cel.testing.testdata.proto3.TestAllTypes"); - declareVariable("dev.cel.testing.testdata.proto3.msgVar", messageType); + container = CelContainer.ofName("cel.expr.conformance.proto3"); + CelType messageType = StructTypeReference.create("cel.expr.conformance.proto3.TestAllTypes"); + declareVariable("cel.expr.conformance.proto3.msgVar", messageType); source = "msgVar.single_int32"; runTest(); } @Test public void userFunctionMultipleOverloadsWithSanitization() throws Exception { - Type structType = createMessage("google.protobuf.Struct"); + CelType structType = StructTypeReference.create("google.protobuf.Struct"); declareVariable("s", structType); declareFunction( "myfun", - globalOverload("myfun_int", ImmutableList.of(CelTypes.INT64), CelTypes.INT64), - globalOverload("myfun_struct", ImmutableList.of(structType), CelTypes.INT64)); + globalOverload("myfun_int", ImmutableList.of(SimpleType.INT), SimpleType.INT), + globalOverload("myfun_struct", ImmutableList.of(structType), SimpleType.INT)); source = "myfun(1) + myfun(s)"; runTest(); } @Test public void userFunctionOverlaps() throws Exception { - Type param = createTypeParam("TEST"); + CelType param = TypeParamType.create("TEST"); // Note, the size() function here shadows the definition of the size() function in the standard // declaration set. The type param name is chosen as 'TEST' to make sure not to conflict with // the standard environment type param name for the same overload signature. declareFunction( "size", - globalOverload( - "my_size", - ImmutableList.of(createList(param)), - ImmutableList.of("TEST"), - CelTypes.UINT64)); - declareVariable("x", createList(CelTypes.INT64)); + globalOverload("my_size", ImmutableList.of(ListType.create(param)), SimpleType.UINT)); + declareVariable("x", ListType.create(SimpleType.INT)); source = "size(x) == 1u"; runTest(); } @Test public void userFunctionAddsOverload() throws Exception { - Type messageType = createMessage("dev.cel.testing.testdata.proto3.TestAllTypes"); + CelType messageType = StructTypeReference.create("cel.expr.conformance.proto3.TestAllTypes"); declareVariable("x", messageType); declareFunction( - "size", globalOverload("size_message", ImmutableList.of(messageType), CelTypes.INT64)); + "size", globalOverload("size_message", ImmutableList.of(messageType), SimpleType.INT)); source = "size(x) > 4"; runTest(); } @Test public void userFunctionAddsMacroError() throws Exception { - declareFunction("has", globalOverload("has_id", ImmutableList.of(CelTypes.DYN), CelTypes.DYN)); + declareFunction( + "has", globalOverload("has_id", ImmutableList.of(SimpleType.DYN), SimpleType.DYN)); source = "false"; runTest(); } @@ -551,7 +719,7 @@ public void userFunctionAddsMacroError() throws Exception { @Test public void proto2PrimitiveField() throws Exception { - declareVariable("x", createMessage("dev.cel.testing.testdata.proto2.Proto2Message")); + declareVariable("x", StructTypeReference.create("cel.expr.conformance.proto2.TestAllTypes")); source = "x.single_fixed32 != 0u && x.single_fixed64 > 1u && x.single_int32 != null"; runTest(); source = "x.nestedgroup.single_name == ''"; @@ -563,21 +731,21 @@ public void proto2PrimitiveField() throws Exception { @Test public void aggregateMessage() throws Exception { - container = "dev.cel.testing.testdata.proto3.TestAllTypesProto"; + container = CelContainer.ofName("cel.expr.conformance.proto3"); source = "TestAllTypes{single_int32: 1, single_int64: 2}"; runTest(); } @Test public void aggregateMessageFieldUndefinedError() throws Exception { - container = "dev.cel.testing.testdata.proto3.TestAllTypesProto"; + container = CelContainer.ofName("cel.expr.conformance.proto3"); source = "TestAllTypes{single_int32: 1, undefined: 2}"; runTest(); } @Test public void aggregateMessageFieldTypeError() throws Exception { - container = "dev.cel.testing.testdata.proto3.TestAllTypesProto"; + container = CelContainer.ofName("cel.expr.conformance.proto3"); source = "TestAllTypes{single_int32: 1u}"; runTest(); } @@ -661,19 +829,19 @@ public void types() throws Exception { @Test public void enumValues() throws Exception { - container = "dev.cel.testing.testdata.proto3.TestAllTypesProto"; + container = CelContainer.ofName("cel.expr.conformance.proto3"); source = "TestAllTypes.NestedEnum.BAR != 99"; runTest(); } @Test public void nestedEnums() throws Exception { - declareVariable("x", createMessage(TestAllTypes.getDescriptor().getFullName())); - container = TestAllTypes.getDescriptor().getFile().getPackage(); + declareVariable("x", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())); + container = CelContainer.ofName(TestAllTypes.getDescriptor().getFile().getPackage()); source = "x.single_nested_enum == TestAllTypes.NestedEnum.BAR"; runTest(); - declareVariable("single_nested_enum", CelTypes.INT64); + declareVariable("single_nested_enum", SimpleType.INT); source = "single_nested_enum == TestAllTypes.NestedEnum.BAR"; runTest(); @@ -684,7 +852,7 @@ public void nestedEnums() throws Exception { @Test public void globalEnumValues() throws Exception { - container = "dev.cel.testing.testdata.proto3.TestAllTypesProto"; + container = CelContainer.ofName("cel.expr.conformance.proto3"); source = "GlobalEnum.GAZ == 2"; runTest(); } @@ -694,7 +862,7 @@ public void globalEnumValues() throws Exception { @Test public void globalStandaloneEnumValues() throws Exception { - container = "dev.cel.testing.testdata.proto3.TestAllTypesProto"; + container = CelContainer.ofName("dev.cel.testing.testdata.proto3"); source = "StandaloneGlobalEnum.SGAZ == 2"; FileDescriptorSet.Builder descriptorBuilder = FileDescriptorSet.newBuilder(); @@ -724,7 +892,7 @@ public void conversions() throws Exception { @Test public void quantifiers() throws Exception { - Type messageType = createMessage("dev.cel.testing.testdata.proto3.TestAllTypes"); + CelType messageType = StructTypeReference.create("cel.expr.conformance.proto3.TestAllTypes"); declareVariable("x", messageType); source = "x.repeated_int64.all(e, e > 0) " @@ -733,9 +901,83 @@ public void quantifiers() throws Exception { runTest(); } + @Test + public void twoVarComprehensions_allMacro() throws Exception { + CelType messageType = StructTypeReference.create("cel.expr.conformance.proto3.TestAllTypes"); + declareVariable("x", messageType); + source = + "x.map_string_string.all(i, v, i < v) " + + "&& x.repeated_int64.all(i, v, i < v) " + + "&& [1, 2, 3, 4].all(i, v, i < 5 && v > 0) " + + "&& {'a': 1, 'b': 2}.all(k, v, k.startsWith('a') && v == 1)"; + runTest(); + } + + @Test + public void twoVarComprehensions_existsMacro() throws Exception { + CelType messageType = StructTypeReference.create("cel.expr.conformance.proto3.TestAllTypes"); + declareVariable("x", messageType); + source = + "x.map_string_string.exists(i, v, i < v) " + + "&& x.repeated_int64.exists(i, v, i < v) " + + "&& [1, 2, 3, 4].exists(i, v, i < 5 && v > 0) " + + "&& {'a': 1, 'b': 2}.exists(k, v, k.startsWith('a') && v == 1)"; + runTest(); + } + + @Test + public void twoVarComprehensions_existsOneMacro() throws Exception { + CelType messageType = StructTypeReference.create("cel.expr.conformance.proto3.TestAllTypes"); + declareVariable("x", messageType); + source = + "x.map_string_string.exists_one(i, v, i < v) " + + "&& x.repeated_int64.exists_one(i, v, i < v) " + + "&& [1, 2, 3, 4].exists_one(i, v, i < 5 && v > 0) " + + "&& {'a': 1, 'b': 2}.exists_one(k, v, k.startsWith('a') && v == 1)"; + runTest(); + } + + @Test + public void twoVarComprehensions_transformListMacro() throws Exception { + CelType messageType = StructTypeReference.create("cel.expr.conformance.proto3.TestAllTypes"); + declareVariable("x", messageType); + source = + "[1, 2, 3].transformList(i, v, i > 0 && v < 3, (i * v) + v) == [4] " + + "&& [1, 2, 3].transformList(i, v, i % 2 == 0, (i * v) + v) == [1,9] " + + "&& [1, 2, 3].transformList(i, v, (i * v) + v) == [1,4,9]"; + runTest(); + } + + @Test + public void twoVarComprehensions_incorrectIterVars() throws Exception { + CelType messageType = StructTypeReference.create("cel.expr.conformance.proto3.TestAllTypes"); + declareVariable("x", messageType); + source = "x.map_string_string.all(i + 1, v, i < v) && x.repeated_int64.all(i, v + 1, i < v)"; + runTest(); + } + + @Test + public void twoVarComprehensions_duplicateIterVars() throws Exception { + CelType messageType = StructTypeReference.create("cel.expr.conformance.proto3.TestAllTypes"); + declareVariable("x", messageType); + source = "x.repeated_int64.exists(i, i, i < v)"; + runTest(); + } + + @Test + public void twoVarComprehensions_incorrectNumberOfArgs() throws Exception { + CelType messageType = StructTypeReference.create("cel.expr.conformance.proto3.TestAllTypes"); + declareVariable("x", messageType); + source = + "[1, 2, 3, 4].exists_one(i, v, i < v, v)" + + "&& x.map_string_string.transformList(i, i < v) " + + "&& [1, 2, 3].transformList(i, v, i > 0 && x < 3, (i * v) + v) == [4]"; + runTest(); + } + @Test public void quantifiersErrors() throws Exception { - Type messageType = createMessage("dev.cel.testing.testdata.proto3.TestAllTypes"); + CelType messageType = StructTypeReference.create("cel.expr.conformance.proto3.TestAllTypes"); declareVariable("x", messageType); source = "x.all(e, 0)"; runTest(); @@ -743,7 +985,7 @@ public void quantifiersErrors() throws Exception { @Test public void mapExpr() throws Exception { - Type messageType = createMessage("dev.cel.testing.testdata.proto3.TestAllTypes"); + CelType messageType = StructTypeReference.create("cel.expr.conformance.proto3.TestAllTypes"); declareVariable("x", messageType); source = "x.repeated_int64.map(x, double(x))"; runTest(); @@ -757,16 +999,16 @@ public void mapExpr() throws Exception { @Test public void mapFilterExpr() throws Exception { - Type messageType = createMessage("dev.cel.testing.testdata.proto3.TestAllTypes"); + CelType messageType = StructTypeReference.create("cel.expr.conformance.proto3.TestAllTypes"); declareVariable("x", messageType); source = "x.repeated_int64.map(x, x > 0, double(x))"; runTest(); - declareVariable("lists", CelTypes.DYN); + declareVariable("lists", SimpleType.DYN); source = "lists.filter(x, x > 1.5)"; runTest(); - declareVariable("args", createMap(CelTypes.STRING, CelTypes.DYN)); + declareVariable("args", MapType.create(SimpleType.STRING, SimpleType.DYN)); source = "args.user[\"myextension\"].customAttributes.filter(x, x.name == \"hobbies\")"; runTest(); } @@ -776,15 +1018,14 @@ public void mapFilterExpr() throws Exception { @Test public void abstractTypeParameterLess() throws Exception { - Type abstractType = - Type.newBuilder().setAbstractType(AbstractType.newBuilder().setName("abs")).build(); + CelType abstractType = OpaqueType.create("abs"); // Declare the identifier 'abs' to bind to the abstract type. - declareVariable("abs", CelTypes.create(abstractType)); + declareVariable("abs", TypeType.create(abstractType)); // Declare a function to create a new value of abstract type. declareFunction("make_abs", globalOverload("make_abs", ImmutableList.of(), abstractType)); // Declare a function to consume value of abstract type. declareFunction( - "as_bool", memberOverload("as_bool", ImmutableList.of(abstractType), CelTypes.BOOL)); + "as_bool", memberOverload("as_bool", ImmutableList.of(abstractType), SimpleType.BOOL)); source = "type(make_abs()) == abs && make_abs().as_bool()"; runTest(); @@ -792,36 +1033,22 @@ public void abstractTypeParameterLess() throws Exception { @Test public void abstractTypeParameterized() throws Exception { - Type typeParam = createTypeParam("T"); - Type abstractType = - Type.newBuilder() - .setAbstractType( - AbstractType.newBuilder().setName("vector").addParameterTypes(typeParam)) - .build(); + CelType typeParam = TypeParamType.create("T"); + CelType abstractType = OpaqueType.create("vector", typeParam); + TypeType typeOfTypeParam = TypeType.create(typeParam); + TypeType typeOfAbstractType = TypeType.create(abstractType); declareFunction( "vector", // Declare the function 'vector' to create the abstract type. - globalOverload( - "vector", - ImmutableList.of(CelTypes.create(typeParam)), - ImmutableList.of("T"), - CelTypes.create(abstractType)), + globalOverload("vector_type", ImmutableList.of(typeOfTypeParam), typeOfAbstractType), // Declare a function to create a new value of abstract type based on a list. - globalOverload( - "vector", - ImmutableList.of(createList(typeParam)), - ImmutableList.of("T"), - abstractType)); + globalOverload("vector_list", ImmutableList.of(ListType.create(typeParam)), abstractType)); // Declare a function to consume value of abstract type. declareFunction( "at", - memberOverload( - "at", - ImmutableList.of(abstractType, CelTypes.INT64), - ImmutableList.of("T"), - typeParam)); + memberOverload("vector_at_int", ImmutableList.of(abstractType, SimpleType.INT), typeParam)); // The parameterization of 'vector(dyn)' is erased at runtime and so is checked as a 'vector', // but no further. @@ -831,26 +1058,17 @@ public void abstractTypeParameterized() throws Exception { @Test public void abstractTypeParameterizedInListLiteral() throws Exception { - Type typeParam = createTypeParam("T"); - Type abstractType = - Type.newBuilder() - .setAbstractType( - AbstractType.newBuilder().setName("vector").addParameterTypes(typeParam)) - .build(); + CelType typeParam = TypeParamType.create("T"); + CelType abstractType = OpaqueType.create("vector", typeParam); + TypeType typeOfAbstractType = TypeType.create(abstractType); + TypeType typeOfTypeParam = TypeType.create(typeParam); + declareFunction( "vector", // Declare the function 'vector' to create the abstract type. - globalOverload( - "vector", - ImmutableList.of(CelTypes.create(typeParam)), - ImmutableList.of("T"), - CelTypes.create(abstractType)), + globalOverload("vector_type", ImmutableList.of(typeOfTypeParam), typeOfAbstractType), // Declare a function to create a new value of abstract type based on a list. - globalOverload( - "vector", - ImmutableList.of(createList(typeParam)), - ImmutableList.of("T"), - abstractType)); + globalOverload("vector_list", ImmutableList.of(ListType.create(typeParam)), abstractType)); source = "size([vector([1, 2]), vector([2u, -1])]) == 2"; runTest(); @@ -858,33 +1076,23 @@ public void abstractTypeParameterizedInListLiteral() throws Exception { @Test public void abstractTypeParameterizedError() throws Exception { - Type typeParam = createTypeParam("T"); - Type abstractType = - Type.newBuilder() - .setAbstractType( - AbstractType.newBuilder().setName("vector").addParameterTypes(typeParam)) - .build(); + CelType typeParam = TypeParamType.create("T"); + CelType abstractType = OpaqueType.create("vector", typeParam); + TypeType typeOfAbstractType = TypeType.create(abstractType); + TypeType typeOfTypeParam = TypeType.create(typeParam); + declareFunction( "vector", // Declare the function 'vector' to create the abstract type. - globalOverload( - "vector", - ImmutableList.of(CelTypes.create(typeParam)), - ImmutableList.of("T"), - CelTypes.create(abstractType)), + globalOverload("vector_type", ImmutableList.of(typeOfTypeParam), typeOfAbstractType), // Declare a function to create a new value of abstract type based on a list. - globalOverload( - "vector", - ImmutableList.of(createList(typeParam)), - ImmutableList.of("T"), - abstractType)); + globalOverload("vector_list", ImmutableList.of(ListType.create(typeParam)), abstractType)); declareFunction( "add", globalOverload( - "add", - ImmutableList.of(CelTypes.create(abstractType), CelTypes.create(abstractType)), - ImmutableList.of("T"), - abstractType)); + "add_vector_type", + ImmutableList.of(typeOfAbstractType, typeOfAbstractType), + typeOfAbstractType)); source = "add(vector([1, 2]), vector([2u, -1])) == vector([1, 2, 2u, -1])"; runTest(); } @@ -892,12 +1100,12 @@ public void abstractTypeParameterizedError() throws Exception { // Optionals @Test public void optionals() throws Exception { - declareVariable("a", createMap(CelTypes.STRING, CelTypes.STRING)); + declareVariable("a", MapType.create(SimpleType.STRING, SimpleType.STRING)); source = "a.?b"; runTest(); clearAllDeclarations(); - declareVariable("x", createOptionalType(createMap(CelTypes.STRING, CelTypes.STRING))); + declareVariable("x", OptionalType.create(MapType.create(SimpleType.STRING, SimpleType.STRING))); source = "x.y"; runTest(); @@ -905,7 +1113,7 @@ public void optionals() throws Exception { runTest(); clearAllDeclarations(); - declareVariable("d", createOptionalType(CelTypes.DYN)); + declareVariable("d", OptionalType.create(SimpleType.DYN)); source = "d.dynamic"; runTest(); @@ -913,7 +1121,7 @@ public void optionals() throws Exception { runTest(); clearAllDeclarations(); - declareVariable("e", createOptionalType(createMap(CelTypes.STRING, CelTypes.DYN))); + declareVariable("e", OptionalType.create(MapType.create(SimpleType.STRING, SimpleType.DYN))); source = "has(e.?b.c)"; runTest(); @@ -924,13 +1132,13 @@ public void optionals() throws Exception { source = "{?'key': {'a': 'b'}.?value}.key"; runTest(); - container = "dev.cel.testing.testdata.proto3.TestAllTypesProto"; + container = CelContainer.ofName("cel.expr.conformance.proto3"); source = "TestAllTypes{?single_int32: {}.?i}"; runTest(); - container = ""; - declareVariable("a", createOptionalType(CelTypes.STRING)); - declareVariable("b", createOptionalType(CelTypes.STRING)); + container = CelContainer.ofName(""); + declareVariable("a", OptionalType.create(SimpleType.STRING)); + declareVariable("b", OptionalType.create(SimpleType.STRING)); source = "[?a, ?b, 'world']"; runTest(); @@ -949,33 +1157,18 @@ public void optionalErrors() throws Exception { source = "[?'value']"; runTest(); - container = "dev.cel.testing.testdata.proto3.TestAllTypesProto"; + container = CelContainer.ofName("cel.expr.conformance.proto3"); source = "TestAllTypes{?single_int32: 1}"; runTest(); source = "a.?b"; - declareVariable("a", createMap(CelTypes.STRING, CelTypes.STRING)); + declareVariable("a", MapType.create(SimpleType.STRING, SimpleType.STRING)); prepareCompiler(new ProtoMessageTypeProvider()); - ParsedExpr parsedExpr = - CelProtoAbstractSyntaxTree.fromCelAst(celCompiler.parse(source).getAst()).toParsedExpr(); - ParsedExpr.Builder parsedExprBuilder = parsedExpr.toBuilder(); - parsedExprBuilder - .getExprBuilder() - .getCallExprBuilder() - .getArgsBuilder(1) - .setConstExpr(Constant.newBuilder().setBoolValue(true).build()); // Const must be a string - runErroneousTest(parsedExprBuilder.build()); - } - - private enum TestCase { - CEL_TYPE(true), - PROTO_TYPE(false); + CelAbstractSyntaxTree parsedAst = celCompiler.parse(source).getAst(); + CelMutableAst mutableAst = CelMutableAst.fromCelAst(parsedAst); + mutableAst.expr().call().args().get(1).setConstant(CelConstant.ofValue(true)); - private final boolean declareWithCelType; - - TestCase(boolean declareWithCelType) { - this.declareWithCelType = declareWithCelType; - } + runErroneousTest(mutableAst.toParsedAst()); } private static class CheckedExprAdorner implements CelAdorner { diff --git a/checker/src/test/java/dev/cel/checker/ProtoTypeMaskTest.java b/checker/src/test/java/dev/cel/checker/ProtoTypeMaskTest.java index f937dbdc2..89241652c 100644 --- a/checker/src/test/java/dev/cel/checker/ProtoTypeMaskTest.java +++ b/checker/src/test/java/dev/cel/checker/ProtoTypeMaskTest.java @@ -85,6 +85,14 @@ public void ofTypeWithFieldMask_invalidMask() { () -> ProtoTypeMask.of("test", FieldMask.newBuilder().addPaths("").build())); } + @Test + public void ofAllFieldsHidden() { + ProtoTypeMask typeExpr = ProtoTypeMask.ofAllFieldsHidden("google.rpc.context.AttributeContext"); + assertThat(typeExpr.areAllFieldPathsExposed()).isFalse(); + assertThat(typeExpr.getFieldPathsExposed()) + .containsExactly(FieldPath.of(ProtoTypeMask.HIDDEN_FIELD)); + } + @Test public void withFieldsAsVariableDeclarations() { assertThat(ProtoTypeMask.ofAllFields("google.type.Expr").fieldsAreVariableDeclarations()) diff --git a/checker/src/test/java/dev/cel/checker/ProtoTypeMaskTypeProviderTest.java b/checker/src/test/java/dev/cel/checker/ProtoTypeMaskTypeProviderTest.java index bf41c1e20..b4b52bd26 100644 --- a/checker/src/test/java/dev/cel/checker/ProtoTypeMaskTypeProviderTest.java +++ b/checker/src/test/java/dev/cel/checker/ProtoTypeMaskTypeProviderTest.java @@ -28,6 +28,7 @@ import dev.cel.common.types.ProtoMessageType; import dev.cel.common.types.ProtoMessageTypeProvider; import dev.cel.common.types.SimpleType; +import dev.cel.common.types.StructType.Field; import java.util.Arrays; import java.util.Optional; import org.junit.Test; @@ -72,7 +73,7 @@ public void lookupFieldNames_noProtoDecls() { ProtoTypeMaskTypeProvider protoTypeMaskProvider = new ProtoTypeMaskTypeProvider(celTypeProvider, ImmutableSet.of()); ProtoMessageType protoType = assertTypeFound(protoTypeMaskProvider, ATTRIBUTE_CONTEXT_TYPE); - assertThat(protoType.fields().stream().map(f -> f.name()).collect(toImmutableList())) + assertThat(protoType.fields().stream().map(Field::name).collect(toImmutableList())) .containsExactly( "resource", "request", @@ -87,6 +88,41 @@ public void lookupFieldNames_noProtoDecls() { assertThat(protoType).isSameInstanceAs(origProtoType); } + @Test + public void lookupFieldNames_allFieldsHidden() { + CelTypeProvider celTypeProvider = + new ProtoMessageTypeProvider(ImmutableSet.of(AttributeContext.getDescriptor())); + ProtoTypeMaskTypeProvider protoTypeMaskProvider = + new ProtoTypeMaskTypeProvider( + celTypeProvider, + ImmutableSet.of(ProtoTypeMask.ofAllFieldsHidden(ATTRIBUTE_CONTEXT_TYPE))); + + ProtoMessageType protoType = assertTypeFound(protoTypeMaskProvider, ATTRIBUTE_CONTEXT_TYPE); + assertThat(protoType.fieldNames()).isEmpty(); + ProtoMessageType origProtoType = assertTypeFound(celTypeProvider, ATTRIBUTE_CONTEXT_TYPE); + assertThat(protoType).isNotSameInstanceAs(origProtoType); + } + + @Test + public void protoTypeMaskProvider_hiddenFieldSentinelCharOnSubPath_throws() { + CelTypeProvider celTypeProvider = + new ProtoMessageTypeProvider(ImmutableSet.of(AttributeContext.getDescriptor())); + + IllegalArgumentException e = + assertThrows( + IllegalArgumentException.class, + () -> + new ProtoTypeMaskTypeProvider( + celTypeProvider, + ImmutableSet.of( + ProtoTypeMask.of( + "google.rpc.context.AttributeContext", + FieldMask.newBuilder().addPaths("resource.!").build())))); + assertThat(e) + .hasMessageThat() + .contains("message google.rpc.context.AttributeContext.Resource does not declare field: !"); + } + @Test public void lookupFieldNames_fullProtoDecl() { CelTypeProvider celTypeProvider = @@ -263,7 +299,7 @@ private ProtoMessageType assertTypeFound(CelTypeProvider celTypeProvider, String private void assertTypeHasFields(ProtoMessageType protoType, ImmutableSet fields) { ImmutableSet typeFieldNames = - protoType.fields().stream().map(f -> f.name()).collect(toImmutableSet()); + protoType.fields().stream().map(Field::name).collect(toImmutableSet()); assertThat(typeFieldNames).containsExactlyElementsIn(fields); } diff --git a/checker/src/test/java/dev/cel/checker/TypeProviderLegacyImplTest.java b/checker/src/test/java/dev/cel/checker/TypeProviderLegacyImplTest.java index bd6635666..4569877c3 100644 --- a/checker/src/test/java/dev/cel/checker/TypeProviderLegacyImplTest.java +++ b/checker/src/test/java/dev/cel/checker/TypeProviderLegacyImplTest.java @@ -21,11 +21,10 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.protobuf.Descriptors.Descriptor; -import dev.cel.common.types.CelTypes; +import dev.cel.common.types.CelProtoTypes; import dev.cel.common.types.ProtoMessageTypeProvider; -import dev.cel.testing.testdata.proto2.Proto2ExtensionScopedMessage; -import dev.cel.testing.testdata.proto2.Proto2Message; -import dev.cel.testing.testdata.proto2.TestAllTypesProto.TestAllTypes; +import dev.cel.expr.conformance.proto2.Proto2ExtensionScopedMessage; +import dev.cel.expr.conformance.proto2.TestAllTypes; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -34,10 +33,7 @@ public final class TypeProviderLegacyImplTest { private static final ImmutableList DESCRIPTORS = - ImmutableList.of( - TestAllTypes.getDescriptor(), - Proto2Message.getDescriptor(), - Proto2ExtensionScopedMessage.getDescriptor()); + ImmutableList.of(TestAllTypes.getDescriptor(), Proto2ExtensionScopedMessage.getDescriptor()); private final ProtoMessageTypeProvider proto2Provider = new ProtoMessageTypeProvider(DESCRIPTORS); @@ -49,9 +45,8 @@ public final class TypeProviderLegacyImplTest { @Test public void lookupType() { - assertThat(compatTypeProvider.lookupType("google.api.expr.test.v1.proto2.TestAllTypes")) - .isEqualTo( - descriptorTypeProvider.lookupType("google.api.expr.test.v1.proto2.TestAllTypes")); + assertThat(compatTypeProvider.lookupType("cel.expr.conformance.proto2.TestAllTypes")) + .isEqualTo(descriptorTypeProvider.lookupType("cel.expr.conformance.proto2.TestAllTypes")); assertThat(compatTypeProvider.lookupType("not.registered.TypeName")) .isEqualTo(descriptorTypeProvider.lookupType("not.registered.TypeName")); } @@ -59,9 +54,7 @@ public void lookupType() { @Test public void lookupFieldNames() { Type nestedTestAllTypes = - compatTypeProvider - .lookupType("dev.cel.testing.testdata.proto2.NestedTestAllTypes") - .getType(); + compatTypeProvider.lookupType("cel.expr.conformance.proto2.NestedTestAllTypes").getType(); ImmutableSet fieldNames = compatTypeProvider.lookupFieldNames(nestedTestAllTypes); assertThat(fieldNames) .containsExactlyElementsIn(descriptorTypeProvider.lookupFieldNames(nestedTestAllTypes)); @@ -71,9 +64,7 @@ public void lookupFieldNames() { @Test public void lookupFieldType() { Type nestedTestAllTypes = - compatTypeProvider - .lookupType("dev.cel.testing.testdata.proto2.NestedTestAllTypes") - .getType(); + compatTypeProvider.lookupType("cel.expr.conformance.proto2.NestedTestAllTypes").getType(); assertThat(compatTypeProvider.lookupFieldType(nestedTestAllTypes, "payload")) .isEqualTo(descriptorTypeProvider.lookupFieldType(nestedTestAllTypes, "payload")); assertThat(compatTypeProvider.lookupFieldType(nestedTestAllTypes, "child")) @@ -83,7 +74,7 @@ public void lookupFieldType() { @Test public void lookupFieldType_inputNotMessage() { Type globalEnumType = - compatTypeProvider.lookupType("dev.cel.testing.testdata.proto2.GlobalEnum").getType(); + compatTypeProvider.lookupType("cel.expr.conformance.proto2.GlobalEnum").getType(); assertThat(compatTypeProvider.lookupFieldType(globalEnumType, "payload")).isNull(); assertThat(compatTypeProvider.lookupFieldType(globalEnumType, "payload")) .isEqualTo(descriptorTypeProvider.lookupFieldType(globalEnumType, "payload")); @@ -92,47 +83,44 @@ public void lookupFieldType_inputNotMessage() { @Test public void lookupExtension() { TypeProvider.ExtensionFieldType extensionType = - compatTypeProvider.lookupExtensionType("dev.cel.testing.testdata.proto2.nested_enum_ext"); + compatTypeProvider.lookupExtensionType("cel.expr.conformance.proto2.nested_enum_ext"); assertThat(extensionType.messageType()) - .isEqualTo(CelTypes.createMessage("dev.cel.testing.testdata.proto2.Proto2Message")); - assertThat(extensionType.fieldType().type()).isEqualTo(CelTypes.INT64); + .isEqualTo(CelProtoTypes.createMessage("cel.expr.conformance.proto2.TestAllTypes")); + assertThat(extensionType.fieldType().type()).isEqualTo(CelProtoTypes.INT64); assertThat(extensionType) .isEqualTo( descriptorTypeProvider.lookupExtensionType( - "dev.cel.testing.testdata.proto2.nested_enum_ext")); + "cel.expr.conformance.proto2.nested_enum_ext")); } @Test public void lookupEnumValue() { Integer enumValue = - compatTypeProvider.lookupEnumValue("dev.cel.testing.testdata.proto2.GlobalEnum.GAR"); + compatTypeProvider.lookupEnumValue("cel.expr.conformance.proto2.GlobalEnum.GAR"); assertThat(enumValue).isEqualTo(1); assertThat(enumValue) .isEqualTo( - descriptorTypeProvider.lookupEnumValue( - "dev.cel.testing.testdata.proto2.GlobalEnum.GAR")); + descriptorTypeProvider.lookupEnumValue("cel.expr.conformance.proto2.GlobalEnum.GAR")); } @Test public void lookupEnumValue_notFoundValue() { Integer enumValue = - compatTypeProvider.lookupEnumValue("dev.cel.testing.testdata.proto2.GlobalEnum.BAR"); + compatTypeProvider.lookupEnumValue("cel.expr.conformance.proto2.GlobalEnum.BAR"); assertThat(enumValue).isNull(); assertThat(enumValue) .isEqualTo( - descriptorTypeProvider.lookupEnumValue( - "dev.cel.testing.testdata.proto2.GlobalEnum.BAR")); + descriptorTypeProvider.lookupEnumValue("cel.expr.conformance.proto2.GlobalEnum.BAR")); } @Test public void lookupEnumValue_notFoundEnumType() { Integer enumValue = - compatTypeProvider.lookupEnumValue("dev.cel.testing.testdata.proto2.InvalidEnum.TEST"); + compatTypeProvider.lookupEnumValue("cel.expr.conformance.proto2.InvalidEnum.TEST"); assertThat(enumValue).isNull(); assertThat(enumValue) .isEqualTo( - descriptorTypeProvider.lookupEnumValue( - "dev.cel.testing.testdata.proto2.InvalidEnum.TEST")); + descriptorTypeProvider.lookupEnumValue("cel.expr.conformance.proto2.InvalidEnum.TEST")); } @Test diff --git a/checker/src/test/java/dev/cel/checker/TypesTest.java b/checker/src/test/java/dev/cel/checker/TypesTest.java index 60be88bb8..960ebec3f 100644 --- a/checker/src/test/java/dev/cel/checker/TypesTest.java +++ b/checker/src/test/java/dev/cel/checker/TypesTest.java @@ -19,8 +19,8 @@ import dev.cel.expr.Type; import dev.cel.expr.Type.PrimitiveType; 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.common.types.SimpleType; import java.util.HashMap; import java.util.Map; @@ -34,8 +34,8 @@ public class TypesTest { @Test public void isAssignable_usingProtoTypes() { Map subs = new HashMap<>(); - Type typeParamA = CelTypes.createTypeParam("A"); - Type stringType = CelTypes.create(PrimitiveType.STRING); + Type typeParamA = CelProtoTypes.createTypeParam("A"); + Type stringType = CelProtoTypes.create(PrimitiveType.STRING); Map result = Types.isAssignable(subs, typeParamA, stringType); diff --git a/checker/src/test/resources/abstractTypeParameterized.baseline b/checker/src/test/resources/abstractTypeParameterized.baseline index 948f61ec9..28cc0000a 100644 --- a/checker/src/test/resources/abstractTypeParameterized.baseline +++ b/checker/src/test/resources/abstractTypeParameterized.baseline @@ -1,10 +1,10 @@ Source: type(vector([1])) == vector(dyn) && vector([1]).at(0) == 1 declare vector { - function vector (type(T)) -> type(vector(T)) - function vector (list(T)) -> vector(T) + function vector_type (type(T)) -> type(vector(T)) + function vector_list (list(T)) -> vector(T) } declare at { - function at vector(T).(int) -> T + function vector_at_int vector(T).(int) -> T } =====> _&&_( @@ -14,20 +14,20 @@ _&&_( [ 1~int ]~list(int) - )~vector(int)^vector + )~vector(int)^vector_list )~type(vector(int))^type, vector( dyn~type(dyn)^dyn - )~type(vector(dyn))^vector + )~type(vector(dyn))^vector_type )~bool^equals, _==_( vector( [ 1~int ]~list(int) - )~vector(int)^vector.at( + )~vector(int)^vector_list.at( 0~int - )~int^at, + )~int^vector_at_int, 1~int )~bool^equals )~bool^logical_and diff --git a/checker/src/test/resources/abstractTypeParameterizedError.baseline b/checker/src/test/resources/abstractTypeParameterizedError.baseline index 140202ab1..8ef50993e 100644 --- a/checker/src/test/resources/abstractTypeParameterizedError.baseline +++ b/checker/src/test/resources/abstractTypeParameterizedError.baseline @@ -1,10 +1,10 @@ Source: add(vector([1, 2]), vector([2u, -1])) == vector([1, 2, 2u, -1]) declare vector { - function vector (type(T)) -> type(vector(T)) - function vector (list(T)) -> vector(T) + function vector_type (type(T)) -> type(vector(T)) + function vector_list (list(T)) -> vector(T) } declare add { - function add (type(vector(T)), type(vector(T))) -> vector(T) + function add_vector_type (type(vector(T)), type(vector(T))) -> type(vector(T)) } =====> ERROR: test_location:1:4: found no matching overload for 'add' applied to '(vector(int), vector(dyn))' (candidates: (type(vector(%T4)), type(vector(%T4)))) diff --git a/checker/src/test/resources/abstractTypeParameterizedInListLiteral.baseline b/checker/src/test/resources/abstractTypeParameterizedInListLiteral.baseline index e07a05598..3425e0325 100644 --- a/checker/src/test/resources/abstractTypeParameterizedInListLiteral.baseline +++ b/checker/src/test/resources/abstractTypeParameterizedInListLiteral.baseline @@ -1,7 +1,7 @@ Source: size([vector([1, 2]), vector([2u, -1])]) == 2 declare vector { - function vector (type(T)) -> type(vector(T)) - function vector (list(T)) -> vector(T) + function vector_type (type(T)) -> type(vector(T)) + function vector_list (list(T)) -> vector(T) } =====> _==_( @@ -12,13 +12,13 @@ _==_( 1~int, 2~int ]~list(int) - )~vector(int)^vector, + )~vector(int)^vector_list, vector( [ 2u~uint, -1~int ]~list(dyn) - )~vector(dyn)^vector + )~vector(dyn)^vector_list ]~list(vector(dyn)) )~int^size_list, 2~int diff --git a/checker/src/test/resources/aggregateMessage.baseline b/checker/src/test/resources/aggregateMessage.baseline index 646c6e15b..eb138b0e8 100644 --- a/checker/src/test/resources/aggregateMessage.baseline +++ b/checker/src/test/resources/aggregateMessage.baseline @@ -1,7 +1,6 @@ Source: TestAllTypes{single_int32: 1, single_int64: 2} =====> -TestAllTypes{ +cel.expr.conformance.proto3.TestAllTypes{ single_int32:1~int, single_int64:2~int -}~dev.cel.testing.testdata.proto3.TestAllTypes^dev.cel.testing.testdata.proto3.TestAllTypes - +}~cel.expr.conformance.proto3.TestAllTypes^cel.expr.conformance.proto3.TestAllTypes \ No newline at end of file diff --git a/checker/src/test/resources/anyMessage.baseline b/checker/src/test/resources/anyMessage.baseline index 55aedb13f..9c94a0b24 100644 --- a/checker/src/test/resources/anyMessage.baseline +++ b/checker/src/test/resources/anyMessage.baseline @@ -1,4 +1,4 @@ -Source: x == google.protobuf.Any{type_url:'types.googleapis.com/dev.cel.testing.testdata.proto3.TestAllTypes'} && x.single_nested_message.bb == 43 || x == dev.cel.testing.testdata.proto3.TestAllTypes{} || y < x|| x >= x +Source: x == google.protobuf.Any{type_url:'types.googleapis.com/cel.expr.conformance.proto3.TestAllTypes'} && x.single_nested_message.bb == 43 || x == cel.expr.conformance.proto3.TestAllTypes{} || y < x|| x >= x declare x { value any } @@ -12,7 +12,7 @@ _||_( _==_( x~any^x, google.protobuf.Any{ - type_url:"types.googleapis.com/dev.cel.testing.testdata.proto3.TestAllTypes"~string + type_url:"types.googleapis.com/cel.expr.conformance.proto3.TestAllTypes"~string }~any^google.protobuf.Any )~bool^equals, _==_( @@ -22,7 +22,7 @@ _||_( )~bool^logical_and, _==_( x~any^x, - dev.cel.testing.testdata.proto3.TestAllTypes{}~dev.cel.testing.testdata.proto3.TestAllTypes^dev.cel.testing.testdata.proto3.TestAllTypes + cel.expr.conformance.proto3.TestAllTypes{}~cel.expr.conformance.proto3.TestAllTypes^cel.expr.conformance.proto3.TestAllTypes )~bool^equals )~bool^logical_or, _||_( diff --git a/checker/src/test/resources/callStyle.baseline b/checker/src/test/resources/callStyle.baseline index 3e1cfcc0a..415f87e62 100644 --- a/checker/src/test/resources/callStyle.baseline +++ b/checker/src/test/resources/callStyle.baseline @@ -1,14 +1,14 @@ Source: size(x) == x.size() -declare size { - function my_size list(A).() -> int -} declare x { value list(int) } +declare size { + function my_size list(A).() -> int +} =====> _==_( size( x~list(int)^x )~int^size_list, x~list(int)^x.size()~int^my_size -)~bool^equals +)~bool^equals \ No newline at end of file diff --git a/checker/src/test/resources/containers.baseline b/checker/src/test/resources/containers.baseline new file mode 100644 index 000000000..cdf26eb63 --- /dev/null +++ b/checker/src/test/resources/containers.baseline @@ -0,0 +1,38 @@ +Source: p3_alias.TestAllTypes{} +=====> +cel.expr.conformance.proto3.TestAllTypes{}~cel.expr.conformance.proto3.TestAllTypes^cel.expr.conformance.proto3.TestAllTypes + +Source: proto2.TestAllTypes{} +=====> +cel.expr.conformance.proto2.TestAllTypes{}~cel.expr.conformance.proto2.TestAllTypes^cel.expr.conformance.proto2.TestAllTypes + +Source: proto3.TestAllTypes{} +=====> +cel.expr.conformance.proto3.TestAllTypes{}~cel.expr.conformance.proto3.TestAllTypes^cel.expr.conformance.proto3.TestAllTypes + +Source: SGAR +=====> +dev.cel.testing.testdata.proto3.StandaloneGlobalEnum.SGAR~int^dev.cel.testing.testdata.proto3.StandaloneGlobalEnum.SGAR + +Source: foo_bar_alias.baz() +declare foo.bar { + value string +} +declare baz { + function foo_bar_baz_overload string.() -> dyn +} +=====> +foo.bar~string^foo.bar.baz()~dyn^foo_bar_baz_overload + +Source: foo_bar_baz_alias.qux() +declare foo.bar { + value string +} +declare baz { + function foo_bar_baz_overload string.() -> dyn +} +declare foo.bar.baz.qux { + function foo_bar_baz_qux_overload () -> dyn +} +=====> +foo.bar.baz.qux()~dyn^foo_bar_baz_qux_overload \ No newline at end of file diff --git a/checker/src/test/resources/dynOperators.baseline b/checker/src/test/resources/dynOperators.baseline index e95cbbb86..ceb4ce9e6 100644 --- a/checker/src/test/resources/dynOperators.baseline +++ b/checker/src/test/resources/dynOperators.baseline @@ -1,14 +1,14 @@ Source: x.single_value + 1 / x.single_struct.y == 23 declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> _==_( _+_( - x~dev.cel.testing.testdata.proto3.TestAllTypes^x.single_value~dyn, + x~cel.expr.conformance.proto3.TestAllTypes^x.single_value~dyn, _/_( 1~int, - x~dev.cel.testing.testdata.proto3.TestAllTypes^x.single_struct~map(string, dyn).y~dyn + x~cel.expr.conformance.proto3.TestAllTypes^x.single_struct~map(string, dyn).y~dyn )~int^divide_int64 )~int^add_int64, 23~int diff --git a/checker/src/test/resources/dynOperatorsAtRuntime.baseline b/checker/src/test/resources/dynOperatorsAtRuntime.baseline index 2a347d6ee..470289997 100644 --- a/checker/src/test/resources/dynOperatorsAtRuntime.baseline +++ b/checker/src/test/resources/dynOperatorsAtRuntime.baseline @@ -1,15 +1,15 @@ Source: x.single_value[23] + x.single_struct['y'] declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> _+_( _[_]( - x~dev.cel.testing.testdata.proto3.TestAllTypes^x.single_value~dyn, + x~cel.expr.conformance.proto3.TestAllTypes^x.single_value~dyn, 23~int )~dyn^index_list|index_map, _[_]( - x~dev.cel.testing.testdata.proto3.TestAllTypes^x.single_struct~map(string, dyn), + x~cel.expr.conformance.proto3.TestAllTypes^x.single_struct~map(string, dyn), "y"~string )~dyn^index_map )~dyn^add_int64|add_uint64|add_double|add_string|add_bytes|add_list|add_timestamp_duration|add_duration_timestamp|add_duration_duration diff --git a/checker/src/test/resources/enumValues.baseline b/checker/src/test/resources/enumValues.baseline index 2bad4ad99..be343985a 100644 --- a/checker/src/test/resources/enumValues.baseline +++ b/checker/src/test/resources/enumValues.baseline @@ -1,6 +1,6 @@ Source: TestAllTypes.NestedEnum.BAR != 99 =====> _!=_( - dev.cel.testing.testdata.proto3.TestAllTypes.NestedEnum.BAR~int^dev.cel.testing.testdata.proto3.TestAllTypes.NestedEnum.BAR, + cel.expr.conformance.proto3.TestAllTypes.NestedEnum.BAR~int^cel.expr.conformance.proto3.TestAllTypes.NestedEnum.BAR, 99~int )~bool^not_equals diff --git a/checker/src/test/resources/equalsWrapper.baseline b/checker/src/test/resources/equalsWrapper.baseline index c069c9625..ad105a5bd 100644 --- a/checker/src/test/resources/equalsWrapper.baseline +++ b/checker/src/test/resources/equalsWrapper.baseline @@ -1,38 +1,38 @@ Source: x.single_int64_wrapper == 1 && x.single_int32_wrapper != 2 && x.single_double_wrapper != 2.0 && x.single_float_wrapper == 1.0 && x.single_uint32_wrapper == 1u && x.single_uint64_wrapper != 42u declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> _&&_( _&&_( _&&_( _==_( - x~dev.cel.testing.testdata.proto3.TestAllTypes^x.single_int64_wrapper~wrapper(int), + x~cel.expr.conformance.proto3.TestAllTypes^x.single_int64_wrapper~wrapper(int), 1~int )~bool^equals, _!=_( - x~dev.cel.testing.testdata.proto3.TestAllTypes^x.single_int32_wrapper~wrapper(int), + x~cel.expr.conformance.proto3.TestAllTypes^x.single_int32_wrapper~wrapper(int), 2~int )~bool^not_equals )~bool^logical_and, _!=_( - x~dev.cel.testing.testdata.proto3.TestAllTypes^x.single_double_wrapper~wrapper(double), + x~cel.expr.conformance.proto3.TestAllTypes^x.single_double_wrapper~wrapper(double), 2.0~double )~bool^not_equals )~bool^logical_and, _&&_( _&&_( _==_( - x~dev.cel.testing.testdata.proto3.TestAllTypes^x.single_float_wrapper~wrapper(double), + x~cel.expr.conformance.proto3.TestAllTypes^x.single_float_wrapper~wrapper(double), 1.0~double )~bool^equals, _==_( - x~dev.cel.testing.testdata.proto3.TestAllTypes^x.single_uint32_wrapper~wrapper(uint), + x~cel.expr.conformance.proto3.TestAllTypes^x.single_uint32_wrapper~wrapper(uint), 1u~uint )~bool^equals )~bool^logical_and, _!=_( - x~dev.cel.testing.testdata.proto3.TestAllTypes^x.single_uint64_wrapper~wrapper(uint), + x~cel.expr.conformance.proto3.TestAllTypes^x.single_uint64_wrapper~wrapper(uint), 42u~uint )~bool^not_equals )~bool^logical_and diff --git a/checker/src/test/resources/globalEnumValues.baseline b/checker/src/test/resources/globalEnumValues.baseline index 87f242428..9a16481e6 100644 --- a/checker/src/test/resources/globalEnumValues.baseline +++ b/checker/src/test/resources/globalEnumValues.baseline @@ -1,6 +1,6 @@ Source: GlobalEnum.GAZ == 2 =====> _==_( - dev.cel.testing.testdata.proto3.GlobalEnum.GAZ~int^dev.cel.testing.testdata.proto3.GlobalEnum.GAZ, + cel.expr.conformance.proto3.GlobalEnum.GAZ~int^cel.expr.conformance.proto3.GlobalEnum.GAZ, 2~int )~bool^equals diff --git a/checker/src/test/resources/jsonStructTypeError.baseline b/checker/src/test/resources/jsonStructTypeError.baseline index a345af322..9923ddcda 100644 --- a/checker/src/test/resources/jsonStructTypeError.baseline +++ b/checker/src/test/resources/jsonStructTypeError.baseline @@ -1,8 +1,8 @@ -ource: x["iss"] != TestAllTypes{single_int32: 1} +Source: x["iss"] != TestAllTypes{single_int32: 1} declare x { value google.protobuf.Struct } =====> -ERROR: test_location:1:10: found no matching overload for '_!=_' applied to '(google.protobuf.Value, dev.cel.testing.testdata.proto3.TestAllTypes)' (candidates: (%A3, %A3)) +ERROR: test_location:1:10: found no matching overload for '_!=_' applied to '(google.protobuf.Value, cel.expr.conformance.proto3.TestAllTypes)' (candidates: (%A3, %A3)) | x["iss"] != TestAllTypes{single_int32: 1} | .........^ 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/checker/src/test/resources/listElemTypeError.baseline b/checker/src/test/resources/listElemTypeError.baseline index 666025b59..47f3ccc95 100644 --- a/checker/src/test/resources/listElemTypeError.baseline +++ b/checker/src/test/resources/listElemTypeError.baseline @@ -1,11 +1,11 @@ Source: x + y declare x { - value list(dev.cel.testing.testdata.proto3.TestAllTypes) + value list(cel.expr.conformance.proto3.TestAllTypes) } declare y { value list(int) } =====> -ERROR: test_location:1:3: found no matching overload for '_+_' applied to '(list(dev.cel.testing.testdata.proto3.TestAllTypes), list(int))' (candidates: (int, int),(uint, uint),(double, double),(string, string),(bytes, bytes),(list(%A0), list(%A0)),(google.protobuf.Timestamp, google.protobuf.Duration),(google.protobuf.Duration, google.protobuf.Timestamp),(google.protobuf.Duration, google.protobuf.Duration)) +ERROR: test_location:1:3: found no matching overload for '_+_' applied to '(list(cel.expr.conformance.proto3.TestAllTypes), list(int))' (candidates: (int, int),(uint, uint),(double, double),(string, string),(bytes, bytes),(list(%A0), list(%A0)),(google.protobuf.Timestamp, google.protobuf.Duration),(google.protobuf.Duration, google.protobuf.Timestamp),(google.protobuf.Duration, google.protobuf.Duration)) | x + y | ..^ \ No newline at end of file diff --git a/checker/src/test/resources/listIndexTypeError.baseline b/checker/src/test/resources/listIndexTypeError.baseline index 3bab400ef..c0fca99bf 100644 --- a/checker/src/test/resources/listIndexTypeError.baseline +++ b/checker/src/test/resources/listIndexTypeError.baseline @@ -1,8 +1,8 @@ Source: x[1u] declare x { - value list(dev.cel.testing.testdata.proto3.TestAllTypes) + value list(cel.expr.conformance.proto3.TestAllTypes) } =====> -ERROR: test_location:1:2: found no matching overload for '_[_]' applied to '(list(dev.cel.testing.testdata.proto3.TestAllTypes), uint)' (candidates: (list(%A0), int),(map(%A1, %B2), %A1)) +ERROR: test_location:1:2: found no matching overload for '_[_]' applied to '(list(cel.expr.conformance.proto3.TestAllTypes), uint)' (candidates: (list(%A0), int),(map(%A1, %B2), %A1)) | x[1u] | .^ diff --git a/checker/src/test/resources/listOperators.baseline b/checker/src/test/resources/listOperators.baseline index 7b3a3c154..5ca16ba91 100644 --- a/checker/src/test/resources/listOperators.baseline +++ b/checker/src/test/resources/listOperators.baseline @@ -1,30 +1,30 @@ Source: (x + x)[1].single_int32 == size(x) declare x { - value list(dev.cel.testing.testdata.proto3.TestAllTypes) + value list(cel.expr.conformance.proto3.TestAllTypes) } =====> _==_( _[_]( _+_( - x~list(dev.cel.testing.testdata.proto3.TestAllTypes)^x, - x~list(dev.cel.testing.testdata.proto3.TestAllTypes)^x - )~list(dev.cel.testing.testdata.proto3.TestAllTypes)^add_list, + x~list(cel.expr.conformance.proto3.TestAllTypes)^x, + x~list(cel.expr.conformance.proto3.TestAllTypes)^x + )~list(cel.expr.conformance.proto3.TestAllTypes)^add_list, 1~int - )~dev.cel.testing.testdata.proto3.TestAllTypes^index_list.single_int32~int, + )~cel.expr.conformance.proto3.TestAllTypes^index_list.single_int32~int, size( - x~list(dev.cel.testing.testdata.proto3.TestAllTypes)^x + x~list(cel.expr.conformance.proto3.TestAllTypes)^x )~int^size_list )~bool^equals Source: x.size() == size(x) declare x { - value list(dev.cel.testing.testdata.proto3.TestAllTypes) + value list(cel.expr.conformance.proto3.TestAllTypes) } =====> _==_( - x~list(dev.cel.testing.testdata.proto3.TestAllTypes)^x.size()~int^list_size, + x~list(cel.expr.conformance.proto3.TestAllTypes)^x.size()~int^list_size, size( - x~list(dev.cel.testing.testdata.proto3.TestAllTypes)^x + x~list(cel.expr.conformance.proto3.TestAllTypes)^x )~int^size_list )~bool^equals diff --git a/checker/src/test/resources/listRepeatedOperators.baseline b/checker/src/test/resources/listRepeatedOperators.baseline index 6f5fa76c8..e04335f98 100644 --- a/checker/src/test/resources/listRepeatedOperators.baseline +++ b/checker/src/test/resources/listRepeatedOperators.baseline @@ -1,12 +1,12 @@ Source: x.repeated_int64[x.single_int32] == 23 declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> _==_( _[_]( - x~dev.cel.testing.testdata.proto3.TestAllTypes^x.repeated_int64~list(int), - x~dev.cel.testing.testdata.proto3.TestAllTypes^x.single_int32~int + x~cel.expr.conformance.proto3.TestAllTypes^x.repeated_int64~list(int), + x~cel.expr.conformance.proto3.TestAllTypes^x.single_int32~int )~int^index_list, 23~int )~bool^equals diff --git a/checker/src/test/resources/mapEmpty.baseline b/checker/src/test/resources/mapEmpty.baseline index db6d5b65b..d8ec99820 100644 --- a/checker/src/test/resources/mapEmpty.baseline +++ b/checker/src/test/resources/mapEmpty.baseline @@ -1,11 +1,11 @@ Source: size(x.map_int64_nested_type) == 0 declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> _==_( size( - x~dev.cel.testing.testdata.proto3.TestAllTypes^x.map_int64_nested_type~map(int, dev.cel.testing.testdata.proto3.NestedTestAllTypes) + x~cel.expr.conformance.proto3.TestAllTypes^x.map_int64_nested_type~map(int, cel.expr.conformance.proto3.NestedTestAllTypes) )~int^size_map, 0~int )~bool^equals diff --git a/checker/src/test/resources/mapExpr.baseline b/checker/src/test/resources/mapExpr.baseline index eb3739e61..2f7964bc0 100644 --- a/checker/src/test/resources/mapExpr.baseline +++ b/checker/src/test/resources/mapExpr.baseline @@ -1,22 +1,22 @@ Source: x.repeated_int64.map(x, double(x)) declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> __comprehension__( // Variable x, // Target - x~dev.cel.testing.testdata.proto3.TestAllTypes^x.repeated_int64~list(int), + x~cel.expr.conformance.proto3.TestAllTypes^x.repeated_int64~list(int), // Accumulator - __result__, + @result, // Init []~list(double), // LoopCondition true~bool, // LoopStep _+_( - __result__~list(double)^__result__, + @result~list(double)^@result, [ double( x~int^x @@ -24,20 +24,20 @@ __comprehension__( ]~list(double) )~list(double)^add_list, // Result - __result__~list(double)^__result__)~list(double) + @result~list(double)^@result)~list(double) Source: [].map(x, [].map(y, x in y && y in x)) declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> ERROR: test_location:1:33: found no matching overload for '@in' applied to '(list(%elem0), %elem0)' (candidates: (%A7, list(%A7)),(%A8, map(%A8, %B9))) - | [].map(x, [].map(y, x in y && y in x)) - | ................................^ + | [].map(x, [].map(y, x in y && y in x)) + | ................................^ Source: [{}.map(c,c,c)]+[{}.map(c,c,c)] declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> _+_( @@ -48,7 +48,7 @@ _+_( // Target {}~map(bool, dyn), // Accumulator - __result__, + @result, // Init []~list(bool), // LoopCondition @@ -57,15 +57,15 @@ _+_( _?_:_( c~bool^c, _+_( - __result__~list(bool)^__result__, + @result~list(bool)^@result, [ c~bool^c ]~list(bool) )~list(bool)^add_list, - __result__~list(bool)^__result__ + @result~list(bool)^@result )~list(bool)^conditional, // Result - __result__~list(bool)^__result__)~list(bool) + @result~list(bool)^@result)~list(bool) ]~list(list(bool)), [ __comprehension__( @@ -74,7 +74,7 @@ _+_( // Target {}~map(bool, dyn), // Accumulator - __result__, + @result, // Init []~list(bool), // LoopCondition @@ -83,14 +83,14 @@ _+_( _?_:_( c~bool^c, _+_( - __result__~list(bool)^__result__, + @result~list(bool)^@result, [ c~bool^c ]~list(bool) )~list(bool)^add_list, - __result__~list(bool)^__result__ + @result~list(bool)^@result )~list(bool)^conditional, // Result - __result__~list(bool)^__result__)~list(bool) + @result~list(bool)^@result)~list(bool) ]~list(list(bool)) -)~list(list(bool))^add_list +)~list(list(bool))^add_list \ No newline at end of file diff --git a/checker/src/test/resources/mapFilterExpr.baseline b/checker/src/test/resources/mapFilterExpr.baseline index b7b56ee88..1a66e493c 100644 --- a/checker/src/test/resources/mapFilterExpr.baseline +++ b/checker/src/test/resources/mapFilterExpr.baseline @@ -1,15 +1,15 @@ Source: x.repeated_int64.map(x, x > 0, double(x)) declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> __comprehension__( // Variable x, // Target - x~dev.cel.testing.testdata.proto3.TestAllTypes^x.repeated_int64~list(int), + x~cel.expr.conformance.proto3.TestAllTypes^x.repeated_int64~list(int), // Accumulator - __result__, + @result, // Init []~list(double), // LoopCondition @@ -21,21 +21,21 @@ __comprehension__( 0~int )~bool^greater_int64, _+_( - __result__~list(double)^__result__, + @result~list(double)^@result, [ double( x~int^x )~double^int64_to_double ]~list(double) )~list(double)^add_list, - __result__~list(double)^__result__ + @result~list(double)^@result )~list(double)^conditional, // Result - __result__~list(double)^__result__)~list(double) + @result~list(double)^@result)~list(double) Source: lists.filter(x, x > 1.5) declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare lists { value dyn @@ -47,7 +47,7 @@ __comprehension__( // Target lists~dyn^lists, // Accumulator - __result__, + @result, // Init []~list(dyn), // LoopCondition @@ -59,19 +59,19 @@ __comprehension__( 1.5~double )~bool^greater_double|greater_int64_double|greater_uint64_double, _+_( - __result__~list(dyn)^__result__, + @result~list(dyn)^@result, [ x~dyn^x ]~list(dyn) )~list(dyn)^add_list, - __result__~list(dyn)^__result__ + @result~list(dyn)^@result )~list(dyn)^conditional, // Result - __result__~list(dyn)^__result__)~list(dyn) + @result~list(dyn)^@result)~list(dyn) Source: args.user["myextension"].customAttributes.filter(x, x.name == "hobbies") declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare lists { value dyn @@ -89,7 +89,7 @@ __comprehension__( "myextension"~string )~dyn^index_map.customAttributes~dyn, // Accumulator - __result__, + @result, // Init []~list(dyn), // LoopCondition @@ -101,12 +101,12 @@ __comprehension__( "hobbies"~string )~bool^equals, _+_( - __result__~list(dyn)^__result__, + @result~list(dyn)^@result, [ x~dyn^x ]~list(dyn) )~list(dyn)^add_list, - __result__~list(dyn)^__result__ + @result~list(dyn)^@result )~list(dyn)^conditional, // Result - __result__~list(dyn)^__result__)~list(dyn) + @result~list(dyn)^@result)~list(dyn) \ No newline at end of file diff --git a/checker/src/test/resources/mapIndexTypeError.baseline b/checker/src/test/resources/mapIndexTypeError.baseline index b681e067d..02fee54dd 100644 --- a/checker/src/test/resources/mapIndexTypeError.baseline +++ b/checker/src/test/resources/mapIndexTypeError.baseline @@ -1,8 +1,8 @@ Source: x[2].single_int32 == 23 declare x { - value map(string, dev.cel.testing.testdata.proto3.TestAllTypes) + value map(string, cel.expr.conformance.proto3.TestAllTypes) } =====> -ERROR: test_location:1:2: found no matching overload for '_[_]' applied to '(map(string, dev.cel.testing.testdata.proto3.TestAllTypes), int)' (candidates: (list(%A0), int),(map(%A1, %B2), %A1)) +ERROR: test_location:1:2: found no matching overload for '_[_]' applied to '(map(string, cel.expr.conformance.proto3.TestAllTypes), int)' (candidates: (list(%A0), int),(map(%A1, %B2), %A1)) | x[2].single_int32 == 23 | .^ diff --git a/checker/src/test/resources/mapOperators.baseline b/checker/src/test/resources/mapOperators.baseline index 703b78898..cf040f056 100644 --- a/checker/src/test/resources/mapOperators.baseline +++ b/checker/src/test/resources/mapOperators.baseline @@ -1,25 +1,25 @@ Source: x["a"].single_int32 == 23 declare x { - value map(string, dev.cel.testing.testdata.proto3.TestAllTypes) + value map(string, cel.expr.conformance.proto3.TestAllTypes) } =====> _==_( _[_]( - x~map(string, dev.cel.testing.testdata.proto3.TestAllTypes)^x, + x~map(string, cel.expr.conformance.proto3.TestAllTypes)^x, "a"~string - )~dev.cel.testing.testdata.proto3.TestAllTypes^index_map.single_int32~int, + )~cel.expr.conformance.proto3.TestAllTypes^index_map.single_int32~int, 23~int )~bool^equals Source: x.size() == size(x) declare x { - value map(string, dev.cel.testing.testdata.proto3.TestAllTypes) + value map(string, cel.expr.conformance.proto3.TestAllTypes) } =====> _==_( - x~map(string, dev.cel.testing.testdata.proto3.TestAllTypes)^x.size()~int^map_size, + x~map(string, cel.expr.conformance.proto3.TestAllTypes)^x.size()~int^map_size, size( - x~map(string, dev.cel.testing.testdata.proto3.TestAllTypes)^x + x~map(string, cel.expr.conformance.proto3.TestAllTypes)^x )~int^size_map )~bool^equals diff --git a/checker/src/test/resources/messageCreationError.baseline b/checker/src/test/resources/messageCreationError.baseline new file mode 100644 index 000000000..790dc6e0c --- /dev/null +++ b/checker/src/test/resources/messageCreationError.baseline @@ -0,0 +1,41 @@ +Source: x{foo: 1} +declare x { + value int +} +=====> +ERROR: test_location:1:2: 'int' is not a type + | x{foo: 1} + | .^ +ERROR: test_location:1:6: Message type resolution failure while referencing field 'foo'. + | x{foo: 1} + | .....^ + +Source: y{foo: 1} +declare x { + value int +} +declare y { + value type(int) +} +=====> +ERROR: test_location:1:2: 'int' is not a message type + | y{foo: 1} + | .^ +ERROR: test_location:1:6: Message type resolution failure while referencing field 'foo'. + | y{foo: 1} + | .....^ + +Source: z{foo: 1} +declare x { + value int +} +declare y { + value type(int) +} +declare z { + value type(msg_without_descriptor) +} +=====> +ERROR: test_location:1:6: Message type resolution failure while referencing field 'foo'. Ensure that the descriptor for type 'msg_without_descriptor' was added to the environment + | z{foo: 1} + | .....^ diff --git a/checker/src/test/resources/messageFieldSelect.baseline b/checker/src/test/resources/messageFieldSelect.baseline index dd4cf8723..2247ce751 100644 --- a/checker/src/test/resources/messageFieldSelect.baseline +++ b/checker/src/test/resources/messageFieldSelect.baseline @@ -1,22 +1,22 @@ Source: x.single_nested_message.bb == 43 && has(x.single_nested_message) && has(x.single_int32) && has(x.repeated_int32) && has(x.map_int64_nested_type) declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> _&&_( _&&_( _&&_( _==_( - x~dev.cel.testing.testdata.proto3.TestAllTypes^x.single_nested_message~dev.cel.testing.testdata.proto3.TestAllTypes.NestedMessage.bb~int, + x~cel.expr.conformance.proto3.TestAllTypes^x.single_nested_message~cel.expr.conformance.proto3.TestAllTypes.NestedMessage.bb~int, 43~int )~bool^equals, - x~dev.cel.testing.testdata.proto3.TestAllTypes^x.single_nested_message~test-only~~bool + x~cel.expr.conformance.proto3.TestAllTypes^x.single_nested_message~test-only~~bool )~bool^logical_and, - x~dev.cel.testing.testdata.proto3.TestAllTypes^x.single_int32~test-only~~bool + x~cel.expr.conformance.proto3.TestAllTypes^x.single_int32~test-only~~bool )~bool^logical_and, _&&_( - x~dev.cel.testing.testdata.proto3.TestAllTypes^x.repeated_int32~test-only~~bool, - x~dev.cel.testing.testdata.proto3.TestAllTypes^x.map_int64_nested_type~test-only~~bool + x~cel.expr.conformance.proto3.TestAllTypes^x.repeated_int32~test-only~~bool, + x~cel.expr.conformance.proto3.TestAllTypes^x.map_int64_nested_type~test-only~~bool )~bool^logical_and )~bool^logical_and diff --git a/checker/src/test/resources/messageFieldSelectError.baseline b/checker/src/test/resources/messageFieldSelectError.baseline index 3bb8538ad..2bf3ad7f7 100644 --- a/checker/src/test/resources/messageFieldSelectError.baseline +++ b/checker/src/test/resources/messageFieldSelectError.baseline @@ -1,6 +1,6 @@ Source: x.single_nested_message.undefined == x.undefined declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> ERROR: test_location:1:24: undefined field 'undefined' diff --git a/checker/src/test/resources/namespacedFunctions.baseline b/checker/src/test/resources/namespacedFunctions.baseline index 449ea5110..b6ace0e3e 100644 --- a/checker/src/test/resources/namespacedFunctions.baseline +++ b/checker/src/test/resources/namespacedFunctions.baseline @@ -84,14 +84,14 @@ __comprehension__( )~int^ns_func_overload ]~list(int), // Accumulator - __result__, + @result, // Init []~list(int), // LoopCondition true~bool, // LoopStep _+_( - __result__~list(int)^__result__, + @result~list(int)^@result, [ _*_( x~int^x, @@ -100,7 +100,7 @@ __comprehension__( ]~list(int) )~list(int)^add_list, // Result - __result__~list(int)^__result__)~list(int) + @result~list(int)^@result)~list(int) Source: [1, 2].map(x, x * ns.func('test')) declare ns.func { @@ -119,14 +119,14 @@ __comprehension__( 2~int ]~list(int), // Accumulator - __result__, + @result, // Init []~list(int), // LoopCondition true~bool, // LoopStep _+_( - __result__~list(int)^__result__, + @result~list(int)^@result, [ _*_( x~int^x, @@ -137,7 +137,7 @@ __comprehension__( ]~list(int) )~list(int)^add_list, // Result - __result__~list(int)^__result__)~list(int) + @result~list(int)^@result)~list(int) Source: func('hello') declare ns.func { @@ -165,4 +165,4 @@ ns.func( ns.func( "test"~string )~int^ns_func_overload -)~int^ns_member_overload +)~int^ns_member_overload \ No newline at end of file diff --git a/checker/src/test/resources/namespacedVariables.baseline b/checker/src/test/resources/namespacedVariables.baseline index 9e0ccfe85..b7594c820 100644 --- a/checker/src/test/resources/namespacedVariables.baseline +++ b/checker/src/test/resources/namespacedVariables.baseline @@ -9,8 +9,8 @@ Source: msgVar.single_int32 declare ns.x { value int } -declare dev.cel.testing.testdata.proto3.msgVar { - value dev.cel.testing.testdata.proto3.TestAllTypes +declare cel.expr.conformance.proto3.msgVar { + value cel.expr.conformance.proto3.TestAllTypes } =====> -dev.cel.testing.testdata.proto3.msgVar~dev.cel.testing.testdata.proto3.TestAllTypes^dev.cel.testing.testdata.proto3.msgVar.single_int32~int +cel.expr.conformance.proto3.msgVar~cel.expr.conformance.proto3.TestAllTypes^cel.expr.conformance.proto3.msgVar.single_int32~int diff --git a/checker/src/test/resources/nestedEnums.baseline b/checker/src/test/resources/nestedEnums.baseline index 3a2869a75..f05594997 100644 --- a/checker/src/test/resources/nestedEnums.baseline +++ b/checker/src/test/resources/nestedEnums.baseline @@ -1,16 +1,16 @@ Source: x.single_nested_enum == TestAllTypes.NestedEnum.BAR declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> _==_( - x~dev.cel.testing.testdata.proto3.TestAllTypes^x.single_nested_enum~int, - dev.cel.testing.testdata.proto3.TestAllTypes.NestedEnum.BAR~int^dev.cel.testing.testdata.proto3.TestAllTypes.NestedEnum.BAR + x~cel.expr.conformance.proto3.TestAllTypes^x.single_nested_enum~int, + cel.expr.conformance.proto3.TestAllTypes.NestedEnum.BAR~int^cel.expr.conformance.proto3.TestAllTypes.NestedEnum.BAR )~bool^equals Source: single_nested_enum == TestAllTypes.NestedEnum.BAR declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare single_nested_enum { value int @@ -18,20 +18,20 @@ declare single_nested_enum { =====> _==_( single_nested_enum~int^single_nested_enum, - dev.cel.testing.testdata.proto3.TestAllTypes.NestedEnum.BAR~int^dev.cel.testing.testdata.proto3.TestAllTypes.NestedEnum.BAR + cel.expr.conformance.proto3.TestAllTypes.NestedEnum.BAR~int^cel.expr.conformance.proto3.TestAllTypes.NestedEnum.BAR )~bool^equals Source: TestAllTypes{single_nested_enum : TestAllTypes.NestedEnum.BAR}.single_nested_enum == 1 declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare single_nested_enum { value int } =====> _==_( - TestAllTypes{ - single_nested_enum:dev.cel.testing.testdata.proto3.TestAllTypes.NestedEnum.BAR~int^dev.cel.testing.testdata.proto3.TestAllTypes.NestedEnum.BAR - }~dev.cel.testing.testdata.proto3.TestAllTypes^dev.cel.testing.testdata.proto3.TestAllTypes.single_nested_enum~int, + cel.expr.conformance.proto3.TestAllTypes{ + single_nested_enum:cel.expr.conformance.proto3.TestAllTypes.NestedEnum.BAR~int^cel.expr.conformance.proto3.TestAllTypes.NestedEnum.BAR + }~cel.expr.conformance.proto3.TestAllTypes^cel.expr.conformance.proto3.TestAllTypes.single_nested_enum~int, 1~int -)~bool^equals +)~bool^equals \ No newline at end of file diff --git a/checker/src/test/resources/nullableMessage.baseline b/checker/src/test/resources/nullableMessage.baseline index e2db74762..54f8e56d5 100644 --- a/checker/src/test/resources/nullableMessage.baseline +++ b/checker/src/test/resources/nullableMessage.baseline @@ -1,25 +1,25 @@ Source: x.single_nested_message != null declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> _!=_( - x~dev.cel.testing.testdata.proto3.TestAllTypes^x.single_nested_message~dev.cel.testing.testdata.proto3.TestAllTypes.NestedMessage, + x~cel.expr.conformance.proto3.TestAllTypes^x.single_nested_message~cel.expr.conformance.proto3.TestAllTypes.NestedMessage, null~null )~bool^not_equals Source: null == TestAllTypes{} || TestAllTypes{} == null declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> _||_( _==_( null~null, - TestAllTypes{}~dev.cel.testing.testdata.proto3.TestAllTypes^dev.cel.testing.testdata.proto3.TestAllTypes + cel.expr.conformance.proto3.TestAllTypes{}~cel.expr.conformance.proto3.TestAllTypes^cel.expr.conformance.proto3.TestAllTypes )~bool^equals, _==_( - TestAllTypes{}~dev.cel.testing.testdata.proto3.TestAllTypes^dev.cel.testing.testdata.proto3.TestAllTypes, + cel.expr.conformance.proto3.TestAllTypes{}~cel.expr.conformance.proto3.TestAllTypes^cel.expr.conformance.proto3.TestAllTypes, null~null )~bool^equals -)~bool^logical_or +)~bool^logical_or \ No newline at end of file diff --git a/checker/src/test/resources/nullablePrimitiveError.baseline b/checker/src/test/resources/nullablePrimitiveError.baseline index 43a50447f..202497f03 100644 --- a/checker/src/test/resources/nullablePrimitiveError.baseline +++ b/checker/src/test/resources/nullablePrimitiveError.baseline @@ -1,6 +1,6 @@ Source: x.single_int64 != null declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> ERROR: test_location:1:16: found no matching overload for '_!=_' applied to '(int, null)' (candidates: (%A0, %A0)) diff --git a/checker/src/test/resources/nullableWrapper.baseline b/checker/src/test/resources/nullableWrapper.baseline index 4e6deb477..d8e323193 100644 --- a/checker/src/test/resources/nullableWrapper.baseline +++ b/checker/src/test/resources/nullableWrapper.baseline @@ -1,10 +1,10 @@ Source: x.single_int64_wrapper == null declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> _==_( - x~dev.cel.testing.testdata.proto3.TestAllTypes^x.single_int64_wrapper~wrapper(int), + x~cel.expr.conformance.proto3.TestAllTypes^x.single_int64_wrapper~wrapper(int), null~null )~bool^equals diff --git a/checker/src/test/resources/operatorsConditional.baseline b/checker/src/test/resources/operatorsConditional.baseline index 96c64bbf6..f567eb9d5 100644 --- a/checker/src/test/resources/operatorsConditional.baseline +++ b/checker/src/test/resources/operatorsConditional.baseline @@ -1,10 +1,10 @@ Source: false ? x.single_timestamp : null declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> _?_:_( false~bool, - x~dev.cel.testing.testdata.proto3.TestAllTypes^x.single_timestamp~google.protobuf.Timestamp, + x~cel.expr.conformance.proto3.TestAllTypes^x.single_timestamp~google.protobuf.Timestamp, null~null )~google.protobuf.Timestamp^conditional diff --git a/checker/src/test/resources/optionals.baseline b/checker/src/test/resources/optionals.baseline index cc534f514..be50e44ba 100644 --- a/checker/src/test/resources/optionals.baseline +++ b/checker/src/test/resources/optionals.baseline @@ -72,12 +72,12 @@ Source: {?'key': {'a': 'b'}.?value}.key Source: TestAllTypes{?single_int32: {}.?i} =====> -TestAllTypes{ +cel.expr.conformance.proto3.TestAllTypes{ ?single_int32:_?._( {}~map(dyn, int), "i" )~optional_type(int)^select_optional_field -}~dev.cel.testing.testdata.proto3.TestAllTypes^dev.cel.testing.testdata.proto3.TestAllTypes +}~cel.expr.conformance.proto3.TestAllTypes^cel.expr.conformance.proto3.TestAllTypes Source: [?a, ?b, 'world'] declare a { @@ -118,4 +118,4 @@ declare b { { ?"str"~string:a~optional_type(string)^a, 2~int:3~int -}~map(dyn, dyn) +}~map(dyn, dyn) \ No newline at end of file diff --git a/checker/src/test/resources/proto2PrimitiveField.baseline b/checker/src/test/resources/proto2PrimitiveField.baseline index 1842b1c5e..47546cc8b 100644 --- a/checker/src/test/resources/proto2PrimitiveField.baseline +++ b/checker/src/test/resources/proto2PrimitiveField.baseline @@ -1,18 +1,18 @@ Source: x.single_fixed32 != 0u && x.single_fixed64 > 1u && x.single_int32 != null declare x { - value dev.cel.testing.testdata.proto2.Proto2Message + value cel.expr.conformance.proto2.TestAllTypes } =====> ERROR: test_location:1:67: found no matching overload for '_!=_' applied to '(int, null)' (candidates: (%A1, %A1)) - | x.single_fixed32 != 0u && x.single_fixed64 > 1u && x.single_int32 != null - | ..................................................................^ + | x.single_fixed32 != 0u && x.single_fixed64 > 1u && x.single_int32 != null + | ..................................................................^ Source: x.nestedgroup.single_name == '' declare x { - value dev.cel.testing.testdata.proto2.Proto2Message + value cel.expr.conformance.proto2.TestAllTypes } =====> _==_( - x~dev.cel.testing.testdata.proto2.Proto2Message^x.nestedgroup~dev.cel.testing.testdata.proto2.Proto2Message.NestedGroup.single_name~string, + x~cel.expr.conformance.proto2.TestAllTypes^x.nestedgroup~cel.expr.conformance.proto2.TestAllTypes.NestedGroup.single_name~string, ""~string )~bool^equals diff --git a/checker/src/test/resources/quantifiers.baseline b/checker/src/test/resources/quantifiers.baseline index 3f4d99bb5..3f204a6f5 100644 --- a/checker/src/test/resources/quantifiers.baseline +++ b/checker/src/test/resources/quantifiers.baseline @@ -1,6 +1,6 @@ Source: x.repeated_int64.all(e, e > 0) && x.repeated_int64.exists(e, e < 0) && x.repeated_int64.exists_one(e, e == 0) declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> _&&_( @@ -9,58 +9,58 @@ _&&_( // Variable e, // Target - x~dev.cel.testing.testdata.proto3.TestAllTypes^x.repeated_int64~list(int), + x~cel.expr.conformance.proto3.TestAllTypes^x.repeated_int64~list(int), // Accumulator - __result__, + @result, // Init true~bool, // LoopCondition @not_strictly_false( - __result__~bool^__result__ + @result~bool^@result )~bool^not_strictly_false, // LoopStep _&&_( - __result__~bool^__result__, + @result~bool^@result, _>_( e~int^e, 0~int )~bool^greater_int64 )~bool^logical_and, // Result - __result__~bool^__result__)~bool, + @result~bool^@result)~bool, __comprehension__( // Variable e, // Target - x~dev.cel.testing.testdata.proto3.TestAllTypes^x.repeated_int64~list(int), + x~cel.expr.conformance.proto3.TestAllTypes^x.repeated_int64~list(int), // Accumulator - __result__, + @result, // Init false~bool, // LoopCondition @not_strictly_false( !_( - __result__~bool^__result__ + @result~bool^@result )~bool^logical_not )~bool^not_strictly_false, // LoopStep _||_( - __result__~bool^__result__, + @result~bool^@result, _<_( e~int^e, 0~int )~bool^less_int64 )~bool^logical_or, // Result - __result__~bool^__result__)~bool + @result~bool^@result)~bool )~bool^logical_and, __comprehension__( // Variable e, // Target - x~dev.cel.testing.testdata.proto3.TestAllTypes^x.repeated_int64~list(int), + x~cel.expr.conformance.proto3.TestAllTypes^x.repeated_int64~list(int), // Accumulator - __result__, + @result, // Init 0~int, // LoopCondition @@ -72,14 +72,14 @@ _&&_( 0~int )~bool^equals, _+_( - __result__~int^__result__, + @result~int^@result, 1~int )~int^add_int64, - __result__~int^__result__ + @result~int^@result )~int^conditional, // Result _==_( - __result__~int^__result__, + @result~int^@result, 1~int )~bool^equals)~bool -)~bool^logical_and +)~bool^logical_and \ No newline at end of file diff --git a/checker/src/test/resources/quantifiersErrors.baseline b/checker/src/test/resources/quantifiersErrors.baseline index 052d9a38c..17c1736c5 100644 --- a/checker/src/test/resources/quantifiersErrors.baseline +++ b/checker/src/test/resources/quantifiersErrors.baseline @@ -1,9 +1,9 @@ Source: x.all(e, 0) declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> -ERROR: test_location:1:1: expression of type 'dev.cel.testing.testdata.proto3.TestAllTypes' cannot be range of a comprehension (must be list, map, or dynamic) +ERROR: test_location:1:1: expression of type 'cel.expr.conformance.proto3.TestAllTypes' cannot be range of a comprehension (must be list, map, or dynamic) | x.all(e, 0) | ^ ERROR: test_location:1:6: found no matching overload for '_&&_' applied to '(bool, int)' (candidates: (bool, bool)) diff --git a/checker/src/test/resources/referenceTypeAbsolute.baseline b/checker/src/test/resources/referenceTypeAbsolute.baseline index 12057f1ce..401e6fe0c 100644 --- a/checker/src/test/resources/referenceTypeAbsolute.baseline +++ b/checker/src/test/resources/referenceTypeAbsolute.baseline @@ -1,3 +1,36 @@ -Source: .dev.cel.testing.testdata.proto3.TestAllTypes +Source: .cel.expr.conformance.proto3.TestAllTypes =====> -dev.cel.testing.testdata.proto3.TestAllTypes~type(dev.cel.testing.testdata.proto3.TestAllTypes)^dev.cel.testing.testdata.proto3.TestAllTypes +cel.expr.conformance.proto3.TestAllTypes~type(cel.expr.conformance.proto3.TestAllTypes)^cel.expr.conformance.proto3.TestAllTypes + +Source: [0].exists(app, .app.config == 1) +declare app.config { + value int +} +=====> +__comprehension__( + // Variable + app, + // Target + [ + 0~int + ]~list(int), + // Accumulator + @result, + // Init + false~bool, + // LoopCondition + @not_strictly_false( + !_( + @result~bool^@result + )~bool^logical_not + )~bool^not_strictly_false, + // LoopStep + _||_( + @result~bool^@result, + _==_( + .app.config~int^.app.config, + 1~int + )~bool^equals + )~bool^logical_or, + // Result + @result~bool^@result)~bool diff --git a/checker/src/test/resources/referenceTypeRelative.baseline b/checker/src/test/resources/referenceTypeRelative.baseline index 80e386607..e0c681d61 100644 --- a/checker/src/test/resources/referenceTypeRelative.baseline +++ b/checker/src/test/resources/referenceTypeRelative.baseline @@ -1,3 +1,3 @@ Source: proto3.TestAllTypes =====> -dev.cel.testing.testdata.proto3.TestAllTypes~type(dev.cel.testing.testdata.proto3.TestAllTypes)^dev.cel.testing.testdata.proto3.TestAllTypes +cel.expr.conformance.proto3.TestAllTypes~type(cel.expr.conformance.proto3.TestAllTypes)^cel.expr.conformance.proto3.TestAllTypes diff --git a/checker/src/test/resources/referenceValue.baseline b/checker/src/test/resources/referenceValue.baseline index b6ed9f23b..165b0bc6b 100644 --- a/checker/src/test/resources/referenceValue.baseline +++ b/checker/src/test/resources/referenceValue.baseline @@ -1,6 +1,6 @@ Source: x declare container.x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> -container.x~dev.cel.testing.testdata.proto3.TestAllTypes^container.x +container.x~cel.expr.conformance.proto3.TestAllTypes^container.x diff --git a/checker/src/test/resources/standardEnvDump.baseline b/checker/src/test/resources/standardEnvDump.baseline index 4ef2fa9cf..864bfd340 100644 --- a/checker/src/test/resources/standardEnvDump.baseline +++ b/checker/src/test/resources/standardEnvDump.baseline @@ -4,6 +4,10 @@ Source: 'redundant expression so the env is constructed and can be printed' Standard environment: +declare cel.@mapInsert { + function cel_@mapInsert_map_map (map(K, V), map(K, V)) -> map(K, V) + function cel_@mapInsert_map_key_value (map(K, V), K, V) -> map(K, V) +} declare !_ { function logical_not (bool) -> bool } @@ -144,8 +148,15 @@ declare _||_ { declare bool { value type(bool) } +declare bool { + function bool_to_bool (bool) -> bool + function string_to_bool (string) -> bool +} declare bytes { value type(bytes) +} +declare bytes { + function bytes_to_bytes (bytes) -> bytes function string_to_bytes (string) -> bytes } declare contains { @@ -153,15 +164,21 @@ declare contains { } declare double { value type(double) +} +declare double { + function double_to_double (double) -> double function int64_to_double (int) -> double function uint64_to_double (uint) -> double function string_to_double (string) -> double } declare duration { + function duration_to_duration (google.protobuf.Duration) -> google.protobuf.Duration function string_to_duration (string) -> google.protobuf.Duration } declare dyn { value type(dyn) +} +declare dyn { function to_dyn (A) -> dyn } declare endsWith { @@ -213,6 +230,9 @@ declare getSeconds { } declare int { value type(int) +} +declare int { + function int64_to_int64 (int) -> int function uint64_to_int64 (uint) -> int function double_to_int64 (double) -> int function string_to_int64 (string) -> int @@ -220,11 +240,9 @@ declare int { } declare list { value type(list(dyn)) - function to_list (type(A), list(A)) -> list(A) } declare map { value type(map(dyn, dyn)) - function to_map (type(A), type(B), map(A, B)) -> map(A, B) } declare matches { function matches (string, string) -> bool @@ -248,24 +266,34 @@ declare startsWith { } declare string { value type(string) +} +declare string { + function string_to_string (string) -> string function int64_to_string (int) -> string function uint64_to_string (uint) -> string function double_to_string (double) -> string + function bool_to_string (bool) -> string function bytes_to_string (bytes) -> string function timestamp_to_string (google.protobuf.Timestamp) -> string function duration_to_string (google.protobuf.Duration) -> string } declare timestamp { function string_to_timestamp (string) -> google.protobuf.Timestamp + function timestamp_to_timestamp (google.protobuf.Timestamp) -> google.protobuf.Timestamp function int64_to_timestamp (int) -> google.protobuf.Timestamp } declare type { value type(dyn) +} +declare type { function type (A) -> type(A) } declare uint { value type(uint) +} +declare uint { + function uint64_to_uint64 (uint) -> uint function int64_to_uint64 (int) -> uint function double_to_uint64 (double) -> uint function string_to_uint64 (string) -> uint -} +} \ No newline at end of file diff --git a/checker/src/test/resources/twoVarComprehensions_allMacro.baseline b/checker/src/test/resources/twoVarComprehensions_allMacro.baseline new file mode 100644 index 000000000..6a6de8b75 --- /dev/null +++ b/checker/src/test/resources/twoVarComprehensions_allMacro.baseline @@ -0,0 +1,126 @@ +Source: x.map_string_string.all(i, v, i < v) && x.repeated_int64.all(i, v, i < v) && [1, 2, 3, 4].all(i, v, i < 5 && v > 0) && {'a': 1, 'b': 2}.all(k, v, k.startsWith('a') && v == 1) +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +_&&_( + _&&_( + __comprehension__( + // Variable + i, + v, + // Target + x~cel.expr.conformance.proto3.TestAllTypes^x.map_string_string~map(string, string), + // Accumulator + @result, + // Init + true~bool, + // LoopCondition + @not_strictly_false( + @result~bool^@result + )~bool^not_strictly_false, + // LoopStep + _&&_( + @result~bool^@result, + _<_( + i~string^i, + v~string^v + )~bool^less_string + )~bool^logical_and, + // Result + @result~bool^@result)~bool, + __comprehension__( + // Variable + i, + v, + // Target + x~cel.expr.conformance.proto3.TestAllTypes^x.repeated_int64~list(int), + // Accumulator + @result, + // Init + true~bool, + // LoopCondition + @not_strictly_false( + @result~bool^@result + )~bool^not_strictly_false, + // LoopStep + _&&_( + @result~bool^@result, + _<_( + i~int^i, + v~int^v + )~bool^less_int64 + )~bool^logical_and, + // Result + @result~bool^@result)~bool + )~bool^logical_and, + _&&_( + __comprehension__( + // Variable + i, + v, + // Target + [ + 1~int, + 2~int, + 3~int, + 4~int + ]~list(int), + // Accumulator + @result, + // Init + true~bool, + // LoopCondition + @not_strictly_false( + @result~bool^@result + )~bool^not_strictly_false, + // LoopStep + _&&_( + @result~bool^@result, + _&&_( + _<_( + i~int^i, + 5~int + )~bool^less_int64, + _>_( + v~int^v, + 0~int + )~bool^greater_int64 + )~bool^logical_and + )~bool^logical_and, + // Result + @result~bool^@result)~bool, + __comprehension__( + // Variable + k, + v, + // Target + { + "a"~string:1~int, + "b"~string:2~int + }~map(string, int), + // Accumulator + @result, + // Init + true~bool, + // LoopCondition + @not_strictly_false( + @result~bool^@result + )~bool^not_strictly_false, + // LoopStep + _&&_( + @result~bool^@result, + _&&_( + k~string^k.startsWith( + "a"~string + )~bool^starts_with_string, + _==_( + v~int^v, + 1~int + )~bool^equals + )~bool^logical_and + )~bool^logical_and, + // Result + @result~bool^@result)~bool + )~bool^logical_and +)~bool^logical_and \ No newline at end of file diff --git a/checker/src/test/resources/twoVarComprehensions_duplicateIterVars.baseline b/checker/src/test/resources/twoVarComprehensions_duplicateIterVars.baseline new file mode 100644 index 000000000..d83030f27 --- /dev/null +++ b/checker/src/test/resources/twoVarComprehensions_duplicateIterVars.baseline @@ -0,0 +1,11 @@ +Source: x.repeated_int64.exists(i, i, i < v) +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +ERROR: test_location:1:1: overlapping declaration name 'i' (type 'int' cannot be distinguished from 'int') + | x.repeated_int64.exists(i, i, i < v) + | ^ +ERROR: test_location:1:35: undeclared reference to 'v' (in container '') + | x.repeated_int64.exists(i, i, i < v) + | ..................................^ \ No newline at end of file diff --git a/checker/src/test/resources/twoVarComprehensions_existsMacro.baseline b/checker/src/test/resources/twoVarComprehensions_existsMacro.baseline new file mode 100644 index 000000000..e11ab7866 --- /dev/null +++ b/checker/src/test/resources/twoVarComprehensions_existsMacro.baseline @@ -0,0 +1,134 @@ +Source: x.map_string_string.exists(i, v, i < v) && x.repeated_int64.exists(i, v, i < v) && [1, 2, 3, 4].exists(i, v, i < 5 && v > 0) && {'a': 1, 'b': 2}.exists(k, v, k.startsWith('a') && v == 1) +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +_&&_( + _&&_( + __comprehension__( + // Variable + i, + v, + // Target + x~cel.expr.conformance.proto3.TestAllTypes^x.map_string_string~map(string, string), + // Accumulator + @result, + // Init + false~bool, + // LoopCondition + @not_strictly_false( + !_( + @result~bool^@result + )~bool^logical_not + )~bool^not_strictly_false, + // LoopStep + _||_( + @result~bool^@result, + _<_( + i~string^i, + v~string^v + )~bool^less_string + )~bool^logical_or, + // Result + @result~bool^@result)~bool, + __comprehension__( + // Variable + i, + v, + // Target + x~cel.expr.conformance.proto3.TestAllTypes^x.repeated_int64~list(int), + // Accumulator + @result, + // Init + false~bool, + // LoopCondition + @not_strictly_false( + !_( + @result~bool^@result + )~bool^logical_not + )~bool^not_strictly_false, + // LoopStep + _||_( + @result~bool^@result, + _<_( + i~int^i, + v~int^v + )~bool^less_int64 + )~bool^logical_or, + // Result + @result~bool^@result)~bool + )~bool^logical_and, + _&&_( + __comprehension__( + // Variable + i, + v, + // Target + [ + 1~int, + 2~int, + 3~int, + 4~int + ]~list(int), + // Accumulator + @result, + // Init + false~bool, + // LoopCondition + @not_strictly_false( + !_( + @result~bool^@result + )~bool^logical_not + )~bool^not_strictly_false, + // LoopStep + _||_( + @result~bool^@result, + _&&_( + _<_( + i~int^i, + 5~int + )~bool^less_int64, + _>_( + v~int^v, + 0~int + )~bool^greater_int64 + )~bool^logical_and + )~bool^logical_or, + // Result + @result~bool^@result)~bool, + __comprehension__( + // Variable + k, + v, + // Target + { + "a"~string:1~int, + "b"~string:2~int + }~map(string, int), + // Accumulator + @result, + // Init + false~bool, + // LoopCondition + @not_strictly_false( + !_( + @result~bool^@result + )~bool^logical_not + )~bool^not_strictly_false, + // LoopStep + _||_( + @result~bool^@result, + _&&_( + k~string^k.startsWith( + "a"~string + )~bool^starts_with_string, + _==_( + v~int^v, + 1~int + )~bool^equals + )~bool^logical_and + )~bool^logical_or, + // Result + @result~bool^@result)~bool + )~bool^logical_and +)~bool^logical_and \ No newline at end of file diff --git a/checker/src/test/resources/twoVarComprehensions_existsOneMacro.baseline b/checker/src/test/resources/twoVarComprehensions_existsOneMacro.baseline new file mode 100644 index 000000000..ed393b921 --- /dev/null +++ b/checker/src/test/resources/twoVarComprehensions_existsOneMacro.baseline @@ -0,0 +1,146 @@ +Source: x.map_string_string.exists_one(i, v, i < v) && x.repeated_int64.exists_one(i, v, i < v) && [1, 2, 3, 4].exists_one(i, v, i < 5 && v > 0) && {'a': 1, 'b': 2}.exists_one(k, v, k.startsWith('a') && v == 1) +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +_&&_( + _&&_( + __comprehension__( + // Variable + i, + v, + // Target + x~cel.expr.conformance.proto3.TestAllTypes^x.map_string_string~map(string, string), + // Accumulator + @result, + // Init + 0~int, + // LoopCondition + true~bool, + // LoopStep + _?_:_( + _<_( + i~string^i, + v~string^v + )~bool^less_string, + _+_( + @result~int^@result, + 1~int + )~int^add_int64, + @result~int^@result + )~int^conditional, + // Result + _==_( + @result~int^@result, + 1~int + )~bool^equals)~bool, + __comprehension__( + // Variable + i, + v, + // Target + x~cel.expr.conformance.proto3.TestAllTypes^x.repeated_int64~list(int), + // Accumulator + @result, + // Init + 0~int, + // LoopCondition + true~bool, + // LoopStep + _?_:_( + _<_( + i~int^i, + v~int^v + )~bool^less_int64, + _+_( + @result~int^@result, + 1~int + )~int^add_int64, + @result~int^@result + )~int^conditional, + // Result + _==_( + @result~int^@result, + 1~int + )~bool^equals)~bool + )~bool^logical_and, + _&&_( + __comprehension__( + // Variable + i, + v, + // Target + [ + 1~int, + 2~int, + 3~int, + 4~int + ]~list(int), + // Accumulator + @result, + // Init + 0~int, + // LoopCondition + true~bool, + // LoopStep + _?_:_( + _&&_( + _<_( + i~int^i, + 5~int + )~bool^less_int64, + _>_( + v~int^v, + 0~int + )~bool^greater_int64 + )~bool^logical_and, + _+_( + @result~int^@result, + 1~int + )~int^add_int64, + @result~int^@result + )~int^conditional, + // Result + _==_( + @result~int^@result, + 1~int + )~bool^equals)~bool, + __comprehension__( + // Variable + k, + v, + // Target + { + "a"~string:1~int, + "b"~string:2~int + }~map(string, int), + // Accumulator + @result, + // Init + 0~int, + // LoopCondition + true~bool, + // LoopStep + _?_:_( + _&&_( + k~string^k.startsWith( + "a"~string + )~bool^starts_with_string, + _==_( + v~int^v, + 1~int + )~bool^equals + )~bool^logical_and, + _+_( + @result~int^@result, + 1~int + )~int^add_int64, + @result~int^@result + )~int^conditional, + // Result + _==_( + @result~int^@result, + 1~int + )~bool^equals)~bool + )~bool^logical_and +)~bool^logical_and \ No newline at end of file diff --git a/checker/src/test/resources/twoVarComprehensions_incorrectIterVars.baseline b/checker/src/test/resources/twoVarComprehensions_incorrectIterVars.baseline new file mode 100644 index 000000000..862dd5657 --- /dev/null +++ b/checker/src/test/resources/twoVarComprehensions_incorrectIterVars.baseline @@ -0,0 +1,11 @@ +Source: x.map_string_string.all(i + 1, v, i < v) && x.repeated_int64.all(i, v + 1, i < v) +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +ERROR: test_location:1:27: The argument must be a simple name + | x.map_string_string.all(i + 1, v, i < v) && x.repeated_int64.all(i, v + 1, i < v) + | ..........................^ +ERROR: test_location:1:71: The argument must be a simple name + | x.map_string_string.all(i + 1, v, i < v) && x.repeated_int64.all(i, v + 1, i < v) + | ......................................................................^ \ No newline at end of file diff --git a/checker/src/test/resources/twoVarComprehensions_incorrectNumberOfArgs.baseline b/checker/src/test/resources/twoVarComprehensions_incorrectNumberOfArgs.baseline new file mode 100644 index 000000000..08bfb51c0 --- /dev/null +++ b/checker/src/test/resources/twoVarComprehensions_incorrectNumberOfArgs.baseline @@ -0,0 +1,38 @@ +Source: [1, 2, 3, 4].exists_one(i, v, i < v, v)&& x.map_string_string.transformList(i, i < v) && [1, 2, 3].transformList(i, v, i > 0 && x < 3, (i * v) + v) == [4] +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +ERROR: test_location:1:24: undeclared reference to 'exists_one' (in container '') + | [1, 2, 3, 4].exists_one(i, v, i < v, v)&& x.map_string_string.transformList(i, i < v) && [1, 2, 3].transformList(i, v, i > 0 && x < 3, (i * v) + v) == [4] + | .......................^ +ERROR: test_location:1:25: undeclared reference to 'i' (in container '') + | [1, 2, 3, 4].exists_one(i, v, i < v, v)&& x.map_string_string.transformList(i, i < v) && [1, 2, 3].transformList(i, v, i > 0 && x < 3, (i * v) + v) == [4] + | ........................^ +ERROR: test_location:1:28: undeclared reference to 'v' (in container '') + | [1, 2, 3, 4].exists_one(i, v, i < v, v)&& x.map_string_string.transformList(i, i < v) && [1, 2, 3].transformList(i, v, i > 0 && x < 3, (i * v) + v) == [4] + | ...........................^ +ERROR: test_location:1:31: undeclared reference to 'i' (in container '') + | [1, 2, 3, 4].exists_one(i, v, i < v, v)&& x.map_string_string.transformList(i, i < v) && [1, 2, 3].transformList(i, v, i > 0 && x < 3, (i * v) + v) == [4] + | ..............................^ +ERROR: test_location:1:35: undeclared reference to 'v' (in container '') + | [1, 2, 3, 4].exists_one(i, v, i < v, v)&& x.map_string_string.transformList(i, i < v) && [1, 2, 3].transformList(i, v, i > 0 && x < 3, (i * v) + v) == [4] + | ..................................^ +ERROR: test_location:1:38: undeclared reference to 'v' (in container '') + | [1, 2, 3, 4].exists_one(i, v, i < v, v)&& x.map_string_string.transformList(i, i < v) && [1, 2, 3].transformList(i, v, i > 0 && x < 3, (i * v) + v) == [4] + | .....................................^ +ERROR: test_location:1:76: undeclared reference to 'transformList' (in container '') + | [1, 2, 3, 4].exists_one(i, v, i < v, v)&& x.map_string_string.transformList(i, i < v) && [1, 2, 3].transformList(i, v, i > 0 && x < 3, (i * v) + v) == [4] + | ...........................................................................^ +ERROR: test_location:1:77: undeclared reference to 'i' (in container '') + | [1, 2, 3, 4].exists_one(i, v, i < v, v)&& x.map_string_string.transformList(i, i < v) && [1, 2, 3].transformList(i, v, i > 0 && x < 3, (i * v) + v) == [4] + | ............................................................................^ +ERROR: test_location:1:80: undeclared reference to 'i' (in container '') + | [1, 2, 3, 4].exists_one(i, v, i < v, v)&& x.map_string_string.transformList(i, i < v) && [1, 2, 3].transformList(i, v, i > 0 && x < 3, (i * v) + v) == [4] + | ...............................................................................^ +ERROR: test_location:1:84: undeclared reference to 'v' (in container '') + | [1, 2, 3, 4].exists_one(i, v, i < v, v)&& x.map_string_string.transformList(i, i < v) && [1, 2, 3].transformList(i, v, i > 0 && x < 3, (i * v) + v) == [4] + | ...................................................................................^ +ERROR: test_location:1:131: found no matching overload for '_<_' applied to '(cel.expr.conformance.proto3.TestAllTypes, int)' (candidates: (bool, bool),(int, int),(uint, uint),(double, double),(string, string),(bytes, bytes),(google.protobuf.Timestamp, google.protobuf.Timestamp),(google.protobuf.Duration, google.protobuf.Duration),(int, uint),(uint, int),(int, double),(double, int),(uint, double),(double, uint)) + | [1, 2, 3, 4].exists_one(i, v, i < v, v)&& x.map_string_string.transformList(i, i < v) && [1, 2, 3].transformList(i, v, i > 0 && x < 3, (i * v) + v) == [4] + | ..................................................................................................................................^ \ No newline at end of file diff --git a/checker/src/test/resources/twoVarComprehensions_transformListMacro.baseline b/checker/src/test/resources/twoVarComprehensions_transformListMacro.baseline new file mode 100644 index 000000000..f821fa9c2 --- /dev/null +++ b/checker/src/test/resources/twoVarComprehensions_transformListMacro.baseline @@ -0,0 +1,143 @@ +Source: [1, 2, 3].transformList(i, v, i > 0 && v < 3, (i * v) + v) == [4] && [1, 2, 3].transformList(i, v, i % 2 == 0, (i * v) + v) == [1,9] && [1, 2, 3].transformList(i, v, (i * v) + v) == [1,4,9] +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +_&&_( + _&&_( + _==_( + __comprehension__( + // Variable + i, + v, + // Target + [ + 1~int, + 2~int, + 3~int + ]~list(int), + // Accumulator + @result, + // Init + []~list(int), + // LoopCondition + true~bool, + // LoopStep + _?_:_( + _&&_( + _>_( + i~int^i, + 0~int + )~bool^greater_int64, + _<_( + v~int^v, + 3~int + )~bool^less_int64 + )~bool^logical_and, + _+_( + @result~list(int)^@result, + [ + _+_( + _*_( + i~int^i, + v~int^v + )~int^multiply_int64, + v~int^v + )~int^add_int64 + ]~list(int) + )~list(int)^add_list, + @result~list(int)^@result + )~list(int)^conditional, + // Result + @result~list(int)^@result)~list(int), + [ + 4~int + ]~list(int) + )~bool^equals, + _==_( + __comprehension__( + // Variable + i, + v, + // Target + [ + 1~int, + 2~int, + 3~int + ]~list(int), + // Accumulator + @result, + // Init + []~list(int), + // LoopCondition + true~bool, + // LoopStep + _?_:_( + _==_( + _%_( + i~int^i, + 2~int + )~int^modulo_int64, + 0~int + )~bool^equals, + _+_( + @result~list(int)^@result, + [ + _+_( + _*_( + i~int^i, + v~int^v + )~int^multiply_int64, + v~int^v + )~int^add_int64 + ]~list(int) + )~list(int)^add_list, + @result~list(int)^@result + )~list(int)^conditional, + // Result + @result~list(int)^@result)~list(int), + [ + 1~int, + 9~int + ]~list(int) + )~bool^equals + )~bool^logical_and, + _==_( + __comprehension__( + // Variable + i, + v, + // Target + [ + 1~int, + 2~int, + 3~int + ]~list(int), + // Accumulator + @result, + // Init + []~list(int), + // LoopCondition + true~bool, + // LoopStep + _+_( + @result~list(int)^@result, + [ + _+_( + _*_( + i~int^i, + v~int^v + )~int^multiply_int64, + v~int^v + )~int^add_int64 + ]~list(int) + )~list(int)^add_list, + // Result + @result~list(int)^@result)~list(int), + [ + 1~int, + 4~int, + 9~int + ]~list(int) + )~bool^equals +)~bool^logical_and \ No newline at end of file diff --git a/checker/src/test/resources/types.baseline b/checker/src/test/resources/types.baseline index 61f3f4423..939e0ed97 100644 --- a/checker/src/test/resources/types.baseline +++ b/checker/src/test/resources/types.baseline @@ -27,14 +27,14 @@ __comprehension__( // Target {}~map(dyn, dyn), // Accumulator - __result__, + @result, // Init []~list(list(dyn)), // LoopCondition true~bool, // LoopStep _+_( - __result__~list(list(dyn))^__result__, + @result~list(list(dyn))^@result, [ [ c~dyn^c, @@ -45,4 +45,4 @@ __comprehension__( ]~list(list(dyn)) )~list(list(dyn))^add_list, // Result - __result__~list(list(dyn))^__result__)~list(list(dyn)) + @result~list(list(dyn))^@result)~list(list(dyn)) \ No newline at end of file diff --git a/checker/src/test/resources/userFunctionAddsOverload.baseline b/checker/src/test/resources/userFunctionAddsOverload.baseline index 775194ce7..515afa9eb 100644 --- a/checker/src/test/resources/userFunctionAddsOverload.baseline +++ b/checker/src/test/resources/userFunctionAddsOverload.baseline @@ -1,14 +1,14 @@ Source: size(x) > 4 declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare size { - function size_message (dev.cel.testing.testdata.proto3.TestAllTypes) -> int + function size_message (cel.expr.conformance.proto3.TestAllTypes) -> int } =====> _>_( size( - x~dev.cel.testing.testdata.proto3.TestAllTypes^x + x~cel.expr.conformance.proto3.TestAllTypes^x )~int^size_message, 4~int )~bool^greater_int64 diff --git a/checker/src/test/resources/userFunctionOverlappingOverloadsError.baseline b/checker/src/test/resources/userFunctionOverlappingOverloadsError.baseline new file mode 100644 index 000000000..436dda450 --- /dev/null +++ b/checker/src/test/resources/userFunctionOverlappingOverloadsError.baseline @@ -0,0 +1,9 @@ +Source: func(1) +declare func { + function overlapping_overload_1 int.() -> int + function overlapping_overload_2 int.() -> int +} +=====> +ERROR: test_location:1:1: overlapping overload for name 'func' (type '(int) -> int' cannot be distinguished from '(int) -> int') + | func(1) + | ^ diff --git a/checker/src/test/resources/userFunctionOverlaps.baseline b/checker/src/test/resources/userFunctionOverlaps.baseline index 0053f4919..059ba94b3 100644 --- a/checker/src/test/resources/userFunctionOverlaps.baseline +++ b/checker/src/test/resources/userFunctionOverlaps.baseline @@ -1,15 +1,14 @@ Source: size(x) == 1u -declare size { - function my_size (list(TEST)) -> uint -} declare x { value list(int) } +declare size { + function my_size (list(TEST)) -> uint +} =====> _==_( size( x~list(int)^x )~uint^my_size, 1u~uint -)~bool^equals - +)~bool^equals \ No newline at end of file diff --git a/checker/src/test/resources/wrapper.baseline b/checker/src/test/resources/wrapper.baseline index f5370bcd7..3b3b82375 100644 --- a/checker/src/test/resources/wrapper.baseline +++ b/checker/src/test/resources/wrapper.baseline @@ -1,11 +1,11 @@ Source: x.single_int64_wrapper + 1 != 23 declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> _!=_( _+_( - x~dev.cel.testing.testdata.proto3.TestAllTypes^x.single_int64_wrapper~wrapper(int), + x~cel.expr.conformance.proto3.TestAllTypes^x.single_int64_wrapper~wrapper(int), 1~int )~int^add_int64, 23~int diff --git a/codelab/BUILD.bazel b/codelab/BUILD.bazel index eea160dc4..95bb127b6 100644 --- a/codelab/BUILD.bazel +++ b/codelab/BUILD.bazel @@ -1,3 +1,5 @@ +load("@rules_java//java:defs.bzl", "java_library") + package( default_applicable_licenses = ["//:license"], default_visibility = ["//codelab:__subpackages__"], diff --git a/codelab/README.md b/codelab/README.md index 65e504215..83257d186 100644 --- a/codelab/README.md +++ b/codelab/README.md @@ -16,11 +16,16 @@ Some key areas covered are: * [Creating variables](#creating-variables) * [Commutatibe logical AND/OR](#logical-andor) * [Adding custom functions](#custom-functions) +* [Building JSON](#building-json) +* [Building Protos](#building-protos) +* [Macros](#macros) +* [Static AST Validators and Optimizers](#static-ast-validators-and-optimizers) +* [Custom AST Validation](#custom-ast-validation) ### Prerequisites This codelab builds upon a basic understanding of Protocol Buffers and Java. -If you're not familiar with Protocol Buffers, the first exercise will give you a sense of how CEL works, but because the more advanced examples use Protocol Buffers as the input into CEL, they may be harder to understand. Consider working through one of these tutorials, first. +If you're not familiar with Protocol Buffers, the first exercise will give you a sense of how CEL works, but because the more advanced examples use Protocol Buffers as the input into CEL, they may be harder to understand. Consider working through one of these [tutorials](https://developers.google.com/protocol-buffers/docs/tutorials?authuser=0), first. Note that Protocol Buffers are not required to use CEL, but they are used extensively in this codelab. @@ -45,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 ``` @@ -69,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. @@ -284,7 +289,6 @@ CelAbstractSyntaxTree compile(String expression, String variableName, CelType va CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder() .addVar(variableName, variableType) - .addMessageTypes(AttributeContext.Request.getDescriptor()) .setResultType(SimpleType.BOOL) .build(); try { @@ -298,7 +302,8 @@ CelAbstractSyntaxTree compile(String expression, String variableName, CelType va The compiler's `addVar` method allows us to declare variables. Note that you must supply the type of the variable being declared. Supported CEL types can be found [here](https://github.com/google/cel-java/tree/main/common/src/main/java/dev/cel/common/types). -Best practice: You may have noticed addVar has an overloaded method which accepts a proto based Type instead of the CEL-Java native CelType used in this example. While the two types are functionally equivalent, we recommend using the native types whenever possible. +> [!TIP] +> Best practice: You may have noticed `addVar` has an overloaded method which accepts a proto based Type instead of the CEL-Java native CelType used in this example. While the two types are functionally equivalent, we recommend using the native types whenever possible. Let's make the evaluation work now. Copy into the eval method: @@ -532,8 +537,9 @@ You should see the following error: To fix the error, the contains function will need to be added to the list of declarations which currently declares the request variable. Declaring a function is not much different than declaring a variable. A function must indicate its common name and enumerate a set of overloads with unique signatures. +The following snippet shows how to declare a parameterized type. This is the most complicated any function overload ever be for CEL: + ```java -The following snippet shows how to declare a parameterized type. This is the most complicated any function overload will ever be for CEL: /** * Compiles the input expression. * @@ -628,7 +634,1051 @@ private static boolean mapContainsKeyValue(Object[] args) { } ``` -Best practice: Use `Unary` or `Binary` helper interfaces to improve compile-time correctness for any overload implementations with 2 arguments or fewer. +> [!TIP] +> Best practice: Use `Unary` or `Binary` helper interfaces to improve compile-time correctness for any overload implementations with 2 arguments or fewer. + +> [!TIP] +> Best practice: Declare overload ids according to their types and function names. e.g. targetType_func_argType_argType. In the case where argType is a type param, use a descriptive name instead of the simple type name. + +## Building JSON + +CEL can also produce non-boolean outputs, such as JSON. + +Have a look at the test case in `Exercise5Test.java` first: + +```java +@Test +public void evaluate_jwtWithTimeVariable_producesJsonString() throws Exception { + // Note the quoted keys in the CEL map literal. For proto messages the field names are unquoted + // as they represent well-defined identifiers. + String jwt = + "{'sub': 'serviceAccount:delegate@acme.co'," + + "'aud': 'my-project'," + + "'iss': 'auth.acme.com:12350'," + + "'iat': time," + + "'nbf': time," + + "'exp': time + duration('300s')," + + "'extra_claims': {" + + "'group': 'admin'" + + "}}"; + CelAbstractSyntaxTree ast = exercise5.compile(jwt); + + // The output of the program is a map type. + @SuppressWarnings("unchecked") + Map evaluatedResult = + (Map) + exercise5.eval(ast, ImmutableMap.of("time", ProtoTimeUtils.fromSecondsToTimestamp(1698361778))); + String jsonOutput = exercise5.toJson(evaluatedResult); + + assertThat(jsonOutput) + .isEqualTo( + "{\"sub\":\"serviceAccount:delegate@acme.co\"," + + "\"aud\":\"my-project\"," + + "\"iss\":\"auth.acme.com:12350\"," + + "\"iat\":\"2023-10-26T23:09:38Z\"," + + "\"nbf\":\"2023-10-26T23:09:38Z\"," + + "\"exp\":\"2023-10-26T23:14:38Z\"," + + "\"extra_claims\":{\"group\":\"admin\"}}"); +} +``` + +Run the test: + +```sh +bazel test --test_output=errors //codelab/src/test/codelab:Exercise5Test +``` + +You should see the following error: + +``` +There was 1 failure: +1) evaluate_jwtWithTimeVariable_producesJsonString(codelab.Exercise5Test) +java.lang.IllegalArgumentException: Failed to compile expression. + at codelab.Exercise5.compile(Exercise5.java:49) + at codelab.Exercise5Test.evaluate_jwtWithTimeVariable_producesJsonString(Exercise5Test.java:46) + ... 26 trimmed +Caused by: dev.cel.common.CelValidationException: ERROR: :1:99: undeclared reference to 'time' (in container '') + | {'sub': 'serviceAccount:delegate@acme.co','aud': 'my-project','iss': 'auth.acme.com:12350','iat': time,'nbf': time,'exp': time + duration('300s'),'extra_claims': {'group': 'admin'}} + | ..................................................................................................^ + ... and more ... +``` + +In `Exercise5.java`, add a declaration for the `time` variable of type `SimpleType.TIMESTAMP`: + +```java +CelAbstractSyntaxTree compile(String expression) { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addVar("time", SimpleType.TIMESTAMP) + .build(); +} +``` + +> [!NOTE] +> Timestamps and durations are well-known types and have special convenience +types within CEL. The `google.protobuf.Timestamp` and `google.protobuf.Duration` +message types are equivalent to the convenience types; however, the fields of +all well-known types are not directly accessible, but are instead mediated by +member functions. + +The expression will successfully compile then evaluate, but you will run into a different error: + +``` +There was 1 failure: +1) evaluate_jwtWithTimeVariable_producesJsonString(codelab.Exercise5Test) +java.lang.UnsupportedOperationException: To be implemented + at codelab.Exercise5.toJson(Exercise5.java:73) + at codelab.Exercise5Test.evaluate_jwtWithTimeVariable_producesJsonString(Exercise5Test.java:52) +``` + +The evaluated result is a native Java map type, and this needs to be explicitly +converted to JSON by leveraging `google.protobuf.Struct`. The internal CEL +representation is JSON convertible as it only refers to types that JSON can +support or for which there is a well-known [Proto to JSON mapping](https://protobuf.dev/programming-guides/proto3/#json). + +Copy and paste the following into `toJson` method: + +```java +/** Converts the evaluated result into a JSON string using protobuf's google.protobuf.Struct. */ +String toJson(Map map) throws InvalidProtocolBufferException { + // Convert the map into google.protobuf.Struct using the CEL provided helper function + Struct jsonStruct = CelProtoJsonAdapter.adaptToJsonStructValue(map); + // Then use Protobuf's JsonFormat to produce a JSON string output. + return JsonFormat.printer().omittingInsignificantWhitespace().print(jsonStruct); +} +``` +Re-running the test will show that it successfully passes. + +## Building Protos + +CEL can also build protobuf messages for any message type compiled into the application. + +Have a look at the test case in `Exercise6Test.java` first: + +```java +@Test +public void evaluate_constructAttributeContext() { + // Given JSON web token and the current time as input variables, + // Setup an expression to construct an AttributeContext protobuf object. + // + // Note: the field names within the proto message types are not quoted as they + // are well-defined names composed of valid identifier characters. Also, note + // that when building nested proto objects, the message name needs to prefix + // the object construction. + String expression = + "Request{\n" + + "auth: Auth{" + + " principal: jwt.iss + '/' + jwt.sub," + + " audiences: [jwt.aud]," + + " presenter: 'azp' in jwt ? jwt.azp : ''," + + " claims: jwt" + + "}," + + "time: now" + + "}"; + // Values for `now` and `jwt` variables to be passed into the runtime + Timestamp now = ProtoTimeUtils.now(); + ImmutableMap jwt = + ImmutableMap.of( + "sub", "serviceAccount:delegate@acme.co", + "aud", "my-project", + "iss", "auth.acme.com:12350", + "extra_claims", ImmutableMap.of("group", "admin")); + AttributeContext.Request expectedMessage = + AttributeContext.Request.newBuilder() + .setTime(now) + .setAuth( + AttributeContext.Auth.newBuilder() + .setPrincipal("auth.acme.com:12350/serviceAccount:delegate@acme.co") + .addAudiences("my-project") + .setClaims( + Struct.newBuilder() + .putAllFields( + ImmutableMap.of( + "sub", newStringValue("serviceAccount:delegate@acme.co"), + "aud", newStringValue("my-project"), + "iss", newStringValue("auth.acme.com:12350"))) + .putFields( + "extra_claims", + Value.newBuilder() + .setStructValue( + Struct.newBuilder() + .putFields("group", newStringValue("admin")) + .build()) + .build()))) + .build(); + + // Compile the `Request` message construction expression and validate that + // the resulting expression type matches the fully qualified message name. + CelAbstractSyntaxTree ast = exercise6.compile(expression); + AttributeContext.Request evaluatedResult = + (AttributeContext.Request) + exercise6.eval( + ast, + ImmutableMap.of( + "now", now, + "jwt", jwt)); + + assertThat(evaluatedResult).isEqualTo(expectedMessage); +} +``` + +Run the test: + +```sh +bazel test --test_output=errors //codelab/src/test/codelab:Exercise6Test +``` + +You should see the following error: + +``` +There was 1 failure: +1) evaluate_constructAttributeContext(codelab.Exercise6Test) +java.lang.IllegalArgumentException: Failed to compile expression. + at codelab.Exercise6.compile(Exercise6.java:42) + at codelab.Exercise6Test.evaluate_constructAttributeContext(Exercise6Test.java:72) + ... 26 trimmed +Caused by: dev.cel.common.CelValidationException: ERROR: :1:8: undeclared reference to 'Request' (in container '') + | Request{ + | .......^ +... and many more ... +``` + +The container is basically the equivalent of a namespace or package, but can +even be as granular as a protobuf message name. CEL containers use the same +namespace resolution rules as [Protobuf and C++][27] for determining where a +given variable, function, or type name is declared. + +Given the container `google.rpc.context.AttributeContext` the type-checker and +the evaluator will try the following identifier names for all variables, types, +and functions: + +* `google.rpc.context.AttributeContext.` +* `google.rpc.context.` +* `google.rpc.` +* `google.` +* `` + +For absolute names, prefix the variable, type, or function reference with a +leading dot `.`. In the example, the expression `.` will only search for the +top-level `` identifier without first checking within the container. + +Try specifying the `.setContainer("google.rpc.context.AttributeContext")` option +to the compiler environment then run the test again: + +```java +CelAbstractSyntaxTree compile(String expression) { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .setContainer("google.rpc.context.AttributeContext") + // Declare variables for "jwt" and "now" here + .addMessageTypes(Request.getDescriptor()) + .setResultType(StructTypeReference.create(Request.getDescriptor().getFullName())) + .build(); + ... +} +``` + +``` +There was 1 failure: +1) evaluate_constructAttributeContext(codelab.Exercise6Test) +... +Caused by: dev.cel.common.CelValidationException: ERROR: :2:25: undeclared reference to 'jwt' (in container 'google.rpc.context.AttributeContext') + | auth: Auth{ principal: jwt.iss + '/' + jwt.sub, audiences: [jwt.aud], presenter: 'azp' in jwt ? jwt.azp : '', claims: jwt},time: now} + | ........................^ +... and many more ... +``` + +We're making progress. Declare the `jwt` and `now` variables: + +```java +CelAbstractSyntaxTree compile(String expression) { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .setContainer("google.rpc.context.AttributeContext") + .addVar("jwt", SimpleType.DYN) + .addVar("now", SimpleType.TIMESTAMP) + .addMessageTypes(Request.getDescriptor()) + .setResultType(StructTypeReference.create(Request.getDescriptor().getFullName())) + .build(); + ... +} +``` + +The test should pass now. + +> [!NOTE] +> Additional considerations for using CEL to build protos: +> +> 1. There is no native support for the conditional assignment for `oneof` +> fields. +> 2. There are issues round-tripping to / from a `google.protobuf.Any`. +> +> When a `oneof` needs to be set, test whether the desired value is present before +> constructing the message, or extend CEL to include a custom object building +> function such as a [`wither`](https://crates.io/crates/withers_derive#the-wither-pattern) +> method, or perhaps something more abstract. +> +> When an `Any` contains a wrapper type such as `google.protobuf.IntValue`, CEL +> automatically unpacks the `Any` to `int` or `null_type` depending on the +> contents. In the case where the wrapper type is unset, the `null` could not be +> assigned to an `Any` field and have its same original meaning. So far, this is +> the only roundtripping issue we have discovered, but it's worth noting. + +## Macros + +Macros can be used to manipulate the CEL program at parse time. Macros match a +call signature and manipulate the input call and its arguments in order to +produce a new subexpression AST. + +Macros can be used to implement complex logic in the AST that can't be written +directly in CEL. For example, the `has` macro enables field presence testing. +The comprehension macros such as `exists` and `all` replace a function call with +bounded iteration over an input list or map. Neither concept is possible at a +syntactic level, but they are possible through macro expansions. + +Have a look at the test case in `Exercise7Test.java` first: + +```java +@Test +public void evaluate_checkJwtClaimsWithMacro_evaluatesToTrue() { + String expression = + "jwt.extra_claims.exists(c, c.startsWith('group'))" + + " && jwt.extra_claims" + + ".filter(c, c.startsWith('group'))" + + ".all(c, jwt.extra_claims[c]" + + ".all(g, g.endsWith('@acme.co')))"; + ImmutableMap jwt = + ImmutableMap.of( + "sub", + "serviceAccount:delegate@acme.co", + "aud", + "my-project", + "iss", + "auth.acme.com:12350", + "extra_claims", + ImmutableMap.of("group1", ImmutableList.of("admin@acme.co", "analyst@acme.co")), + "labels", + ImmutableList.of("metadata", "prod", "pii"), + "groupN", + ImmutableList.of("forever@acme.co")); + CelAbstractSyntaxTree ast = exercise7.compile(expression); + + // Evaluate a complex-ish JWT with two groups that satisfy the criteria. + // Output: true. + boolean evaluatedResult = (boolean) exercise7.eval(ast, ImmutableMap.of("jwt", jwt)); + + assertThat(evaluatedResult).isTrue(); +} +``` + +Run the test: + +```sh +bazel test --test_output=errors //codelab/src/test/codelab:Exercise7Test +``` + +You should see the following error: + +``` +There was 1 failure: +1) evaluate_checkJwtClaimsWithMacro_evaluatesToTrue(codelab.Exercise7Test) +java.lang.IllegalArgumentException: Failed to compile expression. + at codelab.Exercise7.compile(Exercise7.java:40) + at codelab.Exercise7Test.evaluate_checkJwtClaimsWithMacro_evaluatesToTrue(Exercise7Test.java:52) + ... 26 trimmed +Caused by: dev.cel.common.CelValidationException: ERROR: :1:24: undeclared reference to 'exists' (in container '') + | jwt.extra_claims.exists(c, c.startsWith('group')) && jwt.extra_claims.filter(c, c.startsWith('group')).all(c, jwt.extra_claims[c].all(g, g.endsWith('@acme.co'))) + | .......................^ +... and many more ... +``` + +Specify the option `setStandardMacros` with `CelStandardMacro.ALL`, +`CelStandardMacro.FILTER`, and `CelStandardMacro.EXISTS` as arguments: + +```java +CelAbstractSyntaxTree compile(String expression) { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addVar("jwt", SimpleType.DYN) + .setStandardMacros( + CelStandardMacro.ALL, CelStandardMacro.FILTER, CelStandardMacro.EXISTS) + .setResultType(SimpleType.BOOL) + .build(); + ... +} +``` + +Run the test again to confirm that it passes. + +These are the currently supported macros: + +| Macro | Signature | Description | +| ------------ | ------------------------- | --------------------------------- | +| `all` | `r.all(var, cond)` | Test if `cond` evaluates `true` for *all* `var` in range `r`. +| `exists` | `r.exists(var, cond)` | Test if `cond` evaluates `true` for *any* `var` in range `r`. +| `exists_one` | `r.exists_one(var, cond)` | Test if `cond` evaluates `true` for *only one* `var` in range `r`. +| `filter` | `r.filter(var, cond)` | For lists, create a new list where each element `var` in range `r` satisfies the condition `cond`. For maps, create a new list where each key `var` in range `r` satisfies the condition `cond`. +| `map` | `r.map(var, expr)` | Create a new list where each each `var` in range `r` is transformed by `expr`. +| | `r.map(var, cond, expr)` | Same as two-arg `map` but with a conditional `cond` filter before the value is transformed. +| `has` | `has(a.b)` | Presence test for `b` on value `a` \: For maps, json tests definition. For protos, tests non-default primitive value or a or a set message field. + +When the range `r` argument is a `map` type, the `var` will be the map key, and +for `list` type values the `var` will be the list element value. The `all`, +`exists`, `exists_one`, `filter`, and `map` macros perform an AST rewrite that +does for-each iteration which is bounded by the size of the input. + +The bounded comprehensions ensure that CEL programs won't be Turing-complete, +but they evaluate in super-linear time with respect to the input. Use these +macros sparingly or not at all. Heavy use of comprehensions usually a good +indicator that a custom function would provide a better user experience and +better performance. + +> [!TIP] +> Best practice: `CelStandardMacro.STANDARD_MACROS` enables all listed macros, but +it's safer to explicitly enable only the required ones for your use case. + +## Static AST Validators and Optimizers + +CEL can perform complex validations on a compiled AST beyond what the +type-checker is capable of. CEL can also enhance evaluation efficiency through +AST optimizations such as constant folding and common subexpression elimination. +We will explore the use of canonical CEL validators and optimizers available. + +> [!NOTE] +> Note: Both validation and optimization require a type-checked AST. + +> [!CAUTION] +> AST validation and optimization should not be done in latency critical +code paths, similar to parsing and type-checking. + +### Validators + +Inspect the first three test cases in `Exercise8Test.java`: + +```java +@Test +public void validate_invalidTimestampLiteral_returnsError() throws Exception { + CelAbstractSyntaxTree ast = exercise8.compile("timestamp('bad')"); + + CelValidationResult validationResult = exercise8.validate(ast); + + assertThat(validationResult.hasError()).isTrue(); + assertThat(validationResult.getErrorString()) + .isEqualTo( + "ERROR: :1:11: timestamp validation failed. Reason: evaluation error: Text 'bad'" + + " could not be parsed at index 0\n" + + " | timestamp('bad')\n" + + " | ..........^"); +} + +@Test +public void validate_invalidDurationLiteral_returnsError() throws Exception { + CelAbstractSyntaxTree ast = exercise8.compile("duration('bad')"); + + CelValidationResult validationResult = exercise8.validate(ast); + + assertThat(validationResult.hasError()).isTrue(); + assertThat(validationResult.getErrorString()) + .isEqualTo( + "ERROR: :1:10: duration validation failed. Reason: evaluation error: invalid" + + " duration format\n" + + " | duration('bad')\n" + + " | .........^"); +} + +@Test +public void validate_invalidRegexLiteral_returnsError() throws Exception { + CelAbstractSyntaxTree ast = exercise8.compile("'text'.matches('**')"); + + CelValidationResult validationResult = exercise8.validate(ast); + + assertThat(validationResult.hasError()).isTrue(); + assertThat(validationResult.getErrorString()) + .isEqualTo( + "ERROR: :1:16: Regex validation failed. Reason: Dangling meta character '*' near" + + " index 0\n" + + "**\n" + + "^\n" + + " | 'text'.matches('**')\n" + + " | ...............^"); +} +``` + +Note that all three test cases contain an expression with invalid literals +that would fail if evaluated. + +Run the validator tests (note the `--test_filter` flag): + +```sh +bazel test --test_output=errors --test_filter=validate //codelab/src/test/codelab:Exercise8Test +``` + + +You should see 4 tests failing: + +``` +There were 4 failures: +1) validate_invalidTimestampLiteral_returnsError(codelab.Exercise8Test) +... and more +FAILURES!!! +Tests run: 4, Failures: 4 +``` + +Setting up a validator requires an instance of a compiler and a runtime. These +have been provided for you out of convenience in `Exercise8.java`: + +```java +private static final CelCompiler CEL_COMPILER = + CelCompilerFactory.standardCelCompilerBuilder() + .addVar("x", SimpleType.INT) + .addVar( + "request", StructTypeReference.create("google.rpc.context.AttributeContext.Request")) + .addMessageTypes(AttributeContext.Request.getDescriptor()) + .build(); +private static final CelRuntime CEL_RUNTIME = + CelRuntimeFactory.standardCelRuntimeBuilder() + .addMessageTypes(AttributeContext.Request.getDescriptor()) + .build(); +``` + +Copy and paste the following, below where the runtime is declared: + +```java +// Just like the compiler and runtime, the validator and optimizer can be statically +// initialized as their instances are immutable. +private static final CelValidator CEL_VALIDATOR = + CelValidatorFactory.standardCelValidatorBuilder(CEL_COMPILER, CEL_RUNTIME) + .build(); +``` + +Next, replace the implementation of `validate` method with the following code: + +```java +/** Validates a type-checked AST. */ +CelValidationResult validate(CelAbstractSyntaxTree checkedAst) { + return CEL_VALIDATOR.validate(checkedAst); +} +``` + +Re-run the tests. The tests no longer throws an exception, but they still fail +because we aren't actually validating anything at the moment: -Best practice: Declare overload ids according to their types and function names. e.g. targetType_func_argType_argType. In the case where argType is a type param, use a descriptive name instead of the simple type name. +``` +1) validate_invalidTimestampLiteral_returnsError(codelab.Exercise8Test) +value of: hasError() +expected to be true + at codelab.Exercise8Test.validate_invalidTimestampLiteral_returnsError(Exercise8Test.java:28) +... and more +``` + +We now need to register the individual AST validators. Add the literal +validators for timestamp, duration and regular expressions through +`.addAstValidators` builder method: + +```java +private static final CelValidator CEL_VALIDATOR = + CelValidatorFactory.standardCelValidatorBuilder(CEL_COMPILER, CEL_RUNTIME) + .addAstValidators( + TimestampLiteralValidator.INSTANCE, + DurationLiteralValidator.INSTANCE, + RegexLiteralValidator.INSTANCE) + .build(); +``` + +Re-run the test. You should observe that the first three literal validation +tests pass. + +There is one more test remaining to be fixed: + +```java +@Test +public void validate_listHasMixedLiterals_throws() throws Exception { + CelAbstractSyntaxTree ast = exercise8.compile("3 in [1, 2, '3']"); + + // Note that `CelValidationResult` is the same result class used for the compilation path. This + // means you could alternatively invoke `.getAst()` and handle `CelValidationException` as + // usual. + CelValidationResult validationResult = exercise8.validate(ast); + + CelValidationException e = assertThrows(CelValidationException.class, validationResult::getAst); + assertThat(e) + .hasMessageThat() + .contains( + "ERROR: :1:13: expected type 'int' but found 'string'\n" + + " | 3 in [1, 2, '3']\n" + + " | ............^"); +} +``` + +CEL offers a validator to catch literals with mixed types in lists or maps. +For example, `3 in [1, 2, "3"]` is a perfectly valid expression in CEL but +likely unintended as this would evaluate to false. + +Add `HomogeneousLiteralValidator.newInstance()` then rerun the tests to confirm +that all tests pass: + +```java +// Just like the compiler and runtime, the validator and optimizer can be statically +// initialized as their instances are immutable. +private static final CelValidator CEL_VALIDATOR = + CelValidatorFactory.standardCelValidatorBuilder(CEL_COMPILER, CEL_RUNTIME) + .addAstValidators( + TimestampLiteralValidator.INSTANCE, + DurationLiteralValidator.INSTANCE, + RegexLiteralValidator.INSTANCE, + HomogeneousLiteralValidator.newInstance()) + .build(); +``` + +### Optimizers + +Human authored expressions often contain redundancies that may cause suboptimal +evaluation. In such cases, optimization is highly beneficial if the ASTs will be +repeatedly evaluated. Conversely, there is little point in optimizing an AST if +it will be evaluated once. + +We first look at a classic compiler optimization known as constant folding. Have +a look at the relevant test: + +```java +@Test +public void optimize_constantFold_success() throws Exception { + CelUnparser celUnparser = CelUnparserFactory.newUnparser(); + CelAbstractSyntaxTree ast = exercise8.compile("(1 + 2 + 3 == x) && (x in [1, 2, x])"); + + CelAbstractSyntaxTree optimizedAst = exercise8.optimize(ast); + + assertThat(celUnparser.unparse(optimizedAst)).isEqualTo("6 == x"); +} +``` + +> [!NOTE] +> Note: Unparser can be used to convert an AST into its textual representation. +In this exercise, it's used to verify the result of an AST optimization. + +Constant folding will take all arithmetic expression containing only constant +values, computes the expression then replaces with the result. It will also +prune any branches of the expression that can be removed without affecting the +correctness (akin to dead-code elimination). + +Run the optimizer tests (note the `--test_filter` flag): + +```sh +bazel test --test_output=errors --test_filter=optimize //codelab/src/test/codelab:Exercise8Test +``` + +You should see 3 tests failing: + +``` +There were 3 failures: +1) optimize_commonSubexpressionElimination_success(codelab.Exercise8Test) +... and more +FAILURES!!! +Tests run: 3, Failures: 3 +``` + +Similar to how the validator was setup, the optimizer requires both the compiler +and runtime instances. Copy and paste the following into `Exercise8.java`: + +```java +private static final CelOptimizer CEL_OPTIMIZER = + CelOptimizerFactory.standardCelOptimizerBuilder(CEL_COMPILER, CEL_RUNTIME) + .build(); +``` + +Then change the code in `optimize` method as: + +```java +/** + * Optimizes a type-checked AST. + * + * @throws CelOptimizationException If the optimization fails. + */ +CelAbstractSyntaxTree optimize(CelAbstractSyntaxTree checkedAst) throws CelOptimizationException { + return CEL_OPTIMIZER.optimize(checkedAst); +} +``` + +Next, register `ConstantFoldingOptimizer` via `.addAstOptimizers`: + +```java +private static final CelOptimizer CEL_OPTIMIZER = + CelOptimizerFactory.standardCelOptimizerBuilder(CEL_COMPILER, CEL_RUNTIME) + .addAstOptimizers(ConstantFoldingOptimizer.getInstance()) + .build(); +``` + +Re-run the test. You should see 2 out of 3 tests passing now. + +Please note that optimization removes parsing metadata from the AST, as +modifications may cause it to deviate from the original expression. +Practically, this means the error message will no longer indicate the source +location as shown in the test below: + +```java +@Test +public void optimize_constantFold_evaluateError() throws Exception { + CelAbstractSyntaxTree ast = + exercise8.compile("request.headers.referer == 'https://' + 'cel.dev'"); + CelAbstractSyntaxTree optimizedAst = exercise8.optimize(ast); + ImmutableMap runtimeParameters = + ImmutableMap.of("request", AttributeContext.Request.getDefaultInstance()); + + CelEvaluationException e1 = + assertThrows(CelEvaluationException.class, () -> exercise8.eval(ast, runtimeParameters)); + CelEvaluationException e2 = + assertThrows( + CelEvaluationException.class, () -> exercise8.eval(optimizedAst, runtimeParameters)); + // Note that the errors below differ by their source position. + assertThat(e1) + .hasMessageThat() + .contains("evaluation error at :15: key 'referer' is not present in map."); + assertThat(e2) + .hasMessageThat() + .contains("evaluation error at :0: key 'referer' is not present in map."); +} +``` + +The second optimization technique to cover is Common Subexpression Elimination +(CSE). It will replace instances of identical expressions to a single variable +holding the computed value. + +Have a look at the following test and note the expression containing duplicate field +selections `request.auth.claims.group`: + +```java +@Test +public void optimize_commonSubexpressionElimination_success() throws Exception { + CelUnparser celUnparser = CelUnparserFactory.newUnparser(); + CelAbstractSyntaxTree ast = + exercise8.compile( + "request.auth.claims.group == 'admin' || request.auth.claims.group == 'user'"); + + CelAbstractSyntaxTree optimizedAst = exercise8.optimize(ast); + + assertThat(celUnparser.unparse(optimizedAst)) + .isEqualTo( + "cel.@block([request.auth.claims.group], @index0 == \"admin\" || @index0 == \"user\")"); +} +``` + +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`). + +Make the following changes in `Exercise8.java`: + +```java +private static final CelOptimizer CEL_OPTIMIZER = + CelOptimizerFactory.standardCelOptimizerBuilder(CEL_COMPILER, CEL_RUNTIME) + .addAstOptimizers( + ConstantFoldingOptimizer.getInstance(), + SubexpressionOptimizer.newInstance( + SubexpressionOptimizerOptions.newBuilder().enableCelBlock(true).build())) + .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 + +As seen in the earlier exercise, CEL offers many built-in validators. There +are however situations where authoring a custom AST validator is beneficial to +improve user experience by providing context-specific feedback. + +We'll be writing a validator to ensure that `AttributeContext.Request` message +is well formatted. Have a look at the test case: + +```java +@Test +public void validate_invalidHttpMethod_returnsError() throws Exception { + String expression = + "google.rpc.context.AttributeContext.Request { \n" + + "scheme: 'http', " + + "method: 'GETTT', " // method is misspelled. + + "host: 'cel.dev' \n" + + "}"; + CelAbstractSyntaxTree ast = exercise9.compile(expression); + + CelValidationResult validationResult = exercise9.validate(ast); + + assertThat(validationResult.hasError()).isTrue(); + assertThat(validationResult.getErrorString()) + .isEqualTo( + "ERROR: :2:25: GETTT is not an allowed HTTP method.\n" + + " | scheme: 'http', method: 'GETTT', host: 'cel.dev' \n" + + " | ........................^"); + assertThrows(CelValidationException.class, validationResult::getAst); +} +``` + +Run the test: + +```sh +bazel test --test_output=errors //codelab/src/test/codelab:Exercise9Test +``` + +You should see all three tests failing: + +``` +There were 3 failures: +1) validate_invalidHttpMethod_returnsError(codelab.Exercise9Test) +... and more +FAILURES!!! +Tests run: 3, Failures: 3 +``` + +The first step in writing a custom AST validator is to have a class implement +the `CelAstValidator` interface. In `Exercise9.java`, make the +following changes: + +```java +static final class AttributeContextRequestValidator implements CelAstValidator { + @Override + public void validate(CelNavigableAst navigableAst, Cel cel, IssuesFactory issuesFactory) { + // Implement validate method here + } +} +``` + +We need a way to fetch then inspect the expression nodes of interest. For this, +CEL provides fluent APIs to navigate a compiled AST via navigable expressions. +A `CelNavigableExpr` allows you to traverse through its descendants or parent +with ease. + +Let's write some logic to filter for expressions containing +`google.rpc.context.AttributeContext.Request` message name. Copy and paste the +following: + +```java +@Override +public void validate(CelNavigableAst navigableAst, Cel cel, IssuesFactory issuesFactory) { + navigableAst + .getRoot() + .allNodes() + .filter(node -> node.getKind().equals(Kind.STRUCT)) + .map(node -> node.expr().struct()) + .filter( + struct -> struct.messageName().equals("google.rpc.context.AttributeContext.Request")) +} +``` + +> [!TIP] +> Call `.toString()` on a `CelExpr` object to obtain a human-readable format +of the AST. + +Next, we'll iterate through the fields of the message to confirm that it has +the correct HTTP method. Otherwise, we'll add it as an error through +`IssuesFactory`. Copy and paste the rest of the code: + +```java +static final class AttributeContextRequestValidator implements CelAstValidator { + private static final ImmutableSet ALLOWED_HTTP_METHODS = + ImmutableSet.of("GET", "POST", "PUT", "DELETE"); + + @Override + public void validate(CelNavigableAst navigableAst, Cel cel, IssuesFactory issuesFactory) { + navigableAst + .getRoot() + .allNodes() + .filter(node -> node.getKind().equals(Kind.STRUCT)) + .map(node -> node.expr().struct()) + .filter( + struct -> struct.messageName().equals("google.rpc.context.AttributeContext.Request")) + .forEach( + struct -> { + for (CelStruct.Entry entry : struct.entries()) { + String fieldKey = entry.fieldKey(); + if (fieldKey.equals("method")) { + String entryStringValue = getStringValue(entry.value()); + if (!ALLOWED_HTTP_METHODS.contains(entryStringValue)) { + issuesFactory.addError( + entry.value().id(), entryStringValue + " is not an allowed HTTP method."); + } + } + } + }); + } + + /** + * Reads the underlying string value from the expression. + * + * @throws UnsupportedOperationException if the expression is not a constant string value. + */ + private static String getStringValue(CelExpr celExpr) { + return celExpr.constant().stringValue(); + } +} +``` + +Register the custom validator via `.addAstValidators`: + +```java +private static final CelValidator CEL_VALIDATOR = + CelValidatorFactory.standardCelValidatorBuilder(CEL_COMPILER, CEL_RUNTIME) + .addAstValidators(new AttributeContextRequestValidator()) + .build(); +``` + +Run the test again and confirm that the first test case passes. + +Let's look at the next test: + +```java +@Test +public void validate_schemeIsHttp_returnsWarning() throws Exception { + String expression = + "google.rpc.context.AttributeContext.Request { \n" + + "scheme: 'http', " // https is preferred but not required. + + "method: 'GET', " + + "host: 'cel.dev' \n" + + "}"; + CelAbstractSyntaxTree ast = exercise9.compile(expression); + + CelValidationResult validationResult = exercise9.validate(ast); + + assertThat(validationResult.hasError()).isFalse(); + assertThat(validationResult.getIssueString()) + .isEqualTo( + "WARNING: :2:9: Prefer using https for safety.\n" + + " | scheme: 'http', method: 'GET', host: 'cel.dev' \n" + + " | ........^"); + // Because the validation result does not contain any errors, you can still evaluate it. + assertThat(exercise9.eval(validationResult.getAst())) + .isEqualTo( + AttributeContext.Request.newBuilder() + .setScheme("http") + .setMethod("GET") + .setHost("cel.dev") + .build()); +} +``` + +A validator does not necessarily have to produce a pass/fail outcome. It can +instead provide informational feedbacks or warnings. This is useful when an +expression has no correctness issue, but can be improved based on the +application context. Linting is a good example of this. + +Make the following changes to produce a warning when `https` is not used as the +scheme: + +```java +@Override +public void validate(CelNavigableAst navigableAst, Cel cel, IssuesFactory issuesFactory) { + navigableAst + .getRoot() + .allNodes() + .filter(node -> node.getKind().equals(Kind.STRUCT)) + .map(node -> node.expr().struct()) + .filter( + struct -> struct.messageName().equals("google.rpc.context.AttributeContext.Request")) + .forEach( + struct -> { + for (CelStruct.Entry entry : struct.entries()) { + String fieldKey = entry.fieldKey(); + if (fieldKey.equals("method")) { + String entryStringValue = getStringValue(entry.value()); + if (!ALLOWED_HTTP_METHODS.contains(entryStringValue)) { + issuesFactory.addError( + entry.value().id(), entryStringValue + " is not an allowed HTTP method."); + } + } else if (fieldKey.equals("scheme")) { + String entryStringValue = getStringValue(entry.value()); + if (!entryStringValue.equals("https")) { + issuesFactory.addWarning( + entry.value().id(), "Prefer using https for safety."); + } + } + } + }); +} +``` + +Re-run to confirm that the first two tests pass. + +Another common need is to restrict the use of an expensive function call in an +unsafe manner. Suppose we have a function that issues an RPC. We'll write a +validator to ensure that it can't be used within a macro to prevent repeated +invocations within an expression. + +Inspect the test case: + +```java +@Test +public void validate_isPrimeNumberWithinMacro_returnsError() throws Exception { + String expression = "[2,3,5].all(x, is_prime_number(x))"; + CelAbstractSyntaxTree ast = exercise9.compile(expression); + + CelValidationResult validationResult = exercise9.validate(ast); + + assertThat(validationResult.hasError()).isTrue(); + assertThat(validationResult.getErrorString()) + .isEqualTo( + "ERROR: :1:12: is_prime_number function cannot be used within CEL macros.\n" + + " | [2,3,5].all(x, is_prime_number(x))\n" + + " | ...........^"); +} +``` + +Then copy and paste the following into `ComprehensionSafetyValidator` in `Exercise9.java`: + +```java +/** Prevents nesting an expensive function call within a macro. */ +static final class ComprehensionSafetyValidator implements CelAstValidator { + private static final String EXPENSIVE_FUNCTION_NAME = "is_prime_number"; + + @Override + public void validate(CelNavigableAst navigableAst, Cel cel, IssuesFactory issuesFactory) { + navigableAst + .getRoot() + .allNodes() + .filter(node -> node.getKind().equals(Kind.COMPREHENSION)) + .forEach( + comprehensionNode -> { + boolean isFunctionWithinMacro = + comprehensionNode + .descendants() + .anyMatch( + node -> + node.expr() + .callOrDefault() + .function() + .equals(EXPENSIVE_FUNCTION_NAME)); + if (isFunctionWithinMacro) { + issuesFactory.addError( + comprehensionNode.id(), + EXPENSIVE_FUNCTION_NAME + " function cannot be used within CEL macros."); + } + }); + } +} +``` + +> [!NOTE] +> This doesn't stop the expression author from simply chaining the calls together outside the macro (e.g: `is_prime_number(2) && is_prime_number(3) ...)`. One could easily introduce a validator to catch these cases too if desired. + +> [!TIP] +> Use `(kind)OrDefault` to consolidate checking for the expression kind and the retrieval of the underlying expression for brevity. `callOrDefault` here is an example. + +Finally, register the custom validator: + +```java +private static final CelValidator CEL_VALIDATOR = + CelValidatorFactory.standardCelValidatorBuilder(CEL_COMPILER, CEL_RUNTIME) + .addAstValidators( + new AttributeContextRequestValidator(), + new ComprehensionSafetyValidator()) + .build(); +``` +Re-run the test to confirm that all tests pass. diff --git a/codelab/src/main/codelab/BUILD.bazel b/codelab/src/main/codelab/BUILD.bazel index b5c135639..af900769c 100644 --- a/codelab/src/main/codelab/BUILD.bazel +++ b/codelab/src/main/codelab/BUILD.bazel @@ -1,3 +1,5 @@ +load("@rules_java//java:defs.bzl", "java_library") + package( default_applicable_licenses = [ "//:license", @@ -9,15 +11,34 @@ java_library( name = "codelab", srcs = glob(["*.java"]), deps = [ - "@maven//:com_google_api_grpc_proto_google_common_protos", # unuseddeps: keep - "@maven//:com_google_guava_guava", # unuseddeps: keep - "//common", # unuseddeps: keep + "//bundle:cel", # unuseddeps: keep + "//common:cel_ast", "//common:compiler_common", # unuseddeps: keep + "//common:proto_json_adapter", # unuseddeps: keep + "//common/ast", # unuseddeps: keep + "//common/navigation", # unuseddeps: keep "//common/types", # unuseddeps: keep "//common/types:type_providers", # unuseddeps: keep "//compiler", # unuseddeps: keep "//compiler:compiler_builder", # unuseddeps: keep + "//optimizer", # unuseddeps: keep + "//optimizer:optimization_exception", # unuseddeps: keep + "//optimizer:optimizer_builder", # unuseddeps: keep + "//optimizer/optimizers:common_subexpression_elimination", # unuseddeps: keep + "//optimizer/optimizers:constant_folding", # unuseddeps: keep + "//parser:macro", # unuseddeps: keep "//runtime", # unuseddeps: keep - # + "//validator", # unuseddeps: keep + "//validator:ast_validator", # unuseddeps: keep + "//validator:validator_builder", # unuseddeps: keep + "//validator/validators:duration", # unuseddeps: keep + "//validator/validators:homogeneous_literal", # unuseddeps: keep + "//validator/validators:regex", # unuseddeps: keep + "//validator/validators:timestamp", # unuseddeps: keep + "@com_google_googleapis//google/rpc/context:attribute_context_java_proto", # unuseddeps: keep + "@maven//:com_google_guava_guava", # unuseddeps: keep + "@maven//:com_google_protobuf_protobuf_java", # unuseddeps: keep + "@maven//:com_google_protobuf_protobuf_java_util", # unuseddeps: keep + "@maven_android//:com_google_protobuf_protobuf_javalite", # unuseddeps: keep ], ) diff --git a/codelab/src/main/codelab/Exercise5.java b/codelab/src/main/codelab/Exercise5.java new file mode 100644 index 000000000..eca2a5df7 --- /dev/null +++ b/codelab/src/main/codelab/Exercise5.java @@ -0,0 +1,73 @@ +// 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. + +package codelab; + +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelValidationException; +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.Map; + +/** + * Exercise5 covers how to build complex objects as CEL literals. + * + *

Given the input variable "time", construct a JWT with an expiry of 5 minutes + */ +final class Exercise5 { + + /** + * Compiles the input expression. + * + * @throws IllegalArgumentException If the expression is malformed due to syntactic or semantic + * errors. + */ + CelAbstractSyntaxTree compile(String expression) { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + // Declare the 'time' variable here as a Timestamp. + .build(); + + try { + return celCompiler.compile(expression).getAst(); + } catch (CelValidationException e) { + throw new IllegalArgumentException("Failed to compile expression.", e); + } + } + + /** + * Evaluates the compiled AST with the user provided parameter values. + * + * @throws IllegalArgumentException If the compiled expression in AST fails to evaluate. + */ + Object eval(CelAbstractSyntaxTree ast, Map parameterValues) { + CelRuntime celRuntime = CelRuntimeFactory.standardCelRuntimeBuilder().build(); + + try { + CelRuntime.Program program = celRuntime.createProgram(ast); + return program.eval(parameterValues); + } catch (CelEvaluationException e) { + throw new IllegalArgumentException("Evaluation error has occurred.", e); + } + } + + /** Converts the evaluated result into a JSON string using protobuf's google.protobuf.Struct. */ + @SuppressWarnings("DoNotCallSuggester") + String toJson(Map map) { + throw new UnsupportedOperationException("To be implemented"); + } +} diff --git a/codelab/src/main/codelab/Exercise6.java b/codelab/src/main/codelab/Exercise6.java new file mode 100644 index 000000000..9991fb566 --- /dev/null +++ b/codelab/src/main/codelab/Exercise6.java @@ -0,0 +1,73 @@ +// 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. + +package codelab; + +import com.google.rpc.context.AttributeContext.Request; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelValidationException; +import dev.cel.common.types.StructTypeReference; +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.Map; + +/** + * Exercise6 describes how to build proto message types within CEL. + * + *

Given an input `jwt` and time `now` construct a `google.rpc.context.AttributeContext.Request` + * with the `time` and `auth` fields populated according to the + * google.rpc.context.AttributeContext.Request specification. + */ +final class Exercise6 { + + /** + * Compiles the input expression. + * + * @throws IllegalArgumentException If the expression is malformed due to syntactic or semantic + * errors. + */ + CelAbstractSyntaxTree compile(String expression) { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + // Set the container here for "google.rpc.context.AttributeContext" + // Declare variables for "jwt" and "now" here + .addMessageTypes(Request.getDescriptor()) + .setResultType(StructTypeReference.create(Request.getDescriptor().getFullName())) + .build(); + + try { + return celCompiler.compile(expression).getAst(); + } catch (CelValidationException e) { + throw new IllegalArgumentException("Failed to compile expression.", e); + } + } + + /** Evaluates the compiled AST with the user provided parameter values. */ + Object eval(CelAbstractSyntaxTree ast, Map parameterValues) { + CelRuntime celRuntime = + CelRuntimeFactory.standardCelRuntimeBuilder() + .addMessageTypes(Request.getDescriptor()) + .build(); + + try { + CelRuntime.Program program = celRuntime.createProgram(ast); + return program.eval(parameterValues); + } catch (CelEvaluationException e) { + throw new IllegalArgumentException("Evaluation error has occurred.", e); + } + } +} diff --git a/codelab/src/main/codelab/Exercise7.java b/codelab/src/main/codelab/Exercise7.java new file mode 100644 index 000000000..ce2efd88e --- /dev/null +++ b/codelab/src/main/codelab/Exercise7.java @@ -0,0 +1,72 @@ +// 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. + +package codelab; + +import com.google.rpc.context.AttributeContext.Request; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelValidationException; +import dev.cel.common.types.SimpleType; +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.Map; + +/** + * Exercise7 introduces macros for dealing with repeated fields and maps. + * + *

Determine whether the `jwt.extra_claims` has at least one key that starts with the `group` + * prefix, and ensure that all group-like keys have list values containing only strings that end + * with '@acme.co`. + */ +final class Exercise7 { + + /** + * Compiles the input expression. + * + * @throws IllegalArgumentException If the expression is malformed due to syntactic or semantic + * errors. + */ + CelAbstractSyntaxTree compile(String expression) { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addVar("jwt", SimpleType.DYN) + // Set the macros here for `all`, `filter` and `exists`. + .setResultType(SimpleType.BOOL) + .build(); + + try { + return celCompiler.compile(expression).getAst(); + } catch (CelValidationException e) { + throw new IllegalArgumentException("Failed to compile expression.", e); + } + } + + /** Evaluates the compiled AST with the user provided parameter values. */ + Object eval(CelAbstractSyntaxTree ast, Map parameterValues) { + CelRuntime celRuntime = + CelRuntimeFactory.standardCelRuntimeBuilder() + .addMessageTypes(Request.getDescriptor()) + .build(); + + try { + CelRuntime.Program program = celRuntime.createProgram(ast); + return program.eval(parameterValues); + } catch (CelEvaluationException e) { + throw new IllegalArgumentException("Evaluation error has occurred.", e); + } + } +} diff --git a/codelab/src/main/codelab/Exercise8.java b/codelab/src/main/codelab/Exercise8.java new file mode 100644 index 000000000..d38854687 --- /dev/null +++ b/codelab/src/main/codelab/Exercise8.java @@ -0,0 +1,76 @@ +// 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. + +package codelab; + +import com.google.rpc.context.AttributeContext; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelValidationException; +import dev.cel.common.CelValidationResult; +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.runtime.CelEvaluationException; +import dev.cel.runtime.CelRuntime; +import dev.cel.runtime.CelRuntimeFactory; +import java.util.Map; + +/** + * Exercise8 demonstrates how to leverage canonical CEL validators to perform advanced validations + * on an AST and CEL optimizers to improve evaluation efficiency. + */ +final class Exercise8 { + private static final CelCompiler CEL_COMPILER = + CelCompilerFactory.standardCelCompilerBuilder() + .addVar("x", SimpleType.INT) + .addVar( + "request", StructTypeReference.create("google.rpc.context.AttributeContext.Request")) + .addMessageTypes(AttributeContext.Request.getDescriptor()) + .build(); + private static final CelRuntime CEL_RUNTIME = + CelRuntimeFactory.standardCelRuntimeBuilder() + .addMessageTypes(AttributeContext.Request.getDescriptor()) + .build(); + + // Statically declare the validator and optimizer here. + + /** + * Compiles the input expression. + * + * @throws CelValidationException If the expression contains parsing or type-checking errors. + */ + CelAbstractSyntaxTree compile(String expression) throws CelValidationException { + return CEL_COMPILER.compile(expression).getAst(); + } + + /** Validates a type-checked AST. */ + @SuppressWarnings("DoNotCallSuggester") + CelValidationResult validate(CelAbstractSyntaxTree checkedAst) { + throw new UnsupportedOperationException("To be implemented"); + } + + /** Optimizes a type-checked AST. */ + @SuppressWarnings("DoNotCallSuggester") + CelAbstractSyntaxTree optimize(CelAbstractSyntaxTree checkedAst) { + throw new UnsupportedOperationException("To be implemented"); + } + + /** Evaluates the compiled AST with the user provided parameter values. */ + Object eval(CelAbstractSyntaxTree ast, Map parameterValues) + throws CelEvaluationException { + CelRuntime.Program program = CEL_RUNTIME.createProgram(ast); + return program.eval(parameterValues); + } +} diff --git a/codelab/src/main/codelab/Exercise9.java b/codelab/src/main/codelab/Exercise9.java new file mode 100644 index 000000000..85705390c --- /dev/null +++ b/codelab/src/main/codelab/Exercise9.java @@ -0,0 +1,99 @@ +// 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. + +package codelab; + +import com.google.rpc.context.AttributeContext; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelFunctionDecl; +import dev.cel.common.CelOverloadDecl; +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.parser.CelStandardMacro; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.CelRuntime; +import dev.cel.runtime.CelRuntimeFactory; +import dev.cel.validator.CelValidator; +import dev.cel.validator.CelValidatorFactory; + +/** + * Exercise9 demonstrates how to author a custom AST validator to perform domain specific + * validations. + * + *

Given a `google.rpc.context.AttributeContext.Request` message, validate that its fields follow + * the expected HTTP specification. + * + *

Given an expression containing an expensive function call, validate that it is not nested + * within a macro. + */ +final class Exercise9 { + private static final CelCompiler CEL_COMPILER = + CelCompilerFactory.standardCelCompilerBuilder() + .setStandardMacros(CelStandardMacro.ALL) + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "is_prime_number", + CelOverloadDecl.newGlobalOverload( + "is_prime_number_int", + "Invokes an expensive RPC call to check if the value is a prime number.", + SimpleType.BOOL, + SimpleType.INT))) + .addMessageTypes(AttributeContext.Request.getDescriptor()) + .build(); + private static final CelRuntime CEL_RUNTIME = + CelRuntimeFactory.standardCelRuntimeBuilder() + .addMessageTypes(AttributeContext.Request.getDescriptor()) + .build(); + private static final CelValidator CEL_VALIDATOR = + CelValidatorFactory.standardCelValidatorBuilder(CEL_COMPILER, CEL_RUNTIME) + // Add your custom AST validators here + .build(); + + /** + * Compiles the input expression. + * + * @throws CelValidationException If the expression contains parsing or type-checking errors. + */ + CelAbstractSyntaxTree compile(String expression) throws CelValidationException { + return CEL_COMPILER.compile(expression).getAst(); + } + + /** Validates a type-checked AST. */ + CelValidationResult validate(CelAbstractSyntaxTree checkedAst) { + return CEL_VALIDATOR.validate(checkedAst); + } + + /** Evaluates the compiled AST. */ + Object eval(CelAbstractSyntaxTree ast) throws CelEvaluationException { + return CEL_RUNTIME.createProgram(ast).eval(); + } + + /** + * Performs general validation on AttributeContext.Request message. The validator raises errors if + * the HTTP request is malformed and semantically invalid (e.g: contains disallowed HTTP methods). + * Warnings are presented if there's potential problems with the contents of the request (e.g: + * using "http" instead of "https" for scheme). + */ + static final class AttributeContextRequestValidator { + // Implement validate method here + } + + /** Prevents nesting an expensive function call within a macro. */ + static final class ComprehensionSafetyValidator { + // Implement validate method here + } +} diff --git a/codelab/src/main/codelab/solutions/BUILD.bazel b/codelab/src/main/codelab/solutions/BUILD.bazel index c6bc0a0fe..eae465fa7 100644 --- a/codelab/src/main/codelab/solutions/BUILD.bazel +++ b/codelab/src/main/codelab/solutions/BUILD.bazel @@ -1,3 +1,5 @@ +load("@rules_java//java:defs.bzl", "java_library") + package( default_applicable_licenses = [ "//:license", @@ -9,14 +11,36 @@ java_library( name = "solutions", srcs = glob(["*.java"]), deps = [ - "//common", + "//bundle:cel", + "//common:cel_ast", "//common:compiler_common", + "//common:container", + "//common:proto_json_adapter", + "//common/ast", + "//common/navigation", "//common/types", "//common/types:type_providers", "//compiler", "//compiler:compiler_builder", + "//optimizer", + "//optimizer:optimization_exception", + "//optimizer:optimizer_builder", + "//optimizer/optimizers:common_subexpression_elimination", + "//optimizer/optimizers:constant_folding", + "//parser:macro", "//runtime", - "@maven//:com_google_api_grpc_proto_google_common_protos", + "//runtime:function_binding", + "//validator", + "//validator:ast_validator", + "//validator:validator_builder", + "//validator/validators:duration", + "//validator/validators:homogeneous_literal", + "//validator/validators:regex", + "//validator/validators:timestamp", + "@com_google_googleapis//google/rpc/context:attribute_context_java_proto", "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + "@maven//:com_google_protobuf_protobuf_java_util", + "@maven_android//:com_google_protobuf_protobuf_javalite", ], ) diff --git a/codelab/src/main/codelab/solutions/Exercise4.java b/codelab/src/main/codelab/solutions/Exercise4.java index 33366a1e4..b3cc82a24 100644 --- a/codelab/src/main/codelab/solutions/Exercise4.java +++ b/codelab/src/main/codelab/solutions/Exercise4.java @@ -28,8 +28,8 @@ import dev.cel.compiler.CelCompiler; import dev.cel.compiler.CelCompilerFactory; import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.CelFunctionBinding; import dev.cel.runtime.CelRuntime; -import dev.cel.runtime.CelRuntime.CelFunctionBinding; import dev.cel.runtime.CelRuntimeFactory; import java.util.Map; diff --git a/codelab/src/main/codelab/solutions/Exercise5.java b/codelab/src/main/codelab/solutions/Exercise5.java new file mode 100644 index 000000000..e948adfed --- /dev/null +++ b/codelab/src/main/codelab/solutions/Exercise5.java @@ -0,0 +1,80 @@ +// 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. + +package codelab.solutions; + +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.Struct; +import com.google.protobuf.util.JsonFormat; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelProtoJsonAdapter; +import dev.cel.common.CelValidationException; +import dev.cel.common.types.SimpleType; +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.Map; + +/** + * Exercise5 covers how to build complex objects as CEL literals. + * + *

Given the input variable "time", construct a JWT with an expiry of 5 minutes + */ +final class Exercise5 { + + /** + * Compiles the input expression. + * + * @throws IllegalArgumentException If the expression is malformed due to syntactic or semantic + * errors. + */ + CelAbstractSyntaxTree compile(String expression) { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addVar("time", SimpleType.TIMESTAMP) + .build(); + + try { + return celCompiler.compile(expression).getAst(); + } catch (CelValidationException e) { + throw new IllegalArgumentException("Failed to compile expression.", e); + } + } + + /** + * Evaluates the compiled AST with the user provided parameter values. + * + * @throws IllegalArgumentException If the compiled expression in AST fails to evaluate. + */ + Object eval(CelAbstractSyntaxTree ast, Map parameterValues) { + CelRuntime celRuntime = CelRuntimeFactory.standardCelRuntimeBuilder().build(); + + try { + CelRuntime.Program program = celRuntime.createProgram(ast); + return program.eval(parameterValues); + } catch (CelEvaluationException e) { + throw new IllegalArgumentException("Evaluation error has occurred.", e); + } + } + + /** Converts the evaluated result into a JSON string using protobuf's google.protobuf.Struct. */ + String toJson(Map map) throws InvalidProtocolBufferException { + // Convert the map into google.protobuf.Struct using the CEL provided helper function + Struct jsonStruct = CelProtoJsonAdapter.adaptToJsonStructValue(map); + // Then use Protobuf's JsonFormat to produce a JSON string output. + return JsonFormat.printer().omittingInsignificantWhitespace().print(jsonStruct); + } +} diff --git a/codelab/src/main/codelab/solutions/Exercise6.java b/codelab/src/main/codelab/solutions/Exercise6.java new file mode 100644 index 000000000..9b6c59949 --- /dev/null +++ b/codelab/src/main/codelab/solutions/Exercise6.java @@ -0,0 +1,76 @@ +// 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. + +package codelab.solutions; + +import com.google.rpc.context.AttributeContext.Request; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelContainer; +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.runtime.CelEvaluationException; +import dev.cel.runtime.CelRuntime; +import dev.cel.runtime.CelRuntimeFactory; +import java.util.Map; + +/** + * Exercise6 describes how to build proto message types within CEL. + * + *

Given an input `jwt` and time `now` construct a `google.rpc.context.AttributeContext.Request` + * with the `time` and `auth` fields populated according to the + * google.rpc.context.AttributeContext.Request specification. + */ +final class Exercise6 { + + /** + * Compiles the input expression. + * + * @throws IllegalArgumentException If the expression is malformed due to syntactic or semantic + * errors. + */ + CelAbstractSyntaxTree compile(String expression) { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .setContainer(CelContainer.ofName("google.rpc.context.AttributeContext")) + .addVar("jwt", SimpleType.DYN) + .addVar("now", SimpleType.TIMESTAMP) + .addMessageTypes(Request.getDescriptor()) + .setResultType(StructTypeReference.create(Request.getDescriptor().getFullName())) + .build(); + + try { + return celCompiler.compile(expression).getAst(); + } catch (CelValidationException e) { + throw new IllegalArgumentException("Failed to compile expression.", e); + } + } + + /** Evaluates the compiled AST with the user provided parameter values. */ + Object eval(CelAbstractSyntaxTree ast, Map parameterValues) { + CelRuntime celRuntime = + CelRuntimeFactory.standardCelRuntimeBuilder() + .addMessageTypes(Request.getDescriptor()) + .build(); + + try { + CelRuntime.Program program = celRuntime.createProgram(ast); + return program.eval(parameterValues); + } catch (CelEvaluationException e) { + throw new IllegalArgumentException("Evaluation error has occurred.", e); + } + } +} diff --git a/codelab/src/main/codelab/solutions/Exercise7.java b/codelab/src/main/codelab/solutions/Exercise7.java new file mode 100644 index 000000000..e5be29171 --- /dev/null +++ b/codelab/src/main/codelab/solutions/Exercise7.java @@ -0,0 +1,74 @@ +// 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. + +package codelab.solutions; + +import com.google.rpc.context.AttributeContext.Request; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelValidationException; +import dev.cel.common.types.SimpleType; +import dev.cel.compiler.CelCompiler; +import dev.cel.compiler.CelCompilerFactory; +import dev.cel.parser.CelStandardMacro; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.CelRuntime; +import dev.cel.runtime.CelRuntimeFactory; +import java.util.Map; + +/** + * Exercise7 introduces macros for dealing with repeated fields and maps. + * + *

Determine whether the `jwt.extra_claims` has at least one key that starts with the `group` + * prefix, and ensure that all group-like keys have list values containing only strings that end + * with '@acme.co`. + */ +final class Exercise7 { + + /** + * Compiles the input expression. + * + * @throws IllegalArgumentException If the expression is malformed due to syntactic or semantic + * errors. + */ + CelAbstractSyntaxTree compile(String expression) { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addVar("jwt", SimpleType.DYN) + .setStandardMacros( + CelStandardMacro.ALL, CelStandardMacro.FILTER, CelStandardMacro.EXISTS) + .setResultType(SimpleType.BOOL) + .build(); + + try { + return celCompiler.compile(expression).getAst(); + } catch (CelValidationException e) { + throw new IllegalArgumentException("Failed to compile expression.", e); + } + } + + /** Evaluates the compiled AST with the user provided parameter values. */ + Object eval(CelAbstractSyntaxTree ast, Map parameterValues) { + CelRuntime celRuntime = + CelRuntimeFactory.standardCelRuntimeBuilder() + .addMessageTypes(Request.getDescriptor()) + .build(); + + try { + CelRuntime.Program program = celRuntime.createProgram(ast); + return program.eval(parameterValues); + } catch (CelEvaluationException e) { + throw new IllegalArgumentException("Evaluation error has occurred.", e); + } + } +} diff --git a/codelab/src/main/codelab/solutions/Exercise8.java b/codelab/src/main/codelab/solutions/Exercise8.java new file mode 100644 index 000000000..161089354 --- /dev/null +++ b/codelab/src/main/codelab/solutions/Exercise8.java @@ -0,0 +1,106 @@ +// 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. + +package codelab.solutions; + +import com.google.rpc.context.AttributeContext; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelValidationException; +import dev.cel.common.CelValidationResult; +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.optimizer.CelOptimizationException; +import dev.cel.optimizer.CelOptimizer; +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; +import dev.cel.validator.CelValidator; +import dev.cel.validator.CelValidatorFactory; +import dev.cel.validator.validators.DurationLiteralValidator; +import dev.cel.validator.validators.HomogeneousLiteralValidator; +import dev.cel.validator.validators.RegexLiteralValidator; +import dev.cel.validator.validators.TimestampLiteralValidator; +import java.util.Map; + +/** + * Exercise8 demonstrates how to leverage canonical CEL validators to perform advanced validations + * on an AST and CEL optimizers to improve evaluation efficiency. + */ +final class Exercise8 { + private static final CelCompiler CEL_COMPILER = + CelCompilerFactory.standardCelCompilerBuilder() + .addVar("x", SimpleType.INT) + .addVar( + "request", StructTypeReference.create("google.rpc.context.AttributeContext.Request")) + .addMessageTypes(AttributeContext.Request.getDescriptor()) + .build(); + private static final CelRuntime CEL_RUNTIME = + CelRuntimeFactory.standardCelRuntimeBuilder() + .addMessageTypes(AttributeContext.Request.getDescriptor()) + .build(); + + // Just like the compiler and runtime, the validator and optimizer can be statically + // initialized as their instances are immutable. + private static final CelValidator CEL_VALIDATOR = + CelValidatorFactory.standardCelValidatorBuilder(CEL_COMPILER, CEL_RUNTIME) + .addAstValidators( + TimestampLiteralValidator.INSTANCE, + DurationLiteralValidator.INSTANCE, + RegexLiteralValidator.INSTANCE, + HomogeneousLiteralValidator.newInstance()) + .build(); + private static final CelOptimizer CEL_OPTIMIZER = + CelOptimizerFactory.standardCelOptimizerBuilder(CEL_COMPILER, CEL_RUNTIME) + .addAstOptimizers( + ConstantFoldingOptimizer.getInstance(), + SubexpressionOptimizer.newInstance( + SubexpressionOptimizerOptions.newBuilder().enableCelBlock(true).build())) + .build(); + + /** + * Compiles the input expression. + * + * @throws CelValidationException If the expression contains parsing or type-checking errors. + */ + CelAbstractSyntaxTree compile(String expression) throws CelValidationException { + return CEL_COMPILER.compile(expression).getAst(); + } + + /** Validates a type-checked AST. */ + CelValidationResult validate(CelAbstractSyntaxTree checkedAst) { + return CEL_VALIDATOR.validate(checkedAst); + } + + /** + * Optimizes a type-checked AST. + * + * @throws CelOptimizationException If the optimization fails. + */ + CelAbstractSyntaxTree optimize(CelAbstractSyntaxTree checkedAst) throws CelOptimizationException { + return CEL_OPTIMIZER.optimize(checkedAst); + } + + /** Evaluates the compiled AST with the user provided parameter values. */ + Object eval(CelAbstractSyntaxTree ast, Map parameterValues) + throws CelEvaluationException { + CelRuntime.Program program = CEL_RUNTIME.createProgram(ast); + return program.eval(parameterValues); + } +} diff --git a/codelab/src/main/codelab/solutions/Exercise9.java b/codelab/src/main/codelab/solutions/Exercise9.java new file mode 100644 index 000000000..2b45c3539 --- /dev/null +++ b/codelab/src/main/codelab/solutions/Exercise9.java @@ -0,0 +1,173 @@ +// 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. + +package codelab.solutions; + +import com.google.common.collect.ImmutableSet; +import com.google.rpc.context.AttributeContext; +import dev.cel.bundle.Cel; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelFunctionDecl; +import dev.cel.common.CelOverloadDecl; +import dev.cel.common.CelValidationException; +import dev.cel.common.CelValidationResult; +import dev.cel.common.ast.CelExpr; +import dev.cel.common.ast.CelExpr.CelStruct; +import dev.cel.common.ast.CelExpr.ExprKind.Kind; +import dev.cel.common.navigation.CelNavigableAst; +import dev.cel.common.types.SimpleType; +import dev.cel.compiler.CelCompiler; +import dev.cel.compiler.CelCompilerFactory; +import dev.cel.parser.CelStandardMacro; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.CelRuntime; +import dev.cel.runtime.CelRuntimeFactory; +import dev.cel.validator.CelAstValidator; +import dev.cel.validator.CelValidator; +import dev.cel.validator.CelValidatorFactory; + +/** + * Exercise9 demonstrates how to author a custom AST validator to perform domain specific + * validations. + * + *

Given a `google.rpc.context.AttributeContext.Request` message, validate that its fields follow + * the expected HTTP specification. + * + *

Given an expression containing an expensive function call, validate that it is not nested + * within a macro. + */ +final class Exercise9 { + private static final CelCompiler CEL_COMPILER = + CelCompilerFactory.standardCelCompilerBuilder() + .setStandardMacros(CelStandardMacro.ALL) + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "is_prime_number", + CelOverloadDecl.newGlobalOverload( + "is_prime_number_int", + "Invokes an expensive RPC call to check if the value is a prime number.", + SimpleType.BOOL, + SimpleType.INT))) + .addMessageTypes(AttributeContext.Request.getDescriptor()) + .build(); + private static final CelRuntime CEL_RUNTIME = + CelRuntimeFactory.standardCelRuntimeBuilder() + .addMessageTypes(AttributeContext.Request.getDescriptor()) + .build(); + private static final CelValidator CEL_VALIDATOR = + CelValidatorFactory.standardCelValidatorBuilder(CEL_COMPILER, CEL_RUNTIME) + .addAstValidators( + new AttributeContextRequestValidator(), // + new ComprehensionSafetyValidator()) + .build(); + + /** + * Compiles the input expression. + * + * @throws CelValidationException If the expression contains parsing or type-checking errors. + */ + CelAbstractSyntaxTree compile(String expression) throws CelValidationException { + return CEL_COMPILER.compile(expression).getAst(); + } + + /** Validates a type-checked AST. */ + CelValidationResult validate(CelAbstractSyntaxTree checkedAst) { + return CEL_VALIDATOR.validate(checkedAst); + } + + /** Evaluates the compiled AST. */ + Object eval(CelAbstractSyntaxTree ast) throws CelEvaluationException { + return CEL_RUNTIME.createProgram(ast).eval(); + } + + /** + * Performs general validation on AttributeContext.Request message. The validator raises errors if + * the HTTP request is malformed and semantically invalid (e.g: contains disallowed HTTP methods). + * Warnings are presented if there's potential problems with the contents of the request (e.g: + * using "http" instead of "https" for scheme). + */ + static final class AttributeContextRequestValidator implements CelAstValidator { + private static final ImmutableSet ALLOWED_HTTP_METHODS = + ImmutableSet.of("GET", "POST", "PUT", "DELETE"); + + @Override + public void validate(CelNavigableAst navigableAst, Cel cel, IssuesFactory issuesFactory) { + navigableAst + .getRoot() + .allNodes() + .filter(node -> node.getKind().equals(Kind.STRUCT)) + .map(node -> node.expr().struct()) + .filter( + struct -> struct.messageName().equals("google.rpc.context.AttributeContext.Request")) + .forEach( + struct -> { + for (CelStruct.Entry entry : struct.entries()) { + String fieldKey = entry.fieldKey(); + if (fieldKey.equals("method")) { + String entryStringValue = getStringValue(entry.value()); + if (!ALLOWED_HTTP_METHODS.contains(entryStringValue)) { + issuesFactory.addError( + entry.value().id(), entryStringValue + " is not an allowed HTTP method."); + } + } else if (fieldKey.equals("scheme")) { + String entryStringValue = getStringValue(entry.value()); + if (!entryStringValue.equals("https")) { + issuesFactory.addWarning( + entry.value().id(), "Prefer using https for safety."); + } + } + } + }); + } + + /** + * Reads the underlying string value from the expression. + * + * @throws UnsupportedOperationException if the expression is not a constant string value. + */ + private static String getStringValue(CelExpr celExpr) { + return celExpr.constant().stringValue(); + } + } + + /** Prevents nesting an expensive function call within a macro. */ + static final class ComprehensionSafetyValidator implements CelAstValidator { + private static final String EXPENSIVE_FUNCTION_NAME = "is_prime_number"; + + @Override + public void validate(CelNavigableAst navigableAst, Cel cel, IssuesFactory issuesFactory) { + navigableAst + .getRoot() + .allNodes() + .filter(node -> node.getKind().equals(Kind.COMPREHENSION)) + .forEach( + comprehensionNode -> { + boolean isFunctionWithinMacro = + comprehensionNode + .descendants() + .anyMatch( + node -> + node.expr() + .callOrDefault() + .function() + .equals(EXPENSIVE_FUNCTION_NAME)); + if (isFunctionWithinMacro) { + issuesFactory.addError( + comprehensionNode.id(), + EXPENSIVE_FUNCTION_NAME + " function cannot be used within CEL macros."); + } + }); + } + } +} diff --git a/codelab/src/test/codelab/BUILD.bazel b/codelab/src/test/codelab/BUILD.bazel index 40025f21d..fb3f1e235 100644 --- a/codelab/src/test/codelab/BUILD.bazel +++ b/codelab/src/test/codelab/BUILD.bazel @@ -1,3 +1,5 @@ +load("@rules_java//java:defs.bzl", "java_test") + package(default_applicable_licenses = [ "//:license", ]) @@ -10,7 +12,7 @@ java_test( deps = [ "//:java_truth", "//codelab", - "//common", + "//common:cel_ast", "//compiler", "//compiler:compiler_builder", "@maven//:junit_junit", @@ -25,9 +27,9 @@ java_test( deps = [ "//:java_truth", "//codelab", - "//common", + "//common:cel_ast", "//common/types", - "@maven//:com_google_api_grpc_proto_google_common_protos", + "@com_google_googleapis//google/rpc/context:attribute_context_java_proto", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", "@maven//:com_google_testparameterinjector_test_parameter_injector", @@ -57,8 +59,8 @@ java_test( deps = [ "//:java_truth", "//codelab", - "//common", - "@maven//:com_google_api_grpc_proto_google_common_protos", + "//common:cel_ast", + "@com_google_googleapis//google/rpc/context:attribute_context_java_proto", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", "@maven//:com_google_testparameterinjector_test_parameter_injector", @@ -66,6 +68,90 @@ java_test( ], ) +java_test( + name = "Exercise5Test", + srcs = ["Exercise5Test.java"], + tags = ["notap"], + test_class = "codelab.Exercise5Test", + deps = [ + "//:java_truth", + "//codelab", + "//common:cel_ast", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + "@maven//:com_google_testparameterinjector_test_parameter_injector", + "@maven//:junit_junit", + ], +) + +java_test( + name = "Exercise6Test", + srcs = ["Exercise6Test.java"], + tags = ["notap"], + test_class = "codelab.Exercise6Test", + deps = [ + "//:java_truth", + "//codelab", + "//common:cel_ast", + "//common/internal:proto_time_utils", + "@com_google_googleapis//google/rpc/context:attribute_context_java_proto", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + "@maven//:com_google_testparameterinjector_test_parameter_injector", + "@maven//:junit_junit", + ], +) + +java_test( + name = "Exercise7Test", + srcs = ["Exercise7Test.java"], + tags = ["notap"], + test_class = "codelab.Exercise7Test", + deps = [ + "//:java_truth", + "//codelab", + "//common:cel_ast", + "@maven//:com_google_guava_guava", + "@maven//:com_google_testparameterinjector_test_parameter_injector", + "@maven//:junit_junit", + ], +) + +java_test( + name = "Exercise8Test", + srcs = ["Exercise8Test.java"], + tags = ["notap"], + test_class = "codelab.Exercise8Test", + deps = [ + "//:java_truth", + "//codelab", + "//common:cel_ast", + "//common:compiler_common", + "//parser:unparser", + "//runtime", + "@com_google_googleapis//google/rpc/context:attribute_context_java_proto", + "@maven//:com_google_guava_guava", + "@maven//:com_google_testparameterinjector_test_parameter_injector", + "@maven//:junit_junit", + ], +) + +java_test( + name = "Exercise9Test", + srcs = ["Exercise9Test.java"], + tags = ["notap"], + test_class = "codelab.Exercise9Test", + deps = [ + "//:java_truth", + "//codelab", + "//common:cel_ast", + "//common:compiler_common", + "@com_google_googleapis//google/rpc/context:attribute_context_java_proto", + "@maven//:com_google_testparameterinjector_test_parameter_injector", + "@maven//:junit_junit", + ], +) + test_suite( name = "exercise_test_suite", tags = ["notap"], diff --git a/codelab/src/test/codelab/Exercise1Test.java b/codelab/src/test/codelab/Exercise1Test.java index 6d7a16948..7ea1aa1eb 100644 --- a/codelab/src/test/codelab/Exercise1Test.java +++ b/codelab/src/test/codelab/Exercise1Test.java @@ -68,6 +68,6 @@ public void evaluate_divideByZeroExpression_throwsEvaluationException() throws E IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> exercise1.eval(ast)); - assertThat(exception).hasMessageThat().contains("evaluation error: / by zero"); + assertThat(exception).hasMessageThat().contains("evaluation error at :1: / by zero"); } } diff --git a/codelab/src/test/codelab/Exercise3Test.java b/codelab/src/test/codelab/Exercise3Test.java index 9c62d9ad5..8bf0aa027 100644 --- a/codelab/src/test/codelab/Exercise3Test.java +++ b/codelab/src/test/codelab/Exercise3Test.java @@ -57,7 +57,8 @@ public void evaluate_logicalOrFailure_throwsException(String expression) { assertThat(exception).hasMessageThat().contains("Evaluation error has occurred."); assertThat(exception).hasCauseThat().isInstanceOf(CelEvaluationException.class); - assertThat(exception).hasCauseThat().hasMessageThat().contains("evaluation error: / by zero"); + assertThat(exception).hasCauseThat().hasMessageThat().contains("evaluation error"); + assertThat(exception).hasCauseThat().hasMessageThat().contains("/ by zero"); } @Test @@ -77,7 +78,8 @@ public void evaluate_logicalAndFailure_throwsException(String expression) { assertThat(exception).hasMessageThat().contains("Evaluation error has occurred."); assertThat(exception).hasCauseThat().isInstanceOf(CelEvaluationException.class); - assertThat(exception).hasCauseThat().hasMessageThat().contains("evaluation error: / by zero"); + assertThat(exception).hasCauseThat().hasMessageThat().contains("evaluation error"); + assertThat(exception).hasCauseThat().hasMessageThat().contains("/ by zero"); } @Test @@ -98,6 +100,7 @@ public void evaluate_ternaryFailure_throwsException(String expression) { assertThat(exception).hasMessageThat().contains("Evaluation error has occurred."); assertThat(exception).hasCauseThat().isInstanceOf(CelEvaluationException.class); - assertThat(exception).hasCauseThat().hasMessageThat().contains("evaluation error: / by zero"); + assertThat(exception).hasCauseThat().hasMessageThat().contains("evaluation error"); + assertThat(exception).hasCauseThat().hasMessageThat().contains("/ by zero"); } } diff --git a/codelab/src/test/codelab/Exercise5Test.java b/codelab/src/test/codelab/Exercise5Test.java new file mode 100644 index 000000000..128300166 --- /dev/null +++ b/codelab/src/test/codelab/Exercise5Test.java @@ -0,0 +1,66 @@ +// 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. + +package codelab; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableMap; +import com.google.protobuf.Timestamp; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.common.CelAbstractSyntaxTree; +import java.util.Map; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public final class Exercise5Test { + private final Exercise5 exercise5 = new Exercise5(); + + @Test + public void evaluate_jwtWithTimeVariable_producesJsonString() throws Exception { + // Note the quoted keys in the CEL map literal. For proto messages the field names are unquoted + // as they represent well-defined identifiers. + String jwt = + "{'sub': 'serviceAccount:delegate@acme.co'," + + "'aud': 'my-project'," + + "'iss': 'auth.acme.com:12350'," + + "'iat': time," + + "'nbf': time," + + "'exp': time + duration('300s')," + + "'extra_claims': {" + + "'group': 'admin'" + + "}}"; + CelAbstractSyntaxTree ast = exercise5.compile(jwt); + + // The output of the program is a map type. + @SuppressWarnings("unchecked") + Map evaluatedResult = + (Map) + exercise5.eval( + ast, + ImmutableMap.of("time", Timestamp.newBuilder().setSeconds(1698361778).build())); + String jsonOutput = exercise5.toJson(evaluatedResult); + + assertThat(jsonOutput) + .isEqualTo( + "{\"sub\":\"serviceAccount:delegate@acme.co\"," + + "\"aud\":\"my-project\"," + + "\"iss\":\"auth.acme.com:12350\"," + + "\"iat\":\"2023-10-26T23:09:38Z\"," + + "\"nbf\":\"2023-10-26T23:09:38Z\"," + + "\"exp\":\"2023-10-26T23:14:38Z\"," + + "\"extra_claims\":{\"group\":\"admin\"}}"); + } +} diff --git a/codelab/src/test/codelab/Exercise6Test.java b/codelab/src/test/codelab/Exercise6Test.java new file mode 100644 index 000000000..d4add9b90 --- /dev/null +++ b/codelab/src/test/codelab/Exercise6Test.java @@ -0,0 +1,102 @@ +// 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. + +package codelab; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableMap; +import com.google.protobuf.Struct; +import com.google.protobuf.Timestamp; +import com.google.protobuf.Value; +import com.google.rpc.context.AttributeContext; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.internal.ProtoTimeUtils; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public final class Exercise6Test { + private final Exercise6 exercise6 = new Exercise6(); + + @Test + public void evaluate_constructAttributeContext() { + // Given JSON web token and the current time as input variables, + // Setup an expression to construct an AttributeContext protobuf object. + // + // Note: the field names within the proto message types are not quoted as they + // are well-defined names composed of valid identifier characters. Also, note + // that when building nested proto objects, the message name needs to prefix + // the object construction. + String expression = + "Request{\n" + + "auth: Auth{" + + " principal: jwt.iss + '/' + jwt.sub," + + " audiences: [jwt.aud]," + + " presenter: 'azp' in jwt ? jwt.azp : ''," + + " claims: jwt" + + "}," + + "time: now" + + "}"; + // Values for `now` and `jwt` variables to be passed into the runtime + Timestamp now = ProtoTimeUtils.now(); + ImmutableMap jwt = + ImmutableMap.of( + "sub", "serviceAccount:delegate@acme.co", + "aud", "my-project", + "iss", "auth.acme.com:12350", + "extra_claims", ImmutableMap.of("group", "admin")); + AttributeContext.Request expectedMessage = + AttributeContext.Request.newBuilder() + .setTime(now) + .setAuth( + AttributeContext.Auth.newBuilder() + .setPrincipal("auth.acme.com:12350/serviceAccount:delegate@acme.co") + .addAudiences("my-project") + .setClaims( + Struct.newBuilder() + .putAllFields( + ImmutableMap.of( + "sub", newStringValue("serviceAccount:delegate@acme.co"), + "aud", newStringValue("my-project"), + "iss", newStringValue("auth.acme.com:12350"))) + .putFields( + "extra_claims", + Value.newBuilder() + .setStructValue( + Struct.newBuilder() + .putFields("group", newStringValue("admin")) + .build()) + .build()))) + .build(); + + // Compile the `Request` message construction expression and validate that + // the resulting expression type matches the fully qualified message name. + CelAbstractSyntaxTree ast = exercise6.compile(expression); + AttributeContext.Request evaluatedResult = + (AttributeContext.Request) + exercise6.eval( + ast, + ImmutableMap.of( + "now", now, + "jwt", jwt)); + + assertThat(evaluatedResult).isEqualTo(expectedMessage); + } + + private static Value newStringValue(String value) { + return Value.newBuilder().setStringValue(value).build(); + } +} diff --git a/codelab/src/test/codelab/Exercise7Test.java b/codelab/src/test/codelab/Exercise7Test.java new file mode 100644 index 000000000..2caf0e4f9 --- /dev/null +++ b/codelab/src/test/codelab/Exercise7Test.java @@ -0,0 +1,60 @@ +// 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. + +package codelab; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.common.CelAbstractSyntaxTree; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public final class Exercise7Test { + private final Exercise7 exercise7 = new Exercise7(); + + @Test + public void evaluate_checkJwtClaimsWithMacro_evaluatesToTrue() { + String expression = + "jwt.extra_claims.exists(c, c.startsWith('group'))" + + " && jwt.extra_claims" + + ".filter(c, c.startsWith('group'))" + + ".all(c, jwt.extra_claims[c]" + + ".all(g, g.endsWith('@acme.co')))"; + ImmutableMap jwt = + ImmutableMap.of( + "sub", + "serviceAccount:delegate@acme.co", + "aud", + "my-project", + "iss", + "auth.acme.com:12350", + "extra_claims", + ImmutableMap.of("group1", ImmutableList.of("admin@acme.co", "analyst@acme.co")), + "labels", + ImmutableList.of("metadata", "prod", "pii"), + "groupN", + ImmutableList.of("forever@acme.co")); + CelAbstractSyntaxTree ast = exercise7.compile(expression); + + // Evaluate a complex-ish JWT with two groups that satisfy the criteria. + // Output: true. + boolean evaluatedResult = (boolean) exercise7.eval(ast, ImmutableMap.of("jwt", jwt)); + + assertThat(evaluatedResult).isTrue(); + } +} diff --git a/codelab/src/test/codelab/Exercise8Test.java b/codelab/src/test/codelab/Exercise8Test.java new file mode 100644 index 000000000..36fef248c --- /dev/null +++ b/codelab/src/test/codelab/Exercise8Test.java @@ -0,0 +1,147 @@ +// 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. + +package codelab; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableMap; +import com.google.rpc.context.AttributeContext; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelValidationException; +import dev.cel.common.CelValidationResult; +import dev.cel.parser.CelUnparser; +import dev.cel.parser.CelUnparserFactory; +import dev.cel.runtime.CelEvaluationException; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public final class Exercise8Test { + + private final Exercise8 exercise8 = new Exercise8(); + + @Test + public void validate_invalidTimestampLiteral_returnsError() throws Exception { + CelAbstractSyntaxTree ast = exercise8.compile("timestamp('bad')"); + + CelValidationResult validationResult = exercise8.validate(ast); + + assertThat(validationResult.hasError()).isTrue(); + assertThat(validationResult.getErrorString()) + .isEqualTo( + "ERROR: :1:11: timestamp validation failed. Reason: evaluation error: Text 'bad'" + + " could not be parsed at index 0\n" + + " | timestamp('bad')\n" + + " | ..........^"); + } + + @Test + public void validate_invalidDurationLiteral_returnsError() throws Exception { + CelAbstractSyntaxTree ast = exercise8.compile("duration('bad')"); + + CelValidationResult validationResult = exercise8.validate(ast); + + assertThat(validationResult.hasError()).isTrue(); + assertThat(validationResult.getErrorString()) + .isEqualTo( + "ERROR: :1:10: duration validation failed. Reason: evaluation error: invalid" + + " duration format\n" + + " | duration('bad')\n" + + " | .........^"); + } + + @Test + public void validate_invalidRegexLiteral_returnsError() throws Exception { + CelAbstractSyntaxTree ast = exercise8.compile("'text'.matches('**')"); + + CelValidationResult validationResult = exercise8.validate(ast); + + assertThat(validationResult.hasError()).isTrue(); + assertThat(validationResult.getErrorString()) + .isEqualTo( + "ERROR: :1:16: Regex validation failed. Reason: Dangling meta character '*' near" + + " index 0\n" + + "**\n" + + "^\n" + + " | 'text'.matches('**')\n" + + " | ...............^"); + } + + @Test + public void validate_listHasMixedLiterals_throws() throws Exception { + CelAbstractSyntaxTree ast = exercise8.compile("3 in [1, 2, '3']"); + + // Note that `CelValidationResult` is the same result class used for the compilation path. This + // means you could alternatively invoke `.getAst()` and handle `CelValidationException` as + // usual. + CelValidationResult validationResult = exercise8.validate(ast); + + CelValidationException e = assertThrows(CelValidationException.class, validationResult::getAst); + assertThat(e) + .hasMessageThat() + .contains( + "ERROR: :1:13: expected type 'int' but found 'string'\n" + + " | 3 in [1, 2, '3']\n" + + " | ............^"); + } + + @Test + public void optimize_constantFold_success() throws Exception { + CelUnparser celUnparser = CelUnparserFactory.newUnparser(); + CelAbstractSyntaxTree ast = exercise8.compile("(1 + 2 + 3 == x) && (x in [1, 2, x])"); + + CelAbstractSyntaxTree optimizedAst = exercise8.optimize(ast); + + assertThat(celUnparser.unparse(optimizedAst)).isEqualTo("6 == x"); + } + + @Test + public void optimize_constantFold_evaluateError() throws Exception { + CelAbstractSyntaxTree ast = + exercise8.compile("request.headers.referer == 'https://' + 'cel.dev'"); + CelAbstractSyntaxTree optimizedAst = exercise8.optimize(ast); + ImmutableMap runtimeParameters = + ImmutableMap.of("request", AttributeContext.Request.getDefaultInstance()); + + CelEvaluationException e1 = + assertThrows(CelEvaluationException.class, () -> exercise8.eval(ast, runtimeParameters)); + CelEvaluationException e2 = + assertThrows( + CelEvaluationException.class, () -> exercise8.eval(optimizedAst, runtimeParameters)); + // Note that the errors below differ by their source position. + assertThat(e1) + .hasMessageThat() + .contains("evaluation error at :15: key 'referer' is not present in map."); + assertThat(e2) + .hasMessageThat() + .contains("evaluation error: key 'referer' is not present in map."); + } + + @Test + public void optimize_commonSubexpressionElimination_success() throws Exception { + CelUnparser celUnparser = CelUnparserFactory.newUnparser(); + CelAbstractSyntaxTree ast = + exercise8.compile( + "request.auth.claims.group == 'admin' || request.auth.claims.group == 'user'"); + + CelAbstractSyntaxTree optimizedAst = exercise8.optimize(ast); + + assertThat(celUnparser.unparse(optimizedAst)) + .isEqualTo( + "cel.@block([request.auth.claims.group], @index0 == \"admin\" || @index0 == \"user\")"); + } +} diff --git a/codelab/src/test/codelab/Exercise9Test.java b/codelab/src/test/codelab/Exercise9Test.java new file mode 100644 index 000000000..7157d61c2 --- /dev/null +++ b/codelab/src/test/codelab/Exercise9Test.java @@ -0,0 +1,95 @@ +// 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. + +package codelab; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.rpc.context.AttributeContext; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelValidationException; +import dev.cel.common.CelValidationResult; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public final class Exercise9Test { + private final Exercise9 exercise9 = new Exercise9(); + + @Test + public void validate_invalidHttpMethod_returnsError() throws Exception { + String expression = + "google.rpc.context.AttributeContext.Request { \n" + + "scheme: 'http', " + + "method: 'GETTT', " // method is misspelled. + + "host: 'cel.dev' \n" + + "}"; + CelAbstractSyntaxTree ast = exercise9.compile(expression); + + CelValidationResult validationResult = exercise9.validate(ast); + + assertThat(validationResult.hasError()).isTrue(); + assertThat(validationResult.getErrorString()) + .isEqualTo( + "ERROR: :2:25: GETTT is not an allowed HTTP method.\n" + + " | scheme: 'http', method: 'GETTT', host: 'cel.dev' \n" + + " | ........................^"); + assertThrows(CelValidationException.class, validationResult::getAst); + } + + @Test + public void validate_schemeIsHttp_returnsWarning() throws Exception { + String expression = + "google.rpc.context.AttributeContext.Request { \n" + + "scheme: 'http', " // https is preferred but not required. + + "method: 'GET', " + + "host: 'cel.dev' \n" + + "}"; + CelAbstractSyntaxTree ast = exercise9.compile(expression); + + CelValidationResult validationResult = exercise9.validate(ast); + + assertThat(validationResult.hasError()).isFalse(); + assertThat(validationResult.getIssueString()) + .isEqualTo( + "WARNING: :2:9: Prefer using https for safety.\n" + + " | scheme: 'http', method: 'GET', host: 'cel.dev' \n" + + " | ........^"); + // Because the validation result does not contain any errors, you can still evaluate it. + assertThat(exercise9.eval(validationResult.getAst())) + .isEqualTo( + AttributeContext.Request.newBuilder() + .setScheme("http") + .setMethod("GET") + .setHost("cel.dev") + .build()); + } + + @Test + public void validate_isPrimeNumberWithinMacro_returnsError() throws Exception { + String expression = "[2,3,5].all(x, is_prime_number(x))"; + CelAbstractSyntaxTree ast = exercise9.compile(expression); + + CelValidationResult validationResult = exercise9.validate(ast); + + assertThat(validationResult.hasError()).isTrue(); + assertThat(validationResult.getErrorString()) + .isEqualTo( + "ERROR: :1:12: is_prime_number function cannot be used within CEL macros.\n" + + " | [2,3,5].all(x, is_prime_number(x))\n" + + " | ...........^"); + } +} diff --git a/codelab/src/test/codelab/solutions/BUILD.bazel b/codelab/src/test/codelab/solutions/BUILD.bazel index c368a8353..9eebbc3f4 100644 --- a/codelab/src/test/codelab/solutions/BUILD.bazel +++ b/codelab/src/test/codelab/solutions/BUILD.bazel @@ -1,3 +1,5 @@ +load("@rules_java//java:defs.bzl", "java_test") + package(default_applicable_licenses = [ "//:license", ]) @@ -9,7 +11,7 @@ java_test( deps = [ "//:java_truth", "//codelab:solutions", - "//common", + "//common:cel_ast", "//compiler", "//compiler:compiler_builder", "@maven//:junit_junit", @@ -23,9 +25,9 @@ java_test( deps = [ "//:java_truth", "//codelab:solutions", - "//common", + "//common:cel_ast", "//common/types", - "@maven//:com_google_api_grpc_proto_google_common_protos", + "@com_google_googleapis//google/rpc/context:attribute_context_java_proto", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", "@maven//:com_google_testparameterinjector_test_parameter_injector", @@ -53,11 +55,90 @@ java_test( deps = [ "//:java_truth", "//codelab:solutions", - "//common", - "@maven//:com_google_api_grpc_proto_google_common_protos", + "//common:cel_ast", + "@com_google_googleapis//google/rpc/context:attribute_context_java_proto", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + "@maven//:com_google_testparameterinjector_test_parameter_injector", + "@maven//:junit_junit", + ], +) + +java_test( + name = "Exercise5Test", + srcs = ["Exercise5Test.java"], + test_class = "codelab.solutions.Exercise5Test", + deps = [ + "//:java_truth", + "//codelab:solutions", + "//common:cel_ast", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", "@maven//:com_google_testparameterinjector_test_parameter_injector", "@maven//:junit_junit", ], ) + +java_test( + name = "Exercise6Test", + srcs = ["Exercise6Test.java"], + test_class = "codelab.solutions.Exercise6Test", + deps = [ + "//:java_truth", + "//codelab:solutions", + "//common:cel_ast", + "//common/internal:proto_time_utils", + "@com_google_googleapis//google/rpc/context:attribute_context_java_proto", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + "@maven//:com_google_testparameterinjector_test_parameter_injector", + "@maven//:junit_junit", + ], +) + +java_test( + name = "Exercise7Test", + srcs = ["Exercise7Test.java"], + test_class = "codelab.solutions.Exercise7Test", + deps = [ + "//:java_truth", + "//codelab:solutions", + "//common:cel_ast", + "@maven//:com_google_guava_guava", + "@maven//:com_google_testparameterinjector_test_parameter_injector", + "@maven//:junit_junit", + ], +) + +java_test( + name = "Exercise8Test", + srcs = ["Exercise8Test.java"], + test_class = "codelab.solutions.Exercise8Test", + deps = [ + "//:java_truth", + "//codelab:solutions", + "//common:cel_ast", + "//common:compiler_common", + "//parser:unparser", + "//runtime", + "@com_google_googleapis//google/rpc/context:attribute_context_java_proto", + "@maven//:com_google_guava_guava", + "@maven//:com_google_testparameterinjector_test_parameter_injector", + "@maven//:junit_junit", + ], +) + +java_test( + name = "Exercise9Test", + srcs = ["Exercise9Test.java"], + test_class = "codelab.solutions.Exercise9Test", + deps = [ + "//:java_truth", + "//codelab:solutions", + "//common:cel_ast", + "//common:compiler_common", + "@com_google_googleapis//google/rpc/context:attribute_context_java_proto", + "@maven//:com_google_testparameterinjector_test_parameter_injector", + "@maven//:junit_junit", + ], +) diff --git a/codelab/src/test/codelab/solutions/Exercise1Test.java b/codelab/src/test/codelab/solutions/Exercise1Test.java index 8b01690d9..c1e93862c 100644 --- a/codelab/src/test/codelab/solutions/Exercise1Test.java +++ b/codelab/src/test/codelab/solutions/Exercise1Test.java @@ -68,6 +68,6 @@ public void evaluate_divideByZeroExpression_throwsEvaluationException() throws E IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> exercise1.eval(ast)); - assertThat(exception).hasMessageThat().contains("evaluation error: / by zero"); + assertThat(exception).hasMessageThat().contains("evaluation error at :1: / by zero"); } } diff --git a/codelab/src/test/codelab/solutions/Exercise3Test.java b/codelab/src/test/codelab/solutions/Exercise3Test.java index 47d1c3470..0bef54ded 100644 --- a/codelab/src/test/codelab/solutions/Exercise3Test.java +++ b/codelab/src/test/codelab/solutions/Exercise3Test.java @@ -57,7 +57,8 @@ public void evaluate_logicalOrFailure_throwsException(String expression) { assertThat(exception).hasMessageThat().contains("Evaluation error has occurred."); assertThat(exception).hasCauseThat().isInstanceOf(CelEvaluationException.class); - assertThat(exception).hasCauseThat().hasMessageThat().contains("evaluation error: / by zero"); + assertThat(exception).hasCauseThat().hasMessageThat().contains("evaluation error"); + assertThat(exception).hasCauseThat().hasMessageThat().contains("/ by zero"); } @Test @@ -83,7 +84,8 @@ public void evaluate_logicalAndFailure_throwsException(String expression) { assertThat(exception).hasMessageThat().contains("Evaluation error has occurred."); assertThat(exception).hasCauseThat().isInstanceOf(CelEvaluationException.class); - assertThat(exception).hasCauseThat().hasMessageThat().contains("evaluation error: / by zero"); + assertThat(exception).hasCauseThat().hasMessageThat().contains("evaluation error"); + assertThat(exception).hasCauseThat().hasMessageThat().contains("/ by zero"); } @Test @@ -108,6 +110,7 @@ public void evaluate_ternaryFailure_throwsException(String expression) { assertThat(exception).hasMessageThat().contains("Evaluation error has occurred."); assertThat(exception).hasCauseThat().isInstanceOf(CelEvaluationException.class); - assertThat(exception).hasCauseThat().hasMessageThat().contains("evaluation error: / by zero"); + assertThat(exception).hasCauseThat().hasMessageThat().contains("evaluation error"); + assertThat(exception).hasCauseThat().hasMessageThat().contains("/ by zero"); } } diff --git a/codelab/src/test/codelab/solutions/Exercise5Test.java b/codelab/src/test/codelab/solutions/Exercise5Test.java new file mode 100644 index 000000000..405a73f4d --- /dev/null +++ b/codelab/src/test/codelab/solutions/Exercise5Test.java @@ -0,0 +1,66 @@ +// 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. + +package codelab.solutions; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableMap; +import com.google.protobuf.Timestamp; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.common.CelAbstractSyntaxTree; +import java.util.Map; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public final class Exercise5Test { + private final Exercise5 exercise5 = new Exercise5(); + + @Test + public void evaluate_jwtWithTimeVariable_producesJsonString() throws Exception { + // Note the quoted keys in the CEL map literal. For proto messages the field names are unquoted + // as they represent well-defined identifiers. + String jwt = + "{'sub': 'serviceAccount:delegate@acme.co'," + + "'aud': 'my-project'," + + "'iss': 'auth.acme.com:12350'," + + "'iat': time," + + "'nbf': time," + + "'exp': time + duration('300s')," + + "'extra_claims': {" + + "'group': 'admin'" + + "}}"; + CelAbstractSyntaxTree ast = exercise5.compile(jwt); + + // The output of the program is a map type. + @SuppressWarnings("unchecked") + Map evaluatedResult = + (Map) + exercise5.eval( + ast, + ImmutableMap.of("time", Timestamp.newBuilder().setSeconds(1698361778).build())); + String jsonOutput = exercise5.toJson(evaluatedResult); + + assertThat(jsonOutput) + .isEqualTo( + "{\"sub\":\"serviceAccount:delegate@acme.co\"," + + "\"aud\":\"my-project\"," + + "\"iss\":\"auth.acme.com:12350\"," + + "\"iat\":\"2023-10-26T23:09:38Z\"," + + "\"nbf\":\"2023-10-26T23:09:38Z\"," + + "\"exp\":\"2023-10-26T23:14:38Z\"," + + "\"extra_claims\":{\"group\":\"admin\"}}"); + } +} diff --git a/codelab/src/test/codelab/solutions/Exercise6Test.java b/codelab/src/test/codelab/solutions/Exercise6Test.java new file mode 100644 index 000000000..fbb1848cc --- /dev/null +++ b/codelab/src/test/codelab/solutions/Exercise6Test.java @@ -0,0 +1,102 @@ +// 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. + +package codelab.solutions; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableMap; +import com.google.protobuf.Struct; +import com.google.protobuf.Timestamp; +import com.google.protobuf.Value; +import com.google.rpc.context.AttributeContext; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.internal.ProtoTimeUtils; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public final class Exercise6Test { + private final Exercise6 exercise6 = new Exercise6(); + + @Test + public void evaluate_constructAttributeContext() { + // Given JSON web token and the current time as input variables, + // Setup an expression to construct an AttributeContext protobuf object. + // + // Note: the field names within the proto message types are not quoted as they + // are well-defined names composed of valid identifier characters. Also, note + // that when building nested proto objects, the message name needs to prefix + // the object construction. + String expression = + "Request{\n" + + "auth: Auth{" + + " principal: jwt.iss + '/' + jwt.sub," + + " audiences: [jwt.aud]," + + " presenter: 'azp' in jwt ? jwt.azp : ''," + + " claims: jwt" + + "}," + + "time: now" + + "}"; + // Values for `now` and `jwt` variables to be passed into the runtime + Timestamp now = ProtoTimeUtils.now(); + ImmutableMap jwt = + ImmutableMap.of( + "sub", "serviceAccount:delegate@acme.co", + "aud", "my-project", + "iss", "auth.acme.com:12350", + "extra_claims", ImmutableMap.of("group", "admin")); + AttributeContext.Request expectedMessage = + AttributeContext.Request.newBuilder() + .setTime(now) + .setAuth( + AttributeContext.Auth.newBuilder() + .setPrincipal("auth.acme.com:12350/serviceAccount:delegate@acme.co") + .addAudiences("my-project") + .setClaims( + Struct.newBuilder() + .putAllFields( + ImmutableMap.of( + "sub", newStringValue("serviceAccount:delegate@acme.co"), + "aud", newStringValue("my-project"), + "iss", newStringValue("auth.acme.com:12350"))) + .putFields( + "extra_claims", + Value.newBuilder() + .setStructValue( + Struct.newBuilder() + .putFields("group", newStringValue("admin")) + .build()) + .build()))) + .build(); + + // Compile the `Request` message construction expression and validate that + // the resulting expression type matches the fully qualified message name. + CelAbstractSyntaxTree ast = exercise6.compile(expression); + AttributeContext.Request evaluatedResult = + (AttributeContext.Request) + exercise6.eval( + ast, + ImmutableMap.of( + "now", now, + "jwt", jwt)); + + assertThat(evaluatedResult).isEqualTo(expectedMessage); + } + + private static Value newStringValue(String value) { + return Value.newBuilder().setStringValue(value).build(); + } +} diff --git a/codelab/src/test/codelab/solutions/Exercise7Test.java b/codelab/src/test/codelab/solutions/Exercise7Test.java new file mode 100644 index 000000000..be0fe8af1 --- /dev/null +++ b/codelab/src/test/codelab/solutions/Exercise7Test.java @@ -0,0 +1,60 @@ +// 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. + +package codelab.solutions; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.common.CelAbstractSyntaxTree; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public final class Exercise7Test { + private final Exercise7 exercise7 = new Exercise7(); + + @Test + public void evaluate_checkJwtClaimsWithMacro_evaluatesToTrue() { + String expression = + "jwt.extra_claims.exists(c, c.startsWith('group'))" + + " && jwt.extra_claims" + + ".filter(c, c.startsWith('group'))" + + ".all(c, jwt.extra_claims[c]" + + ".all(g, g.endsWith('@acme.co')))"; + ImmutableMap jwt = + ImmutableMap.of( + "sub", + "serviceAccount:delegate@acme.co", + "aud", + "my-project", + "iss", + "auth.acme.com:12350", + "extra_claims", + ImmutableMap.of("group1", ImmutableList.of("admin@acme.co", "analyst@acme.co")), + "labels", + ImmutableList.of("metadata", "prod", "pii"), + "groupN", + ImmutableList.of("forever@acme.co")); + CelAbstractSyntaxTree ast = exercise7.compile(expression); + + // Evaluate a complex-ish JWT with two groups that satisfy the criteria. + // Output: true. + boolean evaluatedResult = (boolean) exercise7.eval(ast, ImmutableMap.of("jwt", jwt)); + + assertThat(evaluatedResult).isTrue(); + } +} diff --git a/codelab/src/test/codelab/solutions/Exercise8Test.java b/codelab/src/test/codelab/solutions/Exercise8Test.java new file mode 100644 index 000000000..73a0ddc91 --- /dev/null +++ b/codelab/src/test/codelab/solutions/Exercise8Test.java @@ -0,0 +1,147 @@ +// 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. + +package codelab.solutions; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableMap; +import com.google.rpc.context.AttributeContext; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelValidationException; +import dev.cel.common.CelValidationResult; +import dev.cel.parser.CelUnparser; +import dev.cel.parser.CelUnparserFactory; +import dev.cel.runtime.CelEvaluationException; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public final class Exercise8Test { + + private final Exercise8 exercise8 = new Exercise8(); + + @Test + public void validate_invalidTimestampLiteral_returnsError() throws Exception { + CelAbstractSyntaxTree ast = exercise8.compile("timestamp('bad')"); + + CelValidationResult validationResult = exercise8.validate(ast); + + assertThat(validationResult.hasError()).isTrue(); + assertThat(validationResult.getErrorString()) + .isEqualTo( + "ERROR: :1:11: timestamp validation failed. Reason: evaluation error: Text 'bad'" + + " could not be parsed at index 0\n" + + " | timestamp('bad')\n" + + " | ..........^"); + } + + @Test + public void validate_invalidDurationLiteral_returnsError() throws Exception { + CelAbstractSyntaxTree ast = exercise8.compile("duration('bad')"); + + CelValidationResult validationResult = exercise8.validate(ast); + + assertThat(validationResult.hasError()).isTrue(); + assertThat(validationResult.getErrorString()) + .isEqualTo( + "ERROR: :1:10: duration validation failed. Reason: evaluation error: invalid" + + " duration format\n" + + " | duration('bad')\n" + + " | .........^"); + } + + @Test + public void validate_invalidRegexLiteral_returnsError() throws Exception { + CelAbstractSyntaxTree ast = exercise8.compile("'text'.matches('**')"); + + CelValidationResult validationResult = exercise8.validate(ast); + + assertThat(validationResult.hasError()).isTrue(); + assertThat(validationResult.getErrorString()) + .isEqualTo( + "ERROR: :1:16: Regex validation failed. Reason: Dangling meta character '*' near" + + " index 0\n" + + "**\n" + + "^\n" + + " | 'text'.matches('**')\n" + + " | ...............^"); + } + + @Test + public void validate_listHasMixedLiterals_throws() throws Exception { + CelAbstractSyntaxTree ast = exercise8.compile("3 in [1, 2, '3']"); + + // Note that `CelValidationResult` is the same result class used for the compilation path. This + // means you could alternatively invoke `.getAst()` and handle `CelValidationException` as + // usual. + CelValidationResult validationResult = exercise8.validate(ast); + + CelValidationException e = assertThrows(CelValidationException.class, validationResult::getAst); + assertThat(e) + .hasMessageThat() + .contains( + "ERROR: :1:13: expected type 'int' but found 'string'\n" + + " | 3 in [1, 2, '3']\n" + + " | ............^"); + } + + @Test + public void optimize_constantFold_success() throws Exception { + CelUnparser celUnparser = CelUnparserFactory.newUnparser(); + CelAbstractSyntaxTree ast = exercise8.compile("(1 + 2 + 3 == x) && (x in [1, 2, x])"); + + CelAbstractSyntaxTree optimizedAst = exercise8.optimize(ast); + + assertThat(celUnparser.unparse(optimizedAst)).isEqualTo("6 == x"); + } + + @Test + public void optimize_constantFold_evaluateError() throws Exception { + CelAbstractSyntaxTree ast = + exercise8.compile("request.headers.referer == 'https://' + 'cel.dev'"); + CelAbstractSyntaxTree optimizedAst = exercise8.optimize(ast); + ImmutableMap runtimeParameters = + ImmutableMap.of("request", AttributeContext.Request.getDefaultInstance()); + + CelEvaluationException e1 = + assertThrows(CelEvaluationException.class, () -> exercise8.eval(ast, runtimeParameters)); + CelEvaluationException e2 = + assertThrows( + CelEvaluationException.class, () -> exercise8.eval(optimizedAst, runtimeParameters)); + // Note that the errors below differ by their source position. + assertThat(e1) + .hasMessageThat() + .contains("evaluation error at :15: key 'referer' is not present in map."); + assertThat(e2) + .hasMessageThat() + .contains("evaluation error: key 'referer' is not present in map."); + } + + @Test + public void optimize_commonSubexpressionElimination_success() throws Exception { + CelUnparser celUnparser = CelUnparserFactory.newUnparser(); + CelAbstractSyntaxTree ast = + exercise8.compile( + "request.auth.claims.group == 'admin' || request.auth.claims.group == 'user'"); + + CelAbstractSyntaxTree optimizedAst = exercise8.optimize(ast); + + assertThat(celUnparser.unparse(optimizedAst)) + .isEqualTo( + "cel.@block([request.auth.claims.group], @index0 == \"admin\" || @index0 == \"user\")"); + } +} diff --git a/codelab/src/test/codelab/solutions/Exercise9Test.java b/codelab/src/test/codelab/solutions/Exercise9Test.java new file mode 100644 index 000000000..622df45ce --- /dev/null +++ b/codelab/src/test/codelab/solutions/Exercise9Test.java @@ -0,0 +1,95 @@ +// 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. + +package codelab.solutions; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.rpc.context.AttributeContext; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelValidationException; +import dev.cel.common.CelValidationResult; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public final class Exercise9Test { + private final Exercise9 exercise9 = new Exercise9(); + + @Test + public void validate_invalidHttpMethod_returnsError() throws Exception { + String expression = + "google.rpc.context.AttributeContext.Request { \n" + + "scheme: 'http', " + + "method: 'GETTT', " // method is misspelled. + + "host: 'cel.dev' \n" + + "}"; + CelAbstractSyntaxTree ast = exercise9.compile(expression); + + CelValidationResult validationResult = exercise9.validate(ast); + + assertThat(validationResult.hasError()).isTrue(); + assertThat(validationResult.getErrorString()) + .isEqualTo( + "ERROR: :2:25: GETTT is not an allowed HTTP method.\n" + + " | scheme: 'http', method: 'GETTT', host: 'cel.dev' \n" + + " | ........................^"); + assertThrows(CelValidationException.class, validationResult::getAst); + } + + @Test + public void validate_schemeIsHttp_returnsWarning() throws Exception { + String expression = + "google.rpc.context.AttributeContext.Request { \n" + + "scheme: 'http', " // https is preferred but not required. + + "method: 'GET', " + + "host: 'cel.dev' \n" + + "}"; + CelAbstractSyntaxTree ast = exercise9.compile(expression); + + CelValidationResult validationResult = exercise9.validate(ast); + + assertThat(validationResult.hasError()).isFalse(); + assertThat(validationResult.getIssueString()) + .isEqualTo( + "WARNING: :2:9: Prefer using https for safety.\n" + + " | scheme: 'http', method: 'GET', host: 'cel.dev' \n" + + " | ........^"); + // Because the validation result does not contain any errors, you can still evaluate it. + assertThat(exercise9.eval(validationResult.getAst())) + .isEqualTo( + AttributeContext.Request.newBuilder() + .setScheme("http") + .setMethod("GET") + .setHost("cel.dev") + .build()); + } + + @Test + public void validate_isPrimeNumberWithinMacro_returnsError() throws Exception { + String expression = "[2,3,5].all(x, is_prime_number(x))"; + CelAbstractSyntaxTree ast = exercise9.compile(expression); + + CelValidationResult validationResult = exercise9.validate(ast); + + assertThat(validationResult.hasError()).isTrue(); + assertThat(validationResult.getErrorString()) + .isEqualTo( + "ERROR: :1:12: is_prime_number function cannot be used within CEL macros.\n" + + " | [2,3,5].all(x, is_prime_number(x))\n" + + " | ...........^"); + } +} diff --git a/common/BUILD.bazel b/common/BUILD.bazel index 032a61150..4e0d7485c 100644 --- a/common/BUILD.bazel +++ b/common/BUILD.bazel @@ -1,13 +1,11 @@ +load("@rules_java//java:defs.bzl", "java_library") +load("//:cel_android_rules.bzl", "cel_android_library") + package( default_applicable_licenses = ["//:license"], default_visibility = ["//visibility:public"], ) -java_library( - name = "common", - exports = ["//common/src/main/java/dev/cel/common"], -) - java_library( name = "compiler_common", exports = ["//common/src/main/java/dev/cel/common:compiler_common"], @@ -15,12 +13,13 @@ java_library( java_library( name = "options", + # used_by_android exports = ["//common/src/main/java/dev/cel/common:options"], ) java_library( - name = "features", - exports = ["//common/src/main/java/dev/cel/common:features"], + name = "container", + exports = ["//common/src/main/java/dev/cel/common:container"], ) java_library( @@ -28,19 +27,103 @@ java_library( exports = ["//common/src/main/java/dev/cel/common:proto_ast"], ) +cel_android_library( + name = "proto_ast_android", + exports = ["//common/src/main/java/dev/cel/common:proto_ast_android"], +) + java_library( name = "proto_v1alpha1_ast", - visibility = ["//visibility:public"], exports = ["//common/src/main/java/dev/cel/common:proto_v1alpha1_ast"], ) java_library( name = "error_codes", + # used_by_android exports = ["//common/src/main/java/dev/cel/common:error_codes"], ) java_library( - name = "runtime_exception", - visibility = ["//visibility:public"], - exports = ["//common/src/main/java/dev/cel/common:runtime_exception"], + name = "mutable_ast", + exports = ["//common/src/main/java/dev/cel/common:mutable_ast"], +) + +java_library( + name = "mutable_source", + exports = ["//common/src/main/java/dev/cel/common:mutable_source"], +) + +java_library( + name = "proto_json_adapter", + exports = ["//common/src/main/java/dev/cel/common:proto_json_adapter"], +) + +java_library( + name = "source", + visibility = ["//:internal"], + exports = ["//common/src/main/java/dev/cel/common:source"], +) + +java_library( + name = "source_location", + exports = ["//common/src/main/java/dev/cel/common:source_location"], +) + +java_library( + name = "cel_source", + exports = ["//common/src/main/java/dev/cel/common:cel_source"], +) + +cel_android_library( + name = "cel_source_android", + exports = ["//common/src/main/java/dev/cel/common:cel_source_android"], +) + +java_library( + name = "cel_source_helper", + visibility = ["//:internal"], + exports = ["//common/src/main/java/dev/cel/common:cel_source_helper"], +) + +java_library( + name = "cel_ast", + exports = ["//common/src/main/java/dev/cel/common:cel_ast"], +) + +cel_android_library( + name = "cel_ast_android", + exports = [ + "//common/src/main/java/dev/cel/common:cel_ast_android", + ], +) + +java_library( + name = "cel_exception", + # used_by_android + exports = ["//common/src/main/java/dev/cel/common:cel_exception"], +) + +java_library( + name = "cel_descriptors", + visibility = ["//:internal"], + exports = ["//common/src/main/java/dev/cel/common:cel_descriptors"], +) + +java_library( + name = "cel_descriptor_util", + visibility = [ + "//:internal", + # TODO: Remove references to the following clients + ], + exports = ["//common/src/main/java/dev/cel/common:cel_descriptor_util"], +) + +java_library( + name = "operator", + exports = ["//common/src/main/java/dev/cel/common:operator"], +) + +cel_android_library( + name = "operator_android", + exports = ["//common/src/main/java/dev/cel/common:operator_android"], ) diff --git a/common/annotations/BUILD.bazel b/common/annotations/BUILD.bazel index 62a6ccfd9..e8ba25e52 100644 --- a/common/annotations/BUILD.bazel +++ b/common/annotations/BUILD.bazel @@ -1,3 +1,5 @@ +load("@rules_java//java:defs.bzl", "java_library") + package( default_applicable_licenses = ["//:license"], default_visibility = ["//visibility:public"], @@ -5,5 +7,6 @@ package( java_library( name = "annotations", + # used_by_android exports = ["//common/src/main/java/dev/cel/common/annotations"], ) diff --git a/common/ast/BUILD.bazel b/common/ast/BUILD.bazel index 91097daf4..276db0322 100644 --- a/common/ast/BUILD.bazel +++ b/common/ast/BUILD.bazel @@ -1,3 +1,6 @@ +load("@rules_java//java:defs.bzl", "java_library") +load("//:cel_android_rules.bzl", "cel_android_library") + package( default_applicable_licenses = ["//:license"], default_visibility = ["//visibility:public"], @@ -8,11 +11,21 @@ java_library( exports = ["//common/src/main/java/dev/cel/common/ast"], ) +cel_android_library( + name = "ast_android", + exports = ["//common/src/main/java/dev/cel/common/ast:ast_android"], +) + java_library( name = "expr_converter", exports = ["//common/src/main/java/dev/cel/common/ast:expr_converter"], ) +cel_android_library( + name = "expr_converter_android", + exports = ["//common/src/main/java/dev/cel/common/ast:expr_converter_android"], +) + java_library( name = "expr_v1alpha1_converter", exports = ["//common/src/main/java/dev/cel/common/ast:expr_v1alpha1_converter"], @@ -29,6 +42,6 @@ java_library( ) java_library( - name = "expr_util", - exports = ["//common/src/main/java/dev/cel/common/ast:expr_util"], + name = "mutable_expr", + exports = ["//common/src/main/java/dev/cel/common/ast:mutable_expr"], ) diff --git a/common/exceptions/BUILD.bazel b/common/exceptions/BUILD.bazel new file mode 100644 index 000000000..3464632d9 --- /dev/null +++ b/common/exceptions/BUILD.bazel @@ -0,0 +1,66 @@ +load("@rules_java//java:defs.bzl", "java_library") + +package( + default_applicable_licenses = ["//:license"], + default_visibility = ["//:internal"], +) + +java_library( + name = "runtime_exception", + # used_by_android + exports = ["//common/src/main/java/dev/cel/common/exceptions:runtime_exception"], +) + +java_library( + name = "attribute_not_found", + # used_by_android + exports = ["//common/src/main/java/dev/cel/common/exceptions:attribute_not_found"], +) + +java_library( + name = "divide_by_zero", + # used_by_android + exports = ["//common/src/main/java/dev/cel/common/exceptions:divide_by_zero"], +) + +java_library( + name = "index_out_of_bounds", + # used_by_android + exports = ["//common/src/main/java/dev/cel/common/exceptions:index_out_of_bounds"], +) + +java_library( + name = "bad_format", + # used_by_android + exports = ["//common/src/main/java/dev/cel/common/exceptions:bad_format"], +) + +java_library( + name = "numeric_overflow", + # used_by_android + exports = ["//common/src/main/java/dev/cel/common/exceptions:numeric_overflow"], +) + +java_library( + name = "invalid_argument", + # used_by_android + exports = ["//common/src/main/java/dev/cel/common/exceptions:invalid_argument"], +) + +java_library( + name = "iteration_budget_exceeded", + # used_by_android + exports = ["//common/src/main/java/dev/cel/common/exceptions:iteration_budget_exceeded"], +) + +java_library( + name = "duplicate_key", + # used_by_android + exports = ["//common/src/main/java/dev/cel/common/exceptions:duplicate_key"], +) + +java_library( + name = "overload_not_found", + # used_by_android + exports = ["//common/src/main/java/dev/cel/common/exceptions:overload_not_found"], +) diff --git a/common/formats/BUILD.bazel b/common/formats/BUILD.bazel new file mode 100644 index 000000000..feed7ce42 --- /dev/null +++ b/common/formats/BUILD.bazel @@ -0,0 +1,39 @@ +load("@rules_java//java:defs.bzl", "java_library") + +package( + default_applicable_licenses = ["//:license"], + default_visibility = ["//:internal"], +) + +java_library( + name = "yaml_helper", + visibility = [ + "//:internal", + "//java/com/google/cloud/security/green/rundetector/custommodules/detectors:__pkg__", + ], + exports = ["//common/src/main/java/dev/cel/common/formats:yaml_helper"], +) + +java_library( + name = "value_string", + visibility = [ + "//:internal", + "//java/com/google/cloud/security/green/rundetector/custommodules/detectors:__pkg__", + ], + exports = ["//common/src/main/java/dev/cel/common/formats:value_string"], +) + +java_library( + name = "parser_context", + exports = ["//common/src/main/java/dev/cel/common/formats:parser_context"], +) + +java_library( + name = "yaml_parser_context_impl", + exports = ["//common/src/main/java/dev/cel/common/formats:yaml_parser_context_impl"], +) + +java_library( + name = "file_source", + exports = ["//common/src/main/java/dev/cel/common/formats:file_source"], +) diff --git a/common/internal/BUILD.bazel b/common/internal/BUILD.bazel index 6293804f2..7c33e56b9 100644 --- a/common/internal/BUILD.bazel +++ b/common/internal/BUILD.bazel @@ -1,6 +1,9 @@ +load("@rules_java//java:defs.bzl", "java_library") +load("//:cel_android_rules.bzl", "cel_android_library") + package( default_applicable_licenses = ["//:license"], - default_visibility = ["//visibility:public"], + default_visibility = ["//:internal"], ) java_library( @@ -8,13 +11,24 @@ java_library( exports = ["//common/src/main/java/dev/cel/common/internal"], ) +java_library( + name = "code_point_stream", + exports = ["//common/src/main/java/dev/cel/common/internal:code_point_stream"], +) + java_library( name = "comparison_functions", exports = ["//common/src/main/java/dev/cel/common/internal:comparison_functions"], ) +cel_android_library( + name = "comparison_functions_android", + exports = ["//common/src/main/java/dev/cel/common/internal:comparison_functions_android"], +) + java_library( name = "converter", + # used_by_android exports = ["//common/src/main/java/dev/cel/common/internal:converter"], ) @@ -23,6 +37,11 @@ java_library( exports = ["//common/src/main/java/dev/cel/common/internal:dynamic_proto"], ) +java_library( + name = "proto_lite_adapter", + exports = ["//common/src/main/java/dev/cel/common/internal:proto_lite_adapter"], +) + java_library( name = "proto_equality", exports = ["//common/src/main/java/dev/cel/common/internal:proto_equality"], @@ -48,11 +67,21 @@ java_library( exports = ["//common/src/main/java/dev/cel/common/internal:default_instance_message_factory"], ) +java_library( + name = "default_instance_message_lite_factory", + exports = ["//common/src/main/java/dev/cel/common/internal:default_instance_message_lite_factory"], +) + java_library( name = "well_known_proto", exports = ["//common/src/main/java/dev/cel/common/internal:well_known_proto"], ) +cel_android_library( + name = "well_known_proto_android", + exports = ["//common/src/main/java/dev/cel/common/internal:well_known_proto_android"], +) + java_library( name = "proto_message_factory", exports = ["//common/src/main/java/dev/cel/common/internal:proto_message_factory"], @@ -68,7 +97,58 @@ java_library( exports = ["//common/src/main/java/dev/cel/common/internal:cel_descriptor_pools"], ) +java_library( + name = "cel_lite_descriptor_pool", + exports = ["//common/src/main/java/dev/cel/common/internal:cel_lite_descriptor_pool"], +) + +cel_android_library( + name = "cel_lite_descriptor_pool_android", + exports = ["//common/src/main/java/dev/cel/common/internal:cel_lite_descriptor_pool_android"], +) + +java_library( + name = "default_lite_descriptor_pool", + exports = ["//common/src/main/java/dev/cel/common/internal:default_lite_descriptor_pool"], +) + +cel_android_library( + name = "default_lite_descriptor_pool_android", + exports = ["//common/src/main/java/dev/cel/common/internal:default_lite_descriptor_pool_android"], +) + java_library( name = "safe_string_formatter", + # used_by_android exports = ["//common/src/main/java/dev/cel/common/internal:safe_string_formatter"], ) + +cel_android_library( + name = "internal_android", + exports = ["//common/src/main/java/dev/cel/common/internal:internal_android"], +) + +java_library( + name = "proto_time_utils", + exports = ["//common/src/main/java/dev/cel/common/internal:proto_time_utils"], +) + +cel_android_library( + name = "proto_time_utils_android", + exports = ["//common/src/main/java/dev/cel/common/internal:proto_time_utils_android"], +) + +java_library( + name = "date_time_helpers", + exports = ["//common/src/main/java/dev/cel/common/internal:date_time_helpers"], +) + +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 f4e757ad5..1dba25b8e 100644 --- a/common/navigation/BUILD.bazel +++ b/common/navigation/BUILD.bazel @@ -1,9 +1,21 @@ +load("@rules_java//java:defs.bzl", "java_library") + package( default_applicable_licenses = ["//:license"], default_visibility = ["//visibility:public"], ) +java_library( + name = "common", + exports = ["//common/src/main/java/dev/cel/common/navigation:common"], +) + java_library( name = "navigation", exports = ["//common/src/main/java/dev/cel/common/navigation"], ) + +java_library( + name = "mutable_navigation", + exports = ["//common/src/main/java/dev/cel/common/navigation:mutable_navigation"], +) diff --git a/common/resources/testdata/proto2/BUILD.bazel b/common/resources/testdata/proto2/BUILD.bazel deleted file mode 100644 index a4afac550..000000000 --- a/common/resources/testdata/proto2/BUILD.bazel +++ /dev/null @@ -1,20 +0,0 @@ -package( - default_applicable_licenses = ["//:license"], - default_testonly = True, - default_visibility = ["//visibility:public"], -) - -alias( - name = "test_all_types_java_proto", - actual = "//common/src/main/resources/testdata/proto2:test_all_types_java_proto", -) - -alias( - name = "messages_proto2_java_proto", - actual = "//common/src/main/resources/testdata/proto2:messages_proto2_java_proto", -) - -alias( - name = "messages_extensions_proto2_java_proto", - actual = "//common/src/main/resources/testdata/proto2:messages_extensions_proto2_java_proto", -) diff --git a/common/resources/testdata/proto3/BUILD.bazel b/common/resources/testdata/proto3/BUILD.bazel index f094fbdb6..76a097d03 100644 --- a/common/resources/testdata/proto3/BUILD.bazel +++ b/common/resources/testdata/proto3/BUILD.bazel @@ -1,12 +1,12 @@ package( default_applicable_licenses = ["//:license"], default_testonly = True, - default_visibility = ["//visibility:public"], + default_visibility = ["//:internal"], ) alias( - name = "test_all_types_java_proto", - actual = "//common/src/main/resources/testdata/proto3:test_all_types_java_proto", + name = "test_all_types_file_descriptor_set", + actual = "//common/src/main/resources/testdata/proto3:test_all_types_file_descriptor_set", ) alias( diff --git a/common/src/main/java/dev/cel/common/BUILD.bazel b/common/src/main/java/dev/cel/common/BUILD.bazel index 31b593708..38548744c 100644 --- a/common/src/main/java/dev/cel/common/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/BUILD.bazel @@ -1,3 +1,6 @@ +load("@rules_java//java:defs.bzl", "java_library") +load("//:cel_android_rules.bzl", "cel_android_library") + package( default_applicable_licenses = ["//:license"], default_visibility = [ @@ -6,17 +9,6 @@ package( ], ) -# keep sorted -COMMON_SOURCES = [ - "CelAbstractSyntaxTree.java", - "CelDescriptorUtil.java", - "CelDescriptors.java", - "CelException.java", - "CelProtoAbstractSyntaxTree.java", # TODO Split target after migrating callers - "CelSource.java", - "CelSourceLocation.java", -] - # keep sorted COMPILER_COMMON_SOURCES = [ "CelFunctionDecl.java", @@ -27,28 +19,47 @@ COMPILER_COMMON_SOURCES = [ "CelVarDecl.java", ] +# keep sorted +SOURCE_SOURCES = [ + "Source.java", +] + +# keep sorted +PROTO_AST_SOURCE = [ + "CelProtoAbstractSyntaxTree.java", +] + # keep sorted PROTO_V1ALPHA1_AST_SOURCE = [ "CelProtoV1Alpha1AbstractSyntaxTree.java", ] java_library( - name = "common", - srcs = COMMON_SOURCES, + name = "cel_descriptor_util", + srcs = [ + "CelDescriptorUtil.java", + ], tags = [ ], deps = [ - ":error_codes", - "//:auto_value", + ":cel_descriptors", "//common/annotations", - "//common/ast", - "//common/ast:expr_converter", - "//common/internal", "//common/internal:file_descriptor_converter", - "//common/types", "//common/types:cel_types", - "//common/types:type_providers", - "@cel_spec//proto/cel/expr:expr_java_proto", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + ], +) + +java_library( + name = "cel_descriptors", + srcs = [ + "CelDescriptors.java", + ], + tags = [ + ], + deps = [ + "//:auto_value", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", @@ -61,45 +72,79 @@ java_library( tags = [ ], deps = [ - ":common", + ":cel_ast", + ":cel_exception", + ":cel_source", + ":source", + ":source_location", "//:auto_value", "//common/annotations", "//common/internal:safe_string_formatter", - "//common/types:cel_types", + "//common/types:cel_proto_types", "//common/types:type_providers", - "@cel_spec//proto/cel/expr:expr_java_proto", + "@cel_spec//proto/cel/expr:checked_java_proto", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:org_jspecify_jspecify", ], ) +java_library( + name = "cel_exception", + srcs = ["CelException.java"], + # used_by_android + tags = [ + ], + deps = [ + ":error_codes", + ], +) + java_library( name = "options", srcs = ["CelOptions.java"], + # used_by_android tags = [ ], deps = [ - ":features", "//:auto_value", + "//common/annotations", "@maven//:com_google_errorprone_error_prone_annotations", - "@maven//:com_google_guava_guava", ], ) java_library( - name = "features", - srcs = ["ExprFeatures.java"], + name = "proto_ast", + srcs = PROTO_AST_SOURCE, tags = [ ], deps = [ + ":cel_ast", + ":cel_source", + "//common/ast:expr_converter", + "//common/types:cel_proto_types", + "@cel_spec//proto/cel/expr:checked_java_proto", + "@cel_spec//proto/cel/expr:syntax_java_proto", + "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", ], ) -java_library( - name = "proto_ast", - exports = [":common"], # TODO Split target after migrating callers +cel_android_library( + name = "proto_ast_android", + srcs = PROTO_AST_SOURCE, + tags = [ + ], + deps = [ + ":cel_ast_android", + ":cel_source_android", + "//common/ast:expr_converter_android", + "//common/types:cel_proto_types_android", + "@cel_spec//proto/cel/expr:checked_java_proto_lite", + "@cel_spec//proto/cel/expr:syntax_java_proto_lite", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", + ], ) java_library( @@ -108,7 +153,8 @@ java_library( tags = [ ], deps = [ - ":common", + ":cel_ast", + ":cel_source", "//common/ast:expr_v1alpha1_converter", "//common/types:cel_v1alpha1_types", "@com_google_googleapis//google/api/expr/v1alpha1:expr_java_proto", @@ -120,17 +166,217 @@ java_library( java_library( name = "error_codes", srcs = ["CelErrorCode.java"], + # used_by_android tags = [ ], ) java_library( - name = "runtime_exception", - srcs = ["CelRuntimeException.java"], + name = "mutable_ast", + srcs = ["CelMutableAst.java"], tags = [ ], deps = [ - ":error_codes", + ":cel_ast", + ":mutable_source", + "//common/ast", + "//common/ast:mutable_expr", + "//common/types:type_providers", + ], +) + +java_library( + name = "mutable_source", + srcs = ["CelMutableSource.java"], + tags = [ + ], + deps = [ + ":cel_source", + "//common/ast:mutable_expr", + "//common/internal", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "proto_json_adapter", + srcs = ["CelProtoJsonAdapter.java"], + tags = [ + ], + deps = [ + "//common/internal:date_time_helpers", + "//common/internal:proto_time_utils", + "//common/values", + "//common/values:cel_byte_string", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + ], +) + +java_library( + name = "source_location", + srcs = ["CelSourceLocation.java"], + tags = [ + ], + deps = [ + "//:auto_value", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "cel_source", + srcs = ["CelSource.java"], + tags = [ + ], + deps = [ + ":cel_source_helper", + ":source", + ":source_location", + "//:auto_value", + "//common/ast", + "//common/internal", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "cel_source_android", + srcs = ["CelSource.java"], + tags = [ + ], + deps = [ + ":cel_source_helper_android", + ":source_android", + ":source_location_android", + "//:auto_value", + "//common/ast:ast_android", + "//common/internal:internal_android", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", + ], +) + +java_library( + name = "cel_source_helper", + srcs = ["CelSourceHelper.java"], + tags = [ + ], + deps = [ + ":source_location", + "//common/annotations", + "//common/internal", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "cel_source_helper_android", + srcs = ["CelSourceHelper.java"], + deps = [ + ":source_location_android", + "//common/annotations", + "//common/internal:internal_android", + "@maven_android//:com_google_guava_guava", + ], +) + +java_library( + name = "cel_ast", + srcs = ["CelAbstractSyntaxTree.java"], + tags = [ + ], + deps = [ + ":cel_source", + "//:auto_value", + "//common/annotations", + "//common/ast", + "//common/types", + "//common/types:type_providers", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "cel_ast_android", + srcs = ["CelAbstractSyntaxTree.java"], + tags = [ + ], + deps = [ + ":cel_source_android", + "//:auto_value", "//common/annotations", + "//common/ast:ast_android", + "//common/types:type_providers_android", + "//common/types:types_android", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", + ], +) + +java_library( + name = "source", + srcs = SOURCE_SOURCES, + tags = [ + ], + deps = [ + "//common/annotations", + "//common/internal", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "source_android", + srcs = SOURCE_SOURCES, + visibility = ["//visibility:private"], + deps = [ + "//common/annotations", + "//common/internal:internal_android", + "@maven_android//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "source_location_android", + srcs = ["CelSourceLocation.java"], + visibility = ["//visibility:private"], + deps = [ + "//:auto_value", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", + ], +) + +java_library( + name = "container", + srcs = ["CelContainer.java"], + tags = [ + ], + deps = [ + "//:auto_value", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "operator", + srcs = ["Operator.java"], + tags = [ + ], + deps = ["@maven//:com_google_guava_guava"], +) + +cel_android_library( + name = "operator_android", + srcs = ["Operator.java"], + tags = [ ], + deps = ["@maven_android//:com_google_guava_guava"], ) diff --git a/common/src/main/java/dev/cel/common/CelAbstractSyntaxTree.java b/common/src/main/java/dev/cel/common/CelAbstractSyntaxTree.java index 3d55bde38..b79c67e79 100644 --- a/common/src/main/java/dev/cel/common/CelAbstractSyntaxTree.java +++ b/common/src/main/java/dev/cel/common/CelAbstractSyntaxTree.java @@ -14,7 +14,7 @@ package dev.cel.common; -import dev.cel.expr.Type; +import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.errorprone.annotations.Immutable; @@ -23,7 +23,6 @@ import dev.cel.common.ast.CelExpr; import dev.cel.common.ast.CelReference; import dev.cel.common.types.CelType; -import dev.cel.common.types.CelTypes; import dev.cel.common.types.SimpleType; import java.util.Map; import java.util.NoSuchElementException; @@ -36,16 +35,17 @@ *

Note: Use {@link CelProtoAbstractSyntaxTree} if you need access to the protobuf equivalent * ASTs, such as ParsedExpr and CheckedExpr from syntax.proto or checked.proto. */ +@AutoValue @Immutable -public final class CelAbstractSyntaxTree { +public abstract class CelAbstractSyntaxTree { - private final CelSource celSource; + abstract CelSource celSource(); - private final CelExpr celExpr; + abstract CelExpr celExpr(); - private final ImmutableMap references; + abstract ImmutableMap references(); - private final ImmutableMap types; + abstract ImmutableMap types(); /** * Constructs a new instance of CelAbstractSyntaxTree that represent a parsed expression. @@ -54,7 +54,8 @@ public final class CelAbstractSyntaxTree { * validating or optimizing an AST. */ public static CelAbstractSyntaxTree newParsedAst(CelExpr celExpr, CelSource celSource) { - return new CelAbstractSyntaxTree(celExpr, celSource); + return new AutoValue_CelAbstractSyntaxTree( + celSource, celExpr, ImmutableMap.of(), ImmutableMap.of()); } /** @@ -69,32 +70,18 @@ public static CelAbstractSyntaxTree newCheckedAst( CelSource celSource, Map references, Map types) { - return new CelAbstractSyntaxTree(celExpr, celSource, references, types); - } - - private CelAbstractSyntaxTree(CelExpr celExpr, CelSource celSource) { - this(celExpr, celSource, ImmutableMap.of(), ImmutableMap.of()); - } - - private CelAbstractSyntaxTree( - CelExpr celExpr, - CelSource celSource, - Map references, - Map types) { - this.celExpr = celExpr; - this.celSource = celSource; - this.references = ImmutableMap.copyOf(references); - this.types = ImmutableMap.copyOf(types); + return new AutoValue_CelAbstractSyntaxTree( + celSource, celExpr, ImmutableMap.copyOf(references), ImmutableMap.copyOf(types)); } /** Returns the underlying {@link CelExpr} representation of the abstract syntax tree. */ public CelExpr getExpr() { - return celExpr; + return celExpr(); } /** Tests whether the underlying abstract syntax tree has been type checked or not. */ public boolean isChecked() { - return !types.isEmpty(); + return !types().isEmpty(); } /** @@ -105,35 +92,32 @@ public CelType getResultType() { return isChecked() ? getType(getExpr().id()).get() : SimpleType.DYN; } - /** - * For a type checked abstract syntax tree the resulting type is returned in proto format - * described in checked.proto. Otherwise, the dynamic type is returned. - */ - public Type getProtoResultType() { - return CelTypes.celTypeToType(getResultType()); - } - /** * Returns the {@link CelSource} that was used during construction of the abstract syntax tree. */ public CelSource getSource() { - return celSource; + return celSource(); } public Optional getType(long exprId) { - return Optional.ofNullable(types.get(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; + return types(); } public Optional getReference(long exprId) { - return Optional.ofNullable(references.get(exprId)); + return Optional.ofNullable(references().get(exprId)); } public ImmutableMap getReferenceMap() { - return references; + return references(); } public CelReference getReferenceOrThrow(long exprId) { @@ -142,12 +126,12 @@ public CelReference getReferenceOrThrow(long exprId) { } Optional findEnumValue(long exprId) { - CelReference ref = references.get(exprId); + CelReference ref = references().get(exprId); return ref != null ? ref.value() : Optional.empty(); } Optional> findOverloadIDs(long exprId) { - CelReference ref = references.get(exprId); + CelReference ref = references().get(exprId); return ref != null && !ref.value().isPresent() ? Optional.of(ref.overloadIds()) : Optional.empty(); diff --git a/common/src/main/java/dev/cel/common/CelContainer.java b/common/src/main/java/dev/cel/common/CelContainer.java new file mode 100644 index 000000000..0ca566fac --- /dev/null +++ b/common/src/main/java/dev/cel/common/CelContainer.java @@ -0,0 +1,349 @@ +// 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; + +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.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.CheckReturnValue; +import com.google.errorprone.annotations.Immutable; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; + +/** CelContainer holds a reference to an optional qualified container name and set of aliases. */ +@AutoValue +@Immutable +public abstract class CelContainer { + + public abstract String name(); + + abstract ImmutableMap aliasMap(); + + /** + * Returns the aliases configured in the container. + * + *

The key of the map is the alias and the value is the corresponding fully qualified name. + */ + public ImmutableMap aliases() { + return aliasMap().entrySet().stream() + .filter(e -> e.getValue().kind().equals(AliasKind.ALIAS)) + .collect(toImmutableMap(Map.Entry::getKey, e -> e.getValue().qualifiedName())); + } + + public ImmutableList abbreviations() { + return aliasMap().entrySet().stream() + .filter(e -> e.getValue().kind().equals(AliasKind.ABBREVIATION)) + .map(e -> e.getValue().qualifiedName()) + .collect(toImmutableList()); + } + + /** Builder for {@link CelContainer} */ + @AutoValue.Builder + public abstract static class Builder { + + private final LinkedHashMap aliasMap = new LinkedHashMap<>(); + + abstract String name(); + + /** Sets the fully-qualified name of the container. */ + public abstract Builder setName(String name); + + abstract Builder setAliasMap(ImmutableMap aliasMap); + + /** See {@link #addAbbreviations(ImmutableSet)} for documentation. */ + @CanIgnoreReturnValue + public Builder addAbbreviations(String... qualifiedNames) { + Preconditions.checkNotNull(qualifiedNames); + return addAbbreviations(ImmutableSet.copyOf(qualifiedNames)); + } + + /** + * Configures a set of simple names as abbreviations for fully-qualified names. + * + *

An abbreviation is a simple name that expands to a fully-qualified name. Abbreviations can + * be useful when working with variables, functions, and especially types from multiple + * namespaces: + * + *

{@code
+     * // CEL object construction
+     * qual.pkg.version.ObjTypeName{
+     *   field: alt.container.ver.FieldTypeName{value: ...}
+     * }
+     * }
+ * + *

Only one the qualified names above may be used as the CEL container, so at least one of + * these references must be a long qualified name within an otherwise short CEL program. Using + * the following abbreviations, the program becomes much simpler: + * + *

{@code
+     * // CEL Java option
+     * CelContainer.newBuilder().addAbbreviations("qual.pkg.version.ObjTypeName", "alt.container.ver.FieldTypeName").build()
+     * }
+     * {@code
+     * // Simplified Object construction
+     * ObjTypeName{field: FieldTypeName{value: ...}}
+     * }
+ * + *

There are a few rules for the qualified names and the simple abbreviations generated from + * them: + * + *

    + *
  • Qualified names must be dot-delimited, e.g. `package.subpkg.name`. + *
  • The last element in the qualified name is the abbreviation. + *
  • Abbreviations must not collide with each other. + *
  • The abbreviation must not collide with unqualified names in use. + *
+ * + *

Abbreviations are distinct from container-based references in the following important + * ways: + * + *

    + *
  • Abbreviations must expand to a fully-qualified name. + *
  • Expanded abbreviations do not participate in namespace resolution. + *
  • Abbreviation expansion is done instead of the container search for a matching + * identifier. + *
  • Containers follow C++ namespace resolution rules with searches from the most qualified + * name to the least qualified name. + *
  • Container references within the CEL program may be relative, and are resolved to fully + * qualified names at either type-check time or program plan time, whichever comes first. + *
+ * + *

If there is ever a case where an identifier could be in both the container and as an + * abbreviation, the abbreviation wins as this will ensure that the meaning of a program is + * preserved between compilations even as the container evolves. + * + * @throws IllegalArgumentException If qualifiedName is invalid per above specification. + */ + @CanIgnoreReturnValue + public Builder addAbbreviations(ImmutableSet qualifiedNames) { + for (String qualifiedName : qualifiedNames) { + qualifiedName = qualifiedName.trim(); + for (int i = 0; i < qualifiedName.length(); i++) { + if (!isIdentifierChar(qualifiedName.charAt(i))) { + throw new IllegalArgumentException( + String.format( + "invalid qualified name: %s, wanted name of the form 'qualified.name'", + qualifiedName)); + } + } + + int index = qualifiedName.lastIndexOf("."); + if (index <= 0 || index >= qualifiedName.length() - 1) { + throw new IllegalArgumentException( + String.format( + "invalid qualified name: %s, wanted name of the form 'qualified.name'", + qualifiedName)); + } + + String alias = qualifiedName.substring(index + 1); + aliasAs(AliasKind.ABBREVIATION, qualifiedName, alias); + } + + return this; + } + + /** + * Alias associates a fully-qualified name with a user-defined alias. + * + *

In general, {@link #addAbbreviations} is preferred to aliasing since the names generated + * from the Abbrevs option are more easily traced back to source code. Aliasing is useful for + * propagating alias configuration from one container instance to another, and may also be + * useful for remapping poorly chosen protobuf message / package names. + * + *

Note: all the rules that apply to abbreviations also apply to aliasing. + * + *

Note: It is also possible to alias a top-level package or a name that does not contain a + * period. When resolving an identifier, CEL checks for variables and functions before + * attempting to expand aliases for type resolution. Therefore, if an expression consists solely + * of an identifier that matches both an alias and a declared variable (e.g., {@code + * short_alias}), the variable will take precedence and the compilation will succeed. The alias + * expansion will only be used when the alias is a prefix to a longer name (e.g., {@code + * short_alias.TestRequest}) or if no variable with the same name exists, in which case using + * the alias as a standalone identifier will likely result in a compilation error. + * + * @param alias Simple name to be expanded. Must be a valid identifier. + * @param qualifiedName The fully qualified name to expand to. This may be a simple name (e.g. a + * package name) but it must be a valid identifier. + */ + @CanIgnoreReturnValue + public Builder addAlias(String alias, String qualifiedName) { + aliasAs(AliasKind.ALIAS, qualifiedName, alias); + return this; + } + + private void aliasAs(AliasKind kind, String qualifiedName, String alias) { + validateAliasOrThrow(kind, qualifiedName, alias); + aliasMap.put(alias, AliasEntry.create(kind, qualifiedName)); + } + + private void validateAliasOrThrow(AliasKind kind, String qualifiedName, String alias) { + if (alias.isEmpty() || alias.contains(".")) { + throw new IllegalArgumentException( + String.format( + "%s must be non-empty and simple (not qualified): %s=%s", kind, kind, alias)); + } + + if (qualifiedName.charAt(0) == '.') { + throw new IllegalArgumentException( + String.format("qualified name must not begin with a leading '.': %s", qualifiedName)); + } + + AliasEntry aliasRef = aliasMap.get(alias); + if (aliasRef != null) { + throw new IllegalArgumentException( + String.format( + "%s collides with existing reference: name=%s, %s=%s, existing=%s", + kind, qualifiedName, kind, alias, aliasRef.qualifiedName())); + } + + String containerName = name(); + if (containerName.startsWith(alias + ".") || containerName.equals(alias)) { + throw new IllegalArgumentException( + String.format( + "%s collides with container name: name=%s, %s=%s, container=%s", + kind, qualifiedName, kind, alias, containerName)); + } + } + + abstract CelContainer autoBuild(); + + @CheckReturnValue + public CelContainer build() { + setAliasMap(ImmutableMap.copyOf(aliasMap)); + return autoBuild(); + } + } + + /** + * Returns the candidates name of namespaced identifiers in C++ resolution order. + * + *

Names which shadow other names are returned first. If a name includes a leading dot ('.'), + * the name is treated as an absolute identifier which cannot be shadowed. + * + *

Given a container name a.b.c.M.N and a type name R.s, this will deliver in order: + * + *

    + *
  • a.b.c.M.N.R.s + *
  • a.b.c.M.R.s + *
  • a.b.c.R.s + *
  • a.b.R.s + *
  • a.R.s + *
  • R.s + *
+ * + *

If aliases or abbreviations are configured for the container, then alias names will take + * precedence over containerized names. + */ + public ImmutableSet resolveCandidateNames(String typeName) { + if (typeName.startsWith(".")) { + String qualifiedName = typeName.substring(1); + String alias = findAlias(qualifiedName).orElse(qualifiedName); + + return ImmutableSet.of(alias); + } + + String alias = findAlias(typeName).orElse(null); + if (alias != null) { + return ImmutableSet.of(alias); + } + + if (name().isEmpty()) { + return ImmutableSet.of(typeName); + } + + String nextContainer = name(); + ImmutableSet.Builder candidates = + ImmutableSet.builder().add(nextContainer + "." + typeName); + for (int i = nextContainer.lastIndexOf("."); i >= 0; i = nextContainer.lastIndexOf(".")) { + nextContainer = nextContainer.substring(0, i); + candidates.add(nextContainer + "." + typeName); + } + + return candidates.add(typeName).build(); + } + + abstract Builder autoToBuilder(); + + public Builder toBuilder() { + Builder builder = autoToBuilder(); + builder.aliasMap.putAll(aliasMap()); + return builder; + } + + public static Builder newBuilder() { + return new AutoValue_CelContainer.Builder().setName(""); + } + + public static CelContainer ofName(String containerName) { + return newBuilder().setName(containerName).build(); + } + + private Optional findAlias(String name) { + // If an alias exists for the name, ensure it is searched last. + String simple = name; + String qualifier = ""; + int dot = name.indexOf("."); + if (dot > 0) { + simple = name.substring(0, dot); + qualifier = name.substring(dot); + } + AliasEntry alias = aliasMap().get(simple); + if (alias == null) { + return Optional.empty(); + } + + return Optional.of(alias.qualifiedName() + qualifier); + } + + private static boolean isIdentifierChar(int r) { + if (r > 127) { + // Not ASCII + return false; + } + + return r == '.' || r == '_' || Character.isLetter(r) || Character.isDigit(r); + } + + enum AliasKind { + ALIAS, + ABBREVIATION; + + @Override + public String toString() { + return this.name().toLowerCase(Locale.getDefault()); + } + } + + /** Represents an alias or abbreviation. */ + @AutoValue + @Immutable + abstract static class AliasEntry { + static AliasEntry create(AliasKind kind, String qualifiedName) { + return new AutoValue_CelContainer_AliasEntry(kind, qualifiedName); + } + + abstract AliasKind kind(); + + abstract String qualifiedName(); + } +} diff --git a/common/src/main/java/dev/cel/common/CelDescriptorUtil.java b/common/src/main/java/dev/cel/common/CelDescriptorUtil.java index 28aff8291..7695e96d8 100644 --- a/common/src/main/java/dev/cel/common/CelDescriptorUtil.java +++ b/common/src/main/java/dev/cel/common/CelDescriptorUtil.java @@ -14,37 +14,28 @@ package dev.cel.common; -import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.protobuf.DescriptorProtos.FileDescriptorSet; import com.google.protobuf.Descriptors.Descriptor; import com.google.protobuf.Descriptors.FieldDescriptor; import com.google.protobuf.Descriptors.FileDescriptor; -import com.google.protobuf.Descriptors.GenericDescriptor; +import dev.cel.common.annotations.Internal; import dev.cel.common.internal.FileDescriptorSetConverter; import dev.cel.common.types.CelTypes; import java.util.Arrays; -import java.util.Collection; import java.util.HashSet; import java.util.Set; -/** Utility class for working with protobuf descriptors. */ +/** + * Utility class for working with protobuf descriptors. + * + *

CEL Library Internals. Do Not Use. + */ +@Internal public final class CelDescriptorUtil { private CelDescriptorUtil() {} - /** - * Converts descriptor collection to an ImmutableMap. - * - *

Key: Descriptor's full name, Value: Descriptor object - */ - public static ImmutableMap descriptorCollectionToMap( - Collection descriptors) { - ImmutableMap.Builder descriptorMapBuilder = new ImmutableMap.Builder<>(); - descriptors.forEach(d -> descriptorMapBuilder.put(d.getFullName(), d)); - return descriptorMapBuilder.buildOrThrow(); - } - /** * Get the full {@code FileDescriptor} set needed to accurately instantiate the {@code * descriptors}. @@ -175,10 +166,12 @@ private static void collectMessageTypeDescriptors( if (visited.contains(messageName)) { return; } + if (!descriptor.getOptions().getMapEntry()) { visited.add(messageName); celDescriptors.addMessageTypeDescriptors(descriptor); } + if (CelTypes.getWellKnownCelType(messageName).isPresent()) { return; } @@ -243,6 +236,7 @@ private static void copyToFileDescriptorSet( if (visited.contains(fd.getFullName())) { return; } + visited.add(fd.getFullName()); for (FileDescriptor dep : fd.getDependencies()) { copyToFileDescriptorSet(visited, dep, files); diff --git a/common/src/main/java/dev/cel/common/CelIssue.java b/common/src/main/java/dev/cel/common/CelIssue.java index 7f7417a53..4de369470 100644 --- a/common/src/main/java/dev/cel/common/CelIssue.java +++ b/common/src/main/java/dev/cel/common/CelIssue.java @@ -14,10 +14,14 @@ package dev.cel.common; +import static com.google.common.collect.ImmutableList.toImmutableList; + import com.google.auto.value.AutoValue; +import com.google.common.base.Joiner; import com.google.errorprone.annotations.CheckReturnValue; import com.google.errorprone.annotations.Immutable; import dev.cel.common.internal.SafeStringFormatter; +import java.util.Collection; import java.util.Optional; import java.util.PrimitiveIterator; @@ -28,6 +32,7 @@ @Immutable @SuppressWarnings("UnicodeEscape") // Suppressed to distinguish half-width and full-width chars. public abstract class CelIssue { + private static final Joiner JOINER = Joiner.on('\n'); /** Severity of a CelIssue. */ public enum Severity { @@ -46,21 +51,33 @@ public enum Severity { public abstract String getMessage(); + public abstract long getExprId(); + public static Builder newBuilder() { return new AutoValue_CelIssue.Builder(); } /** - * Build {@link CelIssue} from the given {@link CelSourceLocation}, format string, and arguments. + * Build {@link CelIssue} from the given expression id, {@link CelSourceLocation}, format string, + * and arguments. */ - public static CelIssue formatError(CelSourceLocation sourceLocation, String message) { + public static CelIssue formatError( + long exprId, CelSourceLocation sourceLocation, String message) { return newBuilder() + .setExprId(exprId) .setSeverity(Severity.ERROR) .setSourceLocation(sourceLocation) .setMessage(message) .build(); } + /** + * Build {@link CelIssue} from the given {@link CelSourceLocation}, format string, and arguments. + */ + public static CelIssue formatError(CelSourceLocation sourceLocation, String message) { + return formatError(0L, sourceLocation, message); + } + /** Build {@link CelIssue} from the given line, column, format string, and arguments. */ public static CelIssue formatError(int line, int column, String message) { return formatError(CelSourceLocation.of(line, column), message); @@ -73,8 +90,14 @@ public static CelIssue formatError(int line, int column, String message) { private static final char WIDE_DOT = '\uff0e'; private static final char WIDE_HAT = '\uff3e'; + /** Returns a human-readable error with all issues joined in a single string. */ + public static String toDisplayString(Collection issues, Source source) { + return JOINER.join( + issues.stream().map(iss -> iss.toDisplayString(source)).collect(toImmutableList())); + } + /** Returns a string representing this error that is suitable for displaying to humans. */ - public String toDisplayString(CelSource source) { + public String toDisplayString(Source source) { // Based onhttps://github.com/google/cel-go/blob/v0.5.1/common/error.go#L42. String result = SafeStringFormatter.format( @@ -126,6 +149,8 @@ public abstract static class Builder { public abstract Builder setMessage(String message); + public abstract Builder setExprId(long exprId); + @CheckReturnValue public abstract CelIssue build(); } diff --git a/common/src/main/java/dev/cel/common/CelMutableAst.java b/common/src/main/java/dev/cel/common/CelMutableAst.java new file mode 100644 index 000000000..dd57d8b4e --- /dev/null +++ b/common/src/main/java/dev/cel/common/CelMutableAst.java @@ -0,0 +1,122 @@ +// 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. + +package dev.cel.common; + +import dev.cel.common.ast.CelMutableExpr; +import dev.cel.common.ast.CelMutableExprConverter; +import dev.cel.common.ast.CelReference; +import dev.cel.common.types.CelType; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * An abstract representation of CEL Abstract Syntax tree that allows mutation in any of its + * properties. This class is semantically the same as that of the immutable {@link + * CelAbstractSyntaxTree}. + * + *

This should only be used within optimizers to augment an AST. + */ +public final class CelMutableAst { + private final CelMutableExpr mutatedExpr; + private final CelMutableSource source; + private final Map references; + private final Map types; + + /** Returns the underlying {@link CelMutableExpr} representation of the abstract syntax tree. */ + public CelMutableExpr expr() { + return mutatedExpr; + } + + /** + * Returns the {@link CelMutableSource} that was used during construction of the abstract syntax + * tree. + */ + public CelMutableSource source() { + return source; + } + + /** + * Returns the resolved reference to a declaration at expression ID for a type-checked AST. + * + * @return Optional of {@link CelReference} or {@link Optional#empty} if the reference does not + * exist at the ID. + */ + public Optional getReference(long exprId) { + return Optional.ofNullable(references.get(exprId)); + } + + /** + * Returns the type of the expression node for a type-checked AST. + * + * @return Optional of {@link CelType} or {@link Optional#empty} if the type does not exist at the + * ID. + */ + public Optional getType(long exprId) { + return Optional.ofNullable(types.get(exprId)); + } + + /** Converts this mutable AST into a parsed {@link CelAbstractSyntaxTree}. */ + public CelAbstractSyntaxTree toParsedAst() { + return toParsedAst(false); + } + + /** + * Converts this mutable AST into a parsed {@link CelAbstractSyntaxTree}. + * + * @param retainSourcePositions If true, the source positions (line offsets, code points) will be + * retained in the resulting AST. If false, they will be scrubbed. + */ + public CelAbstractSyntaxTree toParsedAst(boolean retainSourcePositions) { + return CelAbstractSyntaxTree.newParsedAst( + CelMutableExprConverter.fromMutableExpr(mutatedExpr), + source.toCelSource(retainSourcePositions)); + } + + /** + * Constructs an instance of {@link CelMutableAst} with the incoming {@link + * CelAbstractSyntaxTree}. + */ + public static CelMutableAst fromCelAst(CelAbstractSyntaxTree ast) { + return new CelMutableAst( + CelMutableExprConverter.fromCelExpr(ast.getExpr()), + CelMutableSource.fromCelSource(ast.getSource()), + ast.getReferenceMap(), + ast.getTypeMap()); + } + + /** + * Constructs an instance of {@link CelMutableAst} with the mutable expression and its source + * builder. + */ + public static CelMutableAst of(CelMutableExpr mutableExpr, CelMutableSource mutableSource) { + return new CelMutableAst(mutableExpr, mutableSource); + } + + private CelMutableAst(CelMutableExpr mutatedExpr, CelMutableSource mutableSource) { + this(mutatedExpr, mutableSource, new HashMap<>(), new HashMap<>()); + } + + private CelMutableAst( + CelMutableExpr mutatedExpr, + CelMutableSource mutableSource, + Map references, + Map types) { + this.mutatedExpr = mutatedExpr; + this.source = mutableSource; + this.references = new HashMap<>(references); + this.types = new HashMap<>(types); + } +} diff --git a/common/src/main/java/dev/cel/common/CelMutableSource.java b/common/src/main/java/dev/cel/common/CelMutableSource.java new file mode 100644 index 000000000..cf85f5af6 --- /dev/null +++ b/common/src/main/java/dev/cel/common/CelMutableSource.java @@ -0,0 +1,156 @@ +// 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. + +package dev.cel.common; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.ImmutableMap.toImmutableMap; + +import com.google.common.collect.ImmutableList; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import dev.cel.common.CelSource.Extension; +import dev.cel.common.ast.CelMutableExpr; +import dev.cel.common.ast.CelMutableExprConverter; +import dev.cel.common.internal.CelCodePointArray; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Represents the mutable portion of the {@link CelSource}. This is intended for the purposes of + * augmenting an AST through CEL optimizers. + */ +public final class CelMutableSource { + + private String description; + private final Map macroCalls; + private final Set extensions; + private final CelCodePointArray codePoints; + private final ImmutableList lineOffsets; + private final Map positions; + + @CanIgnoreReturnValue + public CelMutableSource addMacroCalls(long exprId, CelMutableExpr expr) { + this.macroCalls.put(exprId, checkNotNull(CelMutableExpr.newInstance(expr))); + return this; + } + + @CanIgnoreReturnValue + public CelMutableSource addAllMacroCalls(Map macroCalls) { + this.macroCalls.putAll(macroCalls); + return this; + } + + @CanIgnoreReturnValue + public CelMutableSource addAllExtensions(Collection extensions) { + checkNotNull(extensions); + this.extensions.addAll(extensions); + return this; + } + + @CanIgnoreReturnValue + public CelMutableSource setDescription(String description) { + this.description = checkNotNull(description); + return this; + } + + @CanIgnoreReturnValue + public CelMutableSource clearMacroCall(long exprId) { + this.macroCalls.remove(exprId); + return this; + } + + @CanIgnoreReturnValue + public CelMutableSource clearMacroCalls() { + this.macroCalls.clear(); + return this; + } + + public String getDescription() { + return description; + } + + public Map getMacroCalls() { + return macroCalls; + } + + public Set getExtensions() { + return extensions; + } + + public CelSource toCelSource(boolean retainSourcePositions) { + CelSource.Builder builder = + retainSourcePositions + ? CelSource.newBuilder(codePoints, lineOffsets).addPositionsMap(positions) + : CelSource.newBuilder(); + + return builder + .setDescription(description) + .addAllExtensions(extensions) + .addAllMacroCalls( + macroCalls.entrySet().stream() + .collect( + toImmutableMap( + Entry::getKey, v -> CelMutableExprConverter.fromMutableExpr(v.getValue())))) + .build(); + } + + public static CelMutableSource newInstance() { + return new CelMutableSource( + "", + new HashMap<>(), + new HashSet<>(), + CelCodePointArray.fromString(""), + ImmutableList.of(), + new HashMap<>()); + } + + public static CelMutableSource fromCelSource(CelSource source) { + return new CelMutableSource( + source.getDescription(), + source.getMacroCalls().entrySet().stream() + .collect( + Collectors.toMap( + Entry::getKey, + v -> CelMutableExprConverter.fromCelExpr(v.getValue()), + (prev, next) -> { + throw new IllegalStateException( + "Unexpected source collision at ID: " + prev.id()); + }, + HashMap::new)), + source.getExtensions(), + source.getContent(), + source.getLineOffsets(), + source.getPositionsMap()); + } + + CelMutableSource( + String description, + Map macroCalls, + Set extensions, + CelCodePointArray codePoints, + ImmutableList lineOffsets, + Map positions) { + this.description = checkNotNull(description); + this.macroCalls = checkNotNull(macroCalls); + this.extensions = checkNotNull(extensions); + this.codePoints = checkNotNull(codePoints); + this.lineOffsets = checkNotNull(lineOffsets); + this.positions = checkNotNull(positions); + } +} diff --git a/common/src/main/java/dev/cel/common/CelOptions.java b/common/src/main/java/dev/cel/common/CelOptions.java index eaa812e2b..d9c2dd818 100644 --- a/common/src/main/java/dev/cel/common/CelOptions.java +++ b/common/src/main/java/dev/cel/common/CelOptions.java @@ -15,7 +15,6 @@ package dev.cel.common; import com.google.auto.value.AutoValue; -import com.google.common.collect.ImmutableSet; import com.google.errorprone.annotations.CheckReturnValue; import com.google.errorprone.annotations.Immutable; @@ -30,6 +29,18 @@ @Immutable public abstract class CelOptions { + /** + * ProtoUnsetFieldOptions describes how to handle Activation.fromProto() calls where proto message + * fields may be unset and should either be handled perhaps as absent or as the default proto + * value. + */ + public enum ProtoUnsetFieldOptions { + // Do not bind a field if it is unset. Repeated fields are bound as empty list. + SKIP, + // Bind the (proto api) default value for a field. + BIND_DEFAULT + } + public static final CelOptions DEFAULT = current().build(); public static final CelOptions LEGACY = newBuilder().disableCelStandardEquality(true).build(); @@ -55,6 +66,10 @@ public abstract class CelOptions { public abstract boolean retainUnbalancedLogicalExpressions(); + public abstract boolean enableHiddenAccumulatorVar(); + + public abstract boolean enableQuotedIdentifierSyntax(); + // Type-Checker related options public abstract boolean enableCompileTimeOverloadResolution(); @@ -67,10 +82,14 @@ public abstract class CelOptions { public abstract boolean enableNamespacedDeclarations(); + public abstract boolean enableJsonFieldNames(); + // Evaluation related options public abstract boolean disableCelStandardEquality(); + public abstract boolean enableShortCircuiting(); + public abstract boolean enableRegexPartialMatch(); public abstract boolean enableUnsignedComparisonAndArithmeticIsUnsigned(); @@ -91,61 +110,17 @@ public abstract class CelOptions { public abstract int comprehensionMaxIterations(); - public abstract Builder toBuilder(); + public abstract boolean evaluateCanonicalTypesToNativeValues(); - public ImmutableSet toExprFeatures() { - ImmutableSet.Builder features = ImmutableSet.builder(); - if (enableCompileTimeOverloadResolution()) { - features.add(ExprFeatures.COMPILE_TIME_OVERLOAD_RESOLUTION); - } - if (disableCelStandardEquality()) { - features.add(ExprFeatures.LEGACY_JAVA_EQUALITY); - } - if (enableHomogeneousLiterals()) { - features.add(ExprFeatures.HOMOGENEOUS_LITERALS); - } - if (enableRegexPartialMatch()) { - features.add(ExprFeatures.REGEX_PARTIAL_MATCH); - } - if (enableReservedIds()) { - features.add(ExprFeatures.RESERVED_IDS); - } - if (enableUnsignedComparisonAndArithmeticIsUnsigned()) { - features.add(ExprFeatures.UNSIGNED_COMPARISON_AND_ARITHMETIC_IS_UNSIGNED); - } - if (retainRepeatedUnaryOperators()) { - features.add(ExprFeatures.RETAIN_REPEATED_UNARY_OPERATORS); - } - if (retainUnbalancedLogicalExpressions()) { - features.add(ExprFeatures.RETAIN_UNBALANCED_LOGICAL_EXPRESSIONS); - } - if (errorOnIntWrap()) { - features.add(ExprFeatures.ERROR_ON_WRAP); - } - if (errorOnDuplicateMapKeys()) { - features.add(ExprFeatures.ERROR_ON_DUPLICATE_KEYS); - } - if (populateMacroCalls()) { - features.add(ExprFeatures.POPULATE_MACRO_CALLS); - } - if (enableTimestampEpoch()) { - features.add(ExprFeatures.ENABLE_TIMESTAMP_EPOCH); - } - if (enableHeterogeneousNumericComparisons()) { - features.add(ExprFeatures.ENABLE_HETEROGENEOUS_NUMERIC_COMPARISONS); - } - if (enableNamespacedDeclarations()) { - features.add(ExprFeatures.ENABLE_NAMESPACED_DECLARATIONS); - } - if (enableUnsignedLongs()) { - features.add(ExprFeatures.ENABLE_UNSIGNED_LONGS); - } - if (enableProtoDifferencerEquality()) { - features.add(ExprFeatures.PROTO_DIFFERENCER_EQUALITY); - } - - return features.build(); - } + public abstract boolean unwrapWellKnownTypesOnFunctionDispatch(); + + public abstract ProtoUnsetFieldOptions fromProtoUnsetFieldOption(); + + public abstract boolean enableComprehension(); + + public abstract int maxRegexProgramSize(); + + public abstract Builder toBuilder(); /** * Return an unconfigured {@code Builder}. This is equivalent to preserving all legacy behaviors, @@ -162,14 +137,19 @@ public static Builder newBuilder() { .populateMacroCalls(false) .retainRepeatedUnaryOperators(false) .retainUnbalancedLogicalExpressions(false) + .enableHiddenAccumulatorVar(true) + .enableQuotedIdentifierSyntax(true) // Type-Checker options .enableCompileTimeOverloadResolution(false) .enableHomogeneousLiterals(false) .enableTimestampEpoch(false) .enableHeterogeneousNumericComparisons(false) .enableNamespacedDeclarations(true) + .enableJsonFieldNames(false) // Evaluation options .disableCelStandardEquality(true) + .evaluateCanonicalTypesToNativeValues(false) + .enableShortCircuiting(true) .enableRegexPartialMatch(false) .enableUnsignedComparisonAndArithmeticIsUnsigned(false) .enableUnsignedLongs(false) @@ -179,7 +159,11 @@ public static Builder newBuilder() { .resolveTypeDependencies(true) .enableUnknownTracking(false) .enableCelValue(false) - .comprehensionMaxIterations(-1); + .comprehensionMaxIterations(-1) + .unwrapWellKnownTypesOnFunctionDispatch(true) + .fromProtoUnsetFieldOption(ProtoUnsetFieldOptions.BIND_DEFAULT) + .enableComprehension(true) + .maxRegexProgramSize(-1); } /** @@ -190,40 +174,16 @@ public static Builder current() { return newBuilder() .enableReservedIds(true) .enableUnsignedComparisonAndArithmeticIsUnsigned(true) + .enableUnsignedLongs(true) .enableRegexPartialMatch(true) + .enableTimestampEpoch(true) .errorOnDuplicateMapKeys(true) + .evaluateCanonicalTypesToNativeValues(true) .errorOnIntWrap(true) .resolveTypeDependencies(true) .disableCelStandardEquality(false); } - public static CelOptions fromExprFeatures(ImmutableSet features) { - return newBuilder() - .enableCompileTimeOverloadResolution( - features.contains(ExprFeatures.COMPILE_TIME_OVERLOAD_RESOLUTION)) - .disableCelStandardEquality(features.contains(ExprFeatures.LEGACY_JAVA_EQUALITY)) - .enableHomogeneousLiterals(features.contains(ExprFeatures.HOMOGENEOUS_LITERALS)) - .enableRegexPartialMatch(features.contains(ExprFeatures.REGEX_PARTIAL_MATCH)) - .enableReservedIds(features.contains(ExprFeatures.RESERVED_IDS)) - .enableUnsignedComparisonAndArithmeticIsUnsigned( - features.contains(ExprFeatures.UNSIGNED_COMPARISON_AND_ARITHMETIC_IS_UNSIGNED)) - .retainRepeatedUnaryOperators( - features.contains(ExprFeatures.RETAIN_REPEATED_UNARY_OPERATORS)) - .retainUnbalancedLogicalExpressions( - features.contains(ExprFeatures.RETAIN_UNBALANCED_LOGICAL_EXPRESSIONS)) - .errorOnIntWrap(features.contains(ExprFeatures.ERROR_ON_WRAP)) - .errorOnDuplicateMapKeys(features.contains(ExprFeatures.ERROR_ON_DUPLICATE_KEYS)) - .populateMacroCalls(features.contains(ExprFeatures.POPULATE_MACRO_CALLS)) - .enableTimestampEpoch(features.contains(ExprFeatures.ENABLE_TIMESTAMP_EPOCH)) - .enableHeterogeneousNumericComparisons( - features.contains(ExprFeatures.ENABLE_HETEROGENEOUS_NUMERIC_COMPARISONS)) - .enableNamespacedDeclarations( - features.contains(ExprFeatures.ENABLE_NAMESPACED_DECLARATIONS)) - .enableUnsignedLongs(features.contains(ExprFeatures.ENABLE_UNSIGNED_LONGS)) - .enableProtoDifferencerEquality(features.contains(ExprFeatures.PROTO_DIFFERENCER_EQUALITY)) - .build(); - } - /** Builder for configuring the {@code CelOptions}. */ @AutoValue.Builder public abstract static class Builder { @@ -285,6 +245,26 @@ public abstract static class Builder { */ public abstract Builder retainUnbalancedLogicalExpressions(boolean value); + /** + * Enable the use of a hidden accumulator variable name. + * + *

This is a temporary option to transition to using an internal identifier for the + * accumulator variable used by builtin comprehension macros. When enabled, parses result in a + * semantically equivalent AST, but with a different accumulator variable that can't be directly + * referenced in the source expression. + */ + public abstract Builder enableHiddenAccumulatorVar(boolean value); + + /** + * Enable quoted identifier syntax. + * + *

This enables the use of quoted identifier syntax when parsing CEL expressions. When + * enabled, the parser will accept identifiers that are surrounded by backticks (`) and will + * treat them as a single identifier. Currently, this is only supported for field specifiers + * over a limited character set. + */ + public abstract Builder enableQuotedIdentifierSyntax(boolean value); + // Type-Checker related options /** @@ -312,14 +292,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); /** @@ -364,6 +350,17 @@ public abstract static class Builder { */ public abstract Builder disableCelStandardEquality(boolean value); + /** + * Enable short-circuiting of the logical operator evaluation. If enabled, AND, OR, and TERNARY + * do not evaluate the entire expression once the resulting value is known from the left-hand + * side. + * + *

This option is enabled by default. In most cases, this should not be disabled except for + * debugging purposes or collecting results for all evaluated branches through {@link + * dev.cel.runtime.CelEvaluationListener}. + */ + public abstract Builder enableShortCircuiting(boolean value); + /** * Treat regex {@code matches} calls as substring (unanchored) match patterns. * @@ -384,7 +381,11 @@ public abstract static class Builder { /** * Use {@code UnsignedLong} values to represent unsigned integers within CEL instead of the * nearest Java equivalent of {@code Long}. + * + * @deprecated Do not use. This option is enabled by default in the currently supported feature + * set {@link CelOptions#DEFAULT}. This flag will be removed in the future. */ + @Deprecated public abstract Builder enableUnsignedLongs(boolean value); /** @@ -432,12 +433,10 @@ public abstract static class Builder { public abstract Builder enableUnknownTracking(boolean value); /** - * Enables the usage of {@code CelValue} for the runtime. It is a native value representation of - * CEL that wraps Java native objects, and comes with extended capabilities, such as allowing - * value constructs not understood by CEL (ex: POJOs). - * - *

Warning: This option is experimental. + * @deprecated Do not use, this flag will be removed in the future. Use the planner based + * runtime instead, which supports CelValue by default. */ + @Deprecated public abstract Builder enableCelValue(boolean value); /** @@ -452,6 +451,73 @@ public abstract static class Builder { */ public abstract Builder comprehensionMaxIterations(int value); + /** + * If set, canonical CEL types such as bytes and CEL null will return their native value + * equivalents instead of protobuf based values. Specifically: + * + *

    + *
  • Bytes: {@code dev.cel.common.values.CelByteString} instead of {@code + * com.google.protobuf.ByteString}. + *
  • CEL null: {@code dev.cel.common.values.NullValue} instead of {@code + * com.google.protobuf.NullValue}. + *
  • Timestamp: {@code java.time.Instant} instead of {@code com.google.protobuf.Timestamp}. + *
  • Duration: {@code java.time.Duration} instead of {@code com.google.protobuf.Duration}. + *
+ * + * @deprecated Do not use. This flag is enabled by default and will be removed in the future. + */ + @Deprecated + public abstract Builder evaluateCanonicalTypesToNativeValues(boolean value); + + /** + * If disabled, CEL runtime will no longer adapt the function dispatch results for protobuf's + * well known types to other types. This option is enabled by default. + * + * @deprecated This will be removed in the future. Please update your codebase to be conformant + * with CEL specification. + */ + @Deprecated + public abstract Builder unwrapWellKnownTypesOnFunctionDispatch(boolean value); + + /** + * Configure how unset proto fields are handled when evaluating over a protobuf message where + * fields are intended to be treated as top-level variables. Defaults to binding all fields to + * their default value if unset. + * + * @see ProtoUnsetFieldOptions + */ + public abstract Builder fromProtoUnsetFieldOption(ProtoUnsetFieldOptions value); + + /** + * Enables comprehension (macros) for the runtime. Setting false has the same effect with + * assigning 0 for {@link #comprehensionMaxIterations()}. This option exists to maintain parity + * with cel-cpp interpreter options. + */ + public abstract Builder enableComprehension(boolean value); + + /** + * Set maximum program size for RE2J regex. + * + *

The program size is a very approximate measure of a regexp's "cost". Larger numbers are + * more expensive than smaller numbers. + * + *

A negative {@code value} will disable the check. + * + *

There's no guarantee that RE2 program size has the exact same value across other CEL + * implementations (C++ and Go). + */ + public abstract Builder maxRegexProgramSize(int value); + + /** + * Use the `json_name` field option on a protobuf message as the name of the field. + * + *

If enabled, the compiler will only accept the `json_name` and no longer recognize the + * original protobuf field name. Use with caution as this may break existing expressions during + * compilation. The runtime continues to support both names for maintaining backwards + * compatibility. + */ + public abstract Builder enableJsonFieldNames(boolean value); + public abstract CelOptions build(); } } diff --git a/common/src/main/java/dev/cel/common/CelOverloadDecl.java b/common/src/main/java/dev/cel/common/CelOverloadDecl.java index 30bcac5a8..1d9c61e9d 100644 --- a/common/src/main/java/dev/cel/common/CelOverloadDecl.java +++ b/common/src/main/java/dev/cel/common/CelOverloadDecl.java @@ -25,8 +25,8 @@ import com.google.errorprone.annotations.CheckReturnValue; import com.google.errorprone.annotations.Immutable; 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 java.util.Arrays; import java.util.List; @@ -93,7 +93,7 @@ public abstract static class Builder { * Sets the parameter types {@link #parameterTypes()}. Note that this will override any * parameter types added via the accumulator methods {@link #addParameterTypes}. */ - public abstract Builder setParameterTypes(ImmutableSet value); + public abstract Builder setParameterTypes(ImmutableList value); public abstract CelType resultType(); @@ -230,10 +230,10 @@ public static Overload celOverloadToOverload(CelOverloadDecl overload) { return Overload.newBuilder() .setIsInstanceFunction(overload.isInstanceFunction()) .setOverloadId(overload.overloadId()) - .setResultType(CelTypes.celTypeToType(overload.resultType())) + .setResultType(CelProtoTypes.celTypeToType(overload.resultType())) .addAllParams( overload.parameterTypes().stream() - .map(CelTypes::celTypeToType) + .map(CelProtoTypes::celTypeToType) .collect(toImmutableList())) .addAllTypeParams(overload.typeParameterNames()) .setDoc(overload.doc()) @@ -244,11 +244,11 @@ public static CelOverloadDecl overloadToCelOverload(Overload overload) { return CelOverloadDecl.newBuilder() .setIsInstanceFunction(overload.getIsInstanceFunction()) .setOverloadId(overload.getOverloadId()) - .setResultType(CelTypes.typeToCelType(overload.getResultType())) + .setResultType(CelProtoTypes.typeToCelType(overload.getResultType())) .setDoc(overload.getDoc()) .addParameterTypes( overload.getParamsList().stream() - .map(CelTypes::typeToCelType) + .map(CelProtoTypes::typeToCelType) .collect(toImmutableList())) .build(); } diff --git a/common/src/main/java/dev/cel/common/CelProtoAbstractSyntaxTree.java b/common/src/main/java/dev/cel/common/CelProtoAbstractSyntaxTree.java index ef69c174b..7230c80af 100644 --- a/common/src/main/java/dev/cel/common/CelProtoAbstractSyntaxTree.java +++ b/common/src/main/java/dev/cel/common/CelProtoAbstractSyntaxTree.java @@ -29,7 +29,7 @@ import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.CheckReturnValue; import dev.cel.common.ast.CelExprConverter; -import dev.cel.common.types.CelTypes; +import dev.cel.common.types.CelProtoTypes; import java.util.Collection; import java.util.Map.Entry; @@ -66,7 +66,8 @@ private CelProtoAbstractSyntaxTree(CheckedExpr checkedExpr) { Entry::getKey, v -> CelExprConverter.exprReferenceToCelReference(v.getValue()))), checkedExpr.getTypeMapMap().entrySet().stream() - .collect(toImmutableMap(Entry::getKey, v -> CelTypes.typeToCelType(v.getValue())))); + .collect( + toImmutableMap(Entry::getKey, v -> CelProtoTypes.typeToCelType(v.getValue())))); } private CelProtoAbstractSyntaxTree(CelAbstractSyntaxTree ast) { @@ -98,18 +99,19 @@ private CelProtoAbstractSyntaxTree(CelAbstractSyntaxTree ast) { v -> CelExprConverter.celReferenceToExprReference(v.getValue())))); checkedExprBuilder.putAllTypeMap( ast.getTypeMap().entrySet().stream() - .collect(toImmutableMap(Entry::getKey, v -> CelTypes.celTypeToType(v.getValue())))); + .collect( + toImmutableMap(Entry::getKey, v -> CelProtoTypes.celTypeToType(v.getValue())))); } this.checkedExpr = checkedExprBuilder.build(); } - /** Construct an abstract syntax tree from a {@link com.google.api.expr.CheckedExpr}. */ + /** Construct an abstract syntax tree from a {@link dev.cel.expr.CheckedExpr}. */ public static CelProtoAbstractSyntaxTree fromCheckedExpr(CheckedExpr checkedExpr) { return new CelProtoAbstractSyntaxTree(checkedExpr); } - /** Construct an abstract syntax tree from a {@link com.google.api.expr.ParsedExpr}. */ + /** Construct an abstract syntax tree from a {@link dev.cel.expr.ParsedExpr}. */ public static CelProtoAbstractSyntaxTree fromParsedExpr(ParsedExpr parsedExpr) { return new CelProtoAbstractSyntaxTree( CheckedExpr.newBuilder() @@ -137,7 +139,7 @@ public CelAbstractSyntaxTree getAst() { } /** - * Returns the underlying {@link com.google.api.expr.Expr} representation of the abstract syntax + * Returns the underlying {@link dev.cel.expr.Expr} representation of the abstract syntax * tree. */ @CheckReturnValue @@ -146,7 +148,7 @@ public Expr getExpr() { } /** - * Returns the underlying {@link com.google.api.expr.CheckedExpr} representation of the abstract + * Returns the underlying {@link dev.cel.expr.CheckedExpr} representation of the abstract * syntax tree. Throws {@link java.lang.IllegalStateException} if {@link * CelAbstractSyntaxTree#isChecked} is false. */ @@ -159,7 +161,7 @@ public CheckedExpr toCheckedExpr() { } /** - * Returns the underlying {@link com.google.api.expr.SourceInfo} representation of the abstract + * Returns the underlying {@link dev.cel.expr.SourceInfo} representation of the abstract * syntax tree. */ @CheckReturnValue @@ -168,7 +170,7 @@ public SourceInfo getSourceInfo() { } /** - * Returns the underlying {@link com.google.api.expr.ParsedExpr} representation of the abstract + * Returns the underlying {@link dev.cel.expr.ParsedExpr} representation of the abstract * syntax tree. */ @CheckReturnValue @@ -182,7 +184,7 @@ public ParsedExpr toParsedExpr() { */ @CheckReturnValue public Type getProtoResultType() { - return CelTypes.celTypeToType(ast.getResultType()); + return CelProtoTypes.celTypeToType(ast.getResultType()); } private static ImmutableList fromCelExtensionsToExprExtensions( diff --git a/common/src/main/java/dev/cel/common/CelProtoJsonAdapter.java b/common/src/main/java/dev/cel/common/CelProtoJsonAdapter.java new file mode 100644 index 000000000..e74d52f7a --- /dev/null +++ b/common/src/main/java/dev/cel/common/CelProtoJsonAdapter.java @@ -0,0 +1,176 @@ +// 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. + +package dev.cel.common; + +import com.google.common.base.CaseFormat; +import com.google.common.base.Joiner; +import com.google.common.primitives.UnsignedLong; +import com.google.errorprone.annotations.Immutable; +import com.google.protobuf.Duration; +import com.google.protobuf.Empty; +import com.google.protobuf.FieldMask; +import com.google.protobuf.ListValue; +import com.google.protobuf.NullValue; +import com.google.protobuf.Struct; +import com.google.protobuf.Timestamp; +import com.google.protobuf.Value; +import dev.cel.common.internal.DateTimeHelpers; +import dev.cel.common.internal.ProtoTimeUtils; +import dev.cel.common.values.CelByteString; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.Map; + +/** + * {@code CelProtoJsonAdapter} is a utility to handle conversion from Java native objects + * representing CEL values to Protobuf's structured value which maps to JSON object schema. + */ +@Immutable +public final class CelProtoJsonAdapter { + private static final long JSON_MAX_INT_VALUE = (1L << 53) - 1; + private static final long JSON_MIN_INT_VALUE = -JSON_MAX_INT_VALUE; + private static final UnsignedLong JSON_MAX_UINT_VALUE = + UnsignedLong.fromLongBits(JSON_MAX_INT_VALUE); + + /** + * Adapts a map to a JSON Struct. + * + * @throws ClassCastException If the key is not a string literal + * @throws IllegalArgumentException If any of the map's value is not convertible to a canonical + * JSON representation defined by protobuf. + */ + public static Struct adaptToJsonStructValue(Map map) { + Struct.Builder struct = Struct.newBuilder(); + for (Map.Entry entry : map.entrySet()) { + String key = entry.getKey(); + Object keyValue = entry.getValue(); + + struct.putFields(key, adaptValueToJsonValue(keyValue)); + } + return struct.build(); + } + + /** + * Adapts a native Java object to a JSON value. + * + * @throws IllegalArgumentException If the value is not convertible to a canonical JSON * + * representation defined by protobuf. + */ + @SuppressWarnings("unchecked") + public static Value adaptValueToJsonValue(Object value) { + Value.Builder json = Value.newBuilder(); + if (value == null || value instanceof dev.cel.common.values.NullValue) { + return json.setNullValue(NullValue.NULL_VALUE).build(); + } + if (value instanceof Boolean) { + return json.setBoolValue((Boolean) value).build(); + } + if (value instanceof Integer || value instanceof Long) { + long longValue = ((Number) value).longValue(); + if (longValue < JSON_MIN_INT_VALUE || longValue > JSON_MAX_INT_VALUE) { + return json.setStringValue(Long.toString(longValue)).build(); + } + return json.setNumberValue((double) longValue).build(); + } + if (value instanceof UnsignedLong) { + if (((UnsignedLong) value).compareTo(JSON_MAX_UINT_VALUE) > 0) { + return json.setStringValue(((UnsignedLong) value).toString()).build(); + } + return json.setNumberValue((double) ((UnsignedLong) value).longValue()).build(); + } + if (value instanceof Float || value instanceof Double) { + return json.setNumberValue(((Number) value).doubleValue()).build(); + } + if (value instanceof CelByteString) { + return json.setStringValue( + Base64.getEncoder().encodeToString(((CelByteString) value).toByteArray())) + .build(); + } + if (value instanceof String) { + return json.setStringValue((String) value).build(); + } + if (value instanceof Map) { + Struct struct = adaptToJsonStructValue((Map) value); + return json.setStructValue(struct).build(); + } + if (value instanceof Iterable) { + ListValue listValue = adaptToJsonListValue((Iterable) value); + return json.setListValue(listValue).build(); + } + if (value instanceof Timestamp) { + // CEL follows the proto3 to JSON conversion which formats as an RFC 3339 encoded JSON string. + String ts = ProtoTimeUtils.toString((Timestamp) value); + return json.setStringValue(ts).build(); + } + if (value instanceof Duration) { + String duration = ProtoTimeUtils.toString((Duration) value); + return json.setStringValue(duration).build(); + } + if (value instanceof Instant) { + // Instant's toString follows RFC 3339 + return json.setStringValue(value.toString()).build(); + } + if (value instanceof java.time.Duration) { + String duration = DateTimeHelpers.toString((java.time.Duration) value); + return json.setStringValue(duration).build(); + } + if (value instanceof FieldMask) { + String fieldMaskStr = toJsonString((FieldMask) value); + return json.setStringValue(fieldMaskStr).build(); + } + if (value instanceof Empty) { + // google.protobuf.Empty is just an empty json map {} + return json.setStructValue(Struct.getDefaultInstance()).build(); + } + + throw new IllegalArgumentException( + String.format("Value %s cannot be adapted to a JSON Value.", value)); + } + + /** + * Joins the field mask's paths into a single string with commas. This logic is copied from + * Protobuf's FieldMaskUtil.java, which we cannot directly use here due to its dependency to + * descriptors. + */ + private static String toJsonString(FieldMask fieldMask) { + List paths = new ArrayList<>(fieldMask.getPathsCount()); + + for (String path : fieldMask.getPathsList()) { + if (!path.isEmpty()) { + paths.add(CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, path)); + } + } + + return Joiner.on(",").join(paths); + } + + /** + * Adapts an iterable to a JSON list value. + * + * @throws IllegalArgumentException If any of the map's value is not convertible to a canonical + * JSON representation defined by protobuf. + */ + public static ListValue adaptToJsonListValue(Iterable value) { + ListValue.Builder jsonList = ListValue.newBuilder(); + for (Object elem : value) { + jsonList.addValues(adaptValueToJsonValue(elem)); + } + return jsonList.build(); + } + + private CelProtoJsonAdapter() {} +} diff --git a/common/src/main/java/dev/cel/common/CelSource.java b/common/src/main/java/dev/cel/common/CelSource.java index 62a74873a..d64049f61 100644 --- a/common/src/main/java/dev/cel/common/CelSource.java +++ b/common/src/main/java/dev/cel/common/CelSource.java @@ -16,11 +16,12 @@ 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 com.google.auto.value.AutoValue; -import com.google.common.base.Splitter; 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.CheckReturnValue; import com.google.errorprone.annotations.Immutable; @@ -35,35 +36,34 @@ /** Represents the source content of an expression and related metadata. */ @Immutable -public final class CelSource { - private static final Splitter LINE_SPLITTER = Splitter.on('\n'); - - private final CelCodePointArray codePoints; - private final String description; - private final ImmutableList lineOffsets; - private final ImmutableMap positions; - private final ImmutableMap macroCalls; - private final ImmutableList extensions; - - private CelSource(Builder builder) { - this.codePoints = checkNotNull(builder.codePoints); - this.description = checkNotNull(builder.description); - this.positions = checkNotNull(builder.positions.buildOrThrow()); - this.lineOffsets = checkNotNull(ImmutableList.copyOf(builder.lineOffsets)); - this.macroCalls = checkNotNull(ImmutableMap.copyOf(builder.macroCalls)); - this.extensions = checkNotNull(builder.extensions.build()); - } +@AutoValue +public abstract class CelSource implements Source { + + abstract CelCodePointArray codePoints(); + + abstract String description(); + + abstract ImmutableList lineOffsets(); + + abstract ImmutableMap positions(); + + abstract ImmutableMap macroCalls(); + + abstract ImmutableSet extensions(); + @Override public CelCodePointArray getContent() { - return codePoints; + return codePoints(); } + @Override public String getDescription() { - return description; + return description(); } + @Override public ImmutableMap getPositionsMap() { - return positions; + return positions(); } /** @@ -73,15 +73,15 @@ public ImmutableMap getPositionsMap() { *

NOTE: The indices point to the index just after the '\n' not the index of '\n' itself. */ public ImmutableList getLineOffsets() { - return lineOffsets; + return lineOffsets(); } public ImmutableMap getMacroCalls() { - return macroCalls; + return macroCalls(); } - public ImmutableList getExtensions() { - return extensions; + public ImmutableSet getExtensions() { + return extensions(); } /** See {@link #getLocationOffset(int, int)}. */ @@ -98,34 +98,19 @@ public Optional getLocationOffset(CelSourceLocation location) { * @param column the column number starting from 0 */ public Optional getLocationOffset(int line, int column) { - return getLocationOffsetImpl(lineOffsets, line, column); + return getLocationOffsetImpl(lineOffsets(), line, column); } /** * Get the line and column in the source expression text for the given code point {@code offset}. */ public Optional getOffsetLocation(int offset) { - return getOffsetLocationImpl(lineOffsets, offset); + return CelSourceHelper.getOffsetLocation(codePoints(), offset); } - /** - * Get the text from the source expression that corresponds to {@code line}. - * - * @param line the line number starting from 1. - */ + @Override public Optional getSnippet(int line) { - checkArgument(line > 0); - int start = findLineOffset(lineOffsets, line); - if (start == -1) { - return Optional.empty(); - } - int end = findLineOffset(lineOffsets, line + 1); - if (end == -1) { - end = codePoints.size(); - } else { - end--; - } - return Optional.of(end != start ? codePoints.slice(start, end).toString() : ""); + return CelSourceHelper.getSnippet(codePoints(), line); } /** @@ -137,55 +122,22 @@ public Optional getSnippet(int line) { */ private static Optional getLocationOffsetImpl( List lineOffsets, int line, int column) { - checkArgument(line > 0); - checkArgument(column >= 0); - int offset = findLineOffset(lineOffsets, line); + if (line <= 0 || column < 0) { + return Optional.empty(); + } + int offset = CelSourceHelper.findLineOffset(lineOffsets, line); if (offset == -1) { return Optional.empty(); } return Optional.of(offset + column); } - /** - * Get the line and column in the source expression text for the given code point {@code offset}. - */ - public static Optional getOffsetLocationImpl( - List lineOffsets, int offset) { - checkArgument(offset >= 0); - LineAndOffset lineAndOffset = findLine(lineOffsets, offset); - return Optional.of(CelSourceLocation.of(lineAndOffset.line, offset - lineAndOffset.offset)); - } - - private static int findLineOffset(List lineOffsets, int line) { - if (line == 1) { - return 0; - } - if (line > 1 && line <= lineOffsets.size()) { - return lineOffsets.get(line - 2); - } - return -1; - } - - private static LineAndOffset findLine(List lineOffsets, int offset) { - int line = 1; - for (int index = 0; index < lineOffsets.size(); index++) { - if (lineOffsets.get(index) > offset) { - break; - } - line++; - } - if (line == 1) { - return new LineAndOffset(line, 0); - } - return new LineAndOffset(line, lineOffsets.get(line - 2)); - } - public Builder toBuilder() { - return new Builder(codePoints, lineOffsets) - .setDescription(description) - .addPositionsMap(positions) - .addAllExtensions(extensions) - .addAllMacroCalls(macroCalls); + return new Builder(codePoints(), lineOffsets()) + .setDescription(description()) + .addPositionsMap(positions()) + .addAllExtensions(extensions()) + .addAllMacroCalls(macroCalls()); } public static Builder newBuilder() { @@ -193,13 +145,16 @@ public static Builder newBuilder() { } public static Builder newBuilder(String text) { - List lineOffsets = new ArrayList<>(); - int lineOffset = 0; - for (String line : LINE_SPLITTER.split(text)) { - lineOffset += (int) (line.codePoints().count() + 1); - lineOffsets.add(lineOffset); - } - return new Builder(CelCodePointArray.fromString(text), lineOffsets); + return newBuilder(CelCodePointArray.fromString(text)); + } + + public static Builder newBuilder(CelCodePointArray codePoints) { + return newBuilder(codePoints, codePoints.lineOffsets()); + } + + public static Builder newBuilder( + CelCodePointArray codePoints, ImmutableList lineOffsets) { + return new Builder(codePoints, lineOffsets); } /** Builder for {@link CelSource}. */ @@ -207,10 +162,11 @@ public static final class Builder { private final CelCodePointArray codePoints; private final List lineOffsets; - private final ImmutableMap.Builder positions; + private final Map positions; private final Map macroCalls; - private final ImmutableList.Builder extensions; + private final ImmutableSet.Builder extensions; + private final boolean lineOffsetsAlreadyComputed; private String description; private Builder() { @@ -220,10 +176,11 @@ private Builder() { private Builder(CelCodePointArray codePoints, List lineOffsets) { this.codePoints = checkNotNull(codePoints); this.lineOffsets = checkNotNull(lineOffsets); - this.positions = ImmutableMap.builder(); + this.positions = new HashMap<>(); this.macroCalls = new HashMap<>(); - this.extensions = ImmutableList.builder(); + this.extensions = ImmutableSet.builder(); this.description = ""; + this.lineOffsetsAlreadyComputed = !lineOffsets.isEmpty(); } @CanIgnoreReturnValue @@ -235,6 +192,9 @@ public Builder setDescription(String description) { @CanIgnoreReturnValue public Builder addLineOffsets(int lineOffset) { checkArgument(lineOffset >= 0); + checkState( + !lineOffsetsAlreadyComputed, + "Line offsets were already been computed through the provided code points."); lineOffsets.add(lineOffset); return this; } @@ -260,6 +220,12 @@ public Builder addPositions(long exprId, int position) { return this; } + @CanIgnoreReturnValue + public Builder removePositions(long exprId) { + this.positions.remove(exprId); + return this; + } + @CanIgnoreReturnValue public Builder addMacroCalls(long exprId, CelExpr expr) { this.macroCalls.put(exprId, expr); @@ -272,12 +238,14 @@ public Builder addAllMacroCalls(Map macroCalls) { return this; } - @CanIgnoreReturnValue - public Builder clearMacroCall(long exprId) { - this.macroCalls.remove(exprId); - return this; + public ImmutableSet getExtensions() { + return extensions.build(); } + /** + * Adds one or more {@link Extension}s to the source information. Extensions implement set + * semantics and deduped if same ones are provided. + */ @CanIgnoreReturnValue public Builder addAllExtensions(Iterable extensions) { checkNotNull(extensions); @@ -285,6 +253,10 @@ public Builder addAllExtensions(Iterable extensions) { return this; } + /** + * Adds one or more {@link Extension}s to the source information. Extensions implement set + * semantics and deduped if same ones are provided. + */ @CanIgnoreReturnValue public Builder addAllExtensions(Extension... extensions) { return addAllExtensions(Arrays.asList(extensions)); @@ -312,12 +284,12 @@ public Optional getLocationOffset(int line, int column) { * offset}. */ public Optional getOffsetLocation(int offset) { - return getOffsetLocationImpl(lineOffsets, offset); + return CelSourceHelper.getOffsetLocation(codePoints, offset); } @CheckReturnValue - public ImmutableMap getPositionsMap() { - return this.positions.buildOrThrow(); + public Map getPositionsMap() { + return this.positions; } @CheckReturnValue @@ -332,19 +304,14 @@ public boolean containsMacroCalls(long exprId) { @CheckReturnValue public CelSource build() { - return new CelSource(this); - } - } - - private static final class LineAndOffset { - - private LineAndOffset(int line, int offset) { - this.line = line; - this.offset = offset; + return new AutoValue_CelSource( + codePoints, + description, + ImmutableList.copyOf(lineOffsets), + ImmutableMap.copyOf(positions), + ImmutableMap.copyOf(macroCalls), + extensions.build()); } - - int line; - int offset; } /** @@ -404,7 +371,7 @@ public enum Component { /** Type checker. Checks that references in an AST are defined and types agree. */ COMPONENT_TYPE_CHECKER, /** Runtime. Evaluates a parsed and optionally checked CEL AST against a context. */ - COMPONENT_RUNTIME; + COMPONENT_RUNTIME } @CheckReturnValue diff --git a/common/src/main/java/dev/cel/common/CelSourceHelper.java b/common/src/main/java/dev/cel/common/CelSourceHelper.java new file mode 100644 index 000000000..13168edbe --- /dev/null +++ b/common/src/main/java/dev/cel/common/CelSourceHelper.java @@ -0,0 +1,95 @@ +// 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. + +package dev.cel.common; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.collect.ImmutableList; +import dev.cel.common.annotations.Internal; +import dev.cel.common.internal.CelCodePointArray; +import java.util.List; +import java.util.Optional; + +/** + * Helper methods for common source handling in CEL. + * + *

CEL Library Internals. Do Not Use. + */ +@Internal +public final class CelSourceHelper { + + /** Extract the snippet text that corresponds to {@code line}. */ + public static Optional getSnippet(CelCodePointArray content, int line) { + checkArgument(line > 0); + ImmutableList lineOffsets = content.lineOffsets(); + int start = findLineOffset(lineOffsets, line); + if (start == -1) { + return Optional.empty(); + } + int end = findLineOffset(lineOffsets, line + 1); + if (end == -1) { + end = content.size(); + } else { + end--; + } + return Optional.of(end != start ? content.slice(start, end).toString() : ""); + } + + /** + * Get the line and column in the source expression text for the given code point {@code offset}. + */ + public static Optional getOffsetLocation( + CelCodePointArray content, int offset) { + checkArgument(offset >= 0); + LineAndOffset lineAndOffset = findLine(content.lineOffsets(), offset); + return Optional.of(CelSourceLocation.of(lineAndOffset.line, offset - lineAndOffset.offset)); + } + + private static LineAndOffset findLine(List lineOffsets, int offset) { + int line = 1; + for (Integer lineOffset : lineOffsets) { + if (lineOffset > offset) { + break; + } + line++; + } + if (line == 1) { + return new LineAndOffset(line, 0); + } + return new LineAndOffset(line, lineOffsets.get(line - 2)); + } + + private static final class LineAndOffset { + private LineAndOffset(int line, int offset) { + this.line = line; + this.offset = offset; + } + + private final int line; + private final int offset; + } + + static int findLineOffset(List lineOffsets, int line) { + if (line == 1) { + return 0; + } + if (line > 1 && line <= lineOffsets.size()) { + return lineOffsets.get(line - 2); + } + return -1; + } + + private CelSourceHelper() {} +} diff --git a/common/src/main/java/dev/cel/common/CelValidationException.java b/common/src/main/java/dev/cel/common/CelValidationException.java index ef136f8a7..18bec2fe6 100644 --- a/common/src/main/java/dev/cel/common/CelValidationException.java +++ b/common/src/main/java/dev/cel/common/CelValidationException.java @@ -15,17 +15,14 @@ package dev.cel.common; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; -import com.google.common.collect.Iterables; import java.util.List; /** Base class for all checked exceptions explicitly thrown by the library during parsing. */ public final class CelValidationException extends CelException { - private static final Joiner JOINER = Joiner.on('\n'); // Truncates all errors beyond this limit in the message. - private static final int MAX_ERRORS_TO_REPORT = 1000; + private static final int MAX_ERRORS_TO_REPORT = 100; private final CelSource source; private final ImmutableList errors; @@ -46,17 +43,14 @@ public CelValidationException(CelSource source, List errors) { private static String safeJoinErrorMessage(CelSource source, List errors) { if (errors.size() <= MAX_ERRORS_TO_REPORT) { - return JOINER.join(Iterables.transform(errors, error -> error.toDisplayString(source))); + return CelIssue.toDisplayString(errors, source); } List truncatedErrors = errors.subList(0, MAX_ERRORS_TO_REPORT); - StringBuilder sb = new StringBuilder(); - JOINER.appendTo( - sb, Iterables.transform(truncatedErrors, error -> error.toDisplayString(source))); - sb.append( - String.format("%n...and %d more errors (truncated)", errors.size() - MAX_ERRORS_TO_REPORT)); - return sb.toString(); + return CelIssue.toDisplayString(truncatedErrors, source) + + String.format( + "%n...and %d more errors (truncated)", errors.size() - MAX_ERRORS_TO_REPORT); } /** 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 a7e6eceb8..61152c493 100644 --- a/common/src/main/java/dev/cel/common/CelValidationResult.java +++ b/common/src/main/java/dev/cel/common/CelValidationResult.java @@ -17,14 +17,12 @@ import static com.google.common.collect.ImmutableList.toImmutableList; import static java.util.Comparator.comparing; -import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; -import com.google.common.collect.Iterables; import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.Immutable; import com.google.errorprone.annotations.InlineMe; import dev.cel.common.annotations.Internal; -import org.jspecify.nullness.Nullable; +import org.jspecify.annotations.Nullable; /** * CelValidationResult encapsulates the {@code CelAbstractSyntaxTree} and {@code CelIssue} set which @@ -33,8 +31,6 @@ @Immutable public final class CelValidationResult { - private static final Joiner JOINER = Joiner.on('\n'); - @SuppressWarnings("Immutable") private final @Nullable Throwable failure; @@ -115,7 +111,7 @@ public ImmutableList getAllIssues() { /** Convert all issues to a human-readable string. */ public String getIssueString() { - return JOINER.join(Iterables.transform(issues, iss -> iss.toDisplayString(source))); + return CelIssue.toDisplayString(issues, source); } /** @@ -131,7 +127,7 @@ public String getDebugString() { /** Convert the {@code CelIssue}s with {@code ERROR} severity to an error string. */ public String getErrorString() { - return JOINER.join(Iterables.transform(getErrors(), error -> error.toDisplayString(source))); + return CelIssue.toDisplayString(getErrors(), source); } private static boolean issueIsError(CelIssue iss) { diff --git a/common/src/main/java/dev/cel/common/CelVarDecl.java b/common/src/main/java/dev/cel/common/CelVarDecl.java index 9c3930c3a..0543ed225 100644 --- a/common/src/main/java/dev/cel/common/CelVarDecl.java +++ b/common/src/main/java/dev/cel/common/CelVarDecl.java @@ -14,13 +14,9 @@ package dev.cel.common; -import dev.cel.expr.Decl; -import dev.cel.expr.Decl.IdentDecl; import com.google.auto.value.AutoValue; -import com.google.common.annotations.VisibleForTesting; import com.google.errorprone.annotations.CheckReturnValue; import dev.cel.common.types.CelType; -import dev.cel.common.types.CelTypes; /** Abstract representation of a CEL variable declaration. */ @AutoValue @@ -37,13 +33,4 @@ public abstract class CelVarDecl { public static CelVarDecl newVarDeclaration(String name, CelType type) { return new AutoValue_CelVarDecl(name, type); } - - /** Converts a {@link CelVarDecl} to a protobuf equivalent form {@code Decl} */ - @VisibleForTesting - public static Decl celVarToDecl(CelVarDecl varDecl) { - return Decl.newBuilder() - .setName(varDecl.name()) - .setIdent(IdentDecl.newBuilder().setType(CelTypes.celTypeToType(varDecl.type()))) - .build(); - } } diff --git a/common/src/main/java/dev/cel/common/ExprFeatures.java b/common/src/main/java/dev/cel/common/ExprFeatures.java deleted file mode 100644 index 2bc36b6e6..000000000 --- a/common/src/main/java/dev/cel/common/ExprFeatures.java +++ /dev/null @@ -1,165 +0,0 @@ -// 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.common; - -import com.google.common.collect.ImmutableSet; - -/** - * ExprFeatures are flags that alter how the CEL Java parser, checker, and interpreter behave. - * - * @deprecated Use {@code CelOptions} instead. - */ -@Deprecated -public enum ExprFeatures { - - /** - * Require overloads to resolve (narrow to a single candidate) during type checking. - * - *

This eliminates run-time overload dispatch and avoids implicit coercions of the result type - * to type dyn. - */ - COMPILE_TIME_OVERLOAD_RESOLUTION, - - /** - * When enabled use Java equality for (in)equality tests. - * - *

This feature is how the legacy CEL-Java APIs were originally configured, and in most cases - * will yield identical results to CEL equality, with the exception that equality between - * well-known protobuf types (wrapper types, {@code protobuf.Value}, {@code protobuf.Any}) may not - * compare correctly to simple and aggregate CEL types. - * - *

Additionally, Java equality across numeric types such as {@code double} and {@code long}, - * will be trivially false, whereas CEL equality will compare the values as though they exist on a - * continuous number line. - */ - LEGACY_JAVA_EQUALITY, - - /** - * During type checking require list-, and map literals to be type-homogeneous in their element-, - * key-, and value types, respectively. - * - *

Without this flag one can use type-mismatched elements, keys, and values, and the type - * checker will implicitly coerce them to type dyn. - */ - HOMOGENEOUS_LITERALS, - - /** - * Treat regex {@code matches} calls as substring (unanchored) match patterns. - * - *

The default treatment for pattern matching within RE2 is full match within Java; however, - * the CEL standarda specifies that the matches() function is a substring match. - */ - REGEX_PARTIAL_MATCH, - - /** - * Check for use of reserved identifiers during parsing. - * - *

See the language - * spec for a list of reserved identifiers. - */ - RESERVED_IDS, - - /** - * Retain all invocations of unary '-' and '!' that occur in source in the abstract syntax. - * - *

By default the parser collapses towers of repeated unary '-' and '!' into zero or one - * instance by assuming these operators to be inverses of themselves. This behavior may not always - * be desirable. - */ - RETAIN_REPEATED_UNARY_OPERATORS, - - /** - * Retain the original grouping of logical connectives '&&' and '||' without attempting to - * rebalance them in the abstract syntax. - * - *

The default rebalancing can reduce the overall nesting depth of the generated protos - * representing abstract syntax, but it relies on associativity of the operations themselves. This - * behavior may not always be desirable. - */ - RETAIN_UNBALANCED_LOGICAL_EXPRESSIONS, - - /** - * Treat unsigned integers as unsigned when doing arithmetic and comparisons. - * - *

Prior to turning on this feature, attempts to perform arithmetic or comparisons on unsigned - * integers larger than 2^63-1 may result in a runtime exception in the form of an {@link - * java.lang.IllegalArgumentException}. - */ - UNSIGNED_COMPARISON_AND_ARITHMETIC_IS_UNSIGNED, - - /** - * Throw errors when ints or uints wrap. - * - *

Prior to this feature, int and uint arithmetic wrapped, i.e. was coerced into range via mod - * 2^64. The spec settled on throwing an error instead. Note that this makes arithmetic non- - * associative. - */ - ERROR_ON_WRAP, - - /** Error on duplicate keys in map literals. */ - ERROR_ON_DUPLICATE_KEYS, - - /** Populate macro_calls map in source_info with macro calls parsed from the expression. */ - POPULATE_MACRO_CALLS, - - /** - * Enable the timestamp from epoch overload. This will automatically move to CURRENT after a two - * month notice to consumers. - * - *

TODO: Remove this feature once it has been auto-enabled. - */ - ENABLE_TIMESTAMP_EPOCH, - - /** - * Enable numeric comparisons across types. This will automatically move to CURRENT after a two - * month notice to consumers. - * - *

TODO: Remove this feature once it has been auto-enabled. - */ - ENABLE_HETEROGENEOUS_NUMERIC_COMPARISONS, - - /** - * Enable the using of {@code UnsignedLong} values in place of {@code Long} values for unsigned - * integers. - * - *

Note, users must be careful not to supply {@code Long} values when {@code UnsignedLong} - * values are intended. - */ - ENABLE_UNSIGNED_LONGS, - - /** - * Enable proto differencer based equality for messages. This is in place of Message.equals() - * which has a slightly different behavior. - */ - PROTO_DIFFERENCER_EQUALITY, - - /** - * Enables the usage of namespaced functions and identifiers. This causes the type-checker to - * rewrite the AST to support namespacing. - */ - ENABLE_NAMESPACED_DECLARATIONS; - - /** Feature flags that enable the current best practices for CEL. */ - public static final ImmutableSet CURRENT = - ImmutableSet.of( - REGEX_PARTIAL_MATCH, - RESERVED_IDS, - UNSIGNED_COMPARISON_AND_ARITHMETIC_IS_UNSIGNED, - ENABLE_NAMESPACED_DECLARATIONS, - ERROR_ON_WRAP, - ERROR_ON_DUPLICATE_KEYS); - - public static final ImmutableSet LEGACY = ImmutableSet.of(LEGACY_JAVA_EQUALITY); -} diff --git a/parser/src/main/java/dev/cel/parser/Operator.java b/common/src/main/java/dev/cel/common/Operator.java similarity index 79% rename from parser/src/main/java/dev/cel/parser/Operator.java rename to common/src/main/java/dev/cel/common/Operator.java index 8ae1b5461..c49c78267 100644 --- a/parser/src/main/java/dev/cel/parser/Operator.java +++ b/common/src/main/java/dev/cel/common/Operator.java @@ -1,4 +1,4 @@ -// Copyright 2022 Google LLC +// 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. @@ -12,17 +12,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -package dev.cel.parser; +package dev.cel.common; import com.google.common.collect.ImmutableMap; -import dev.cel.common.ast.CelExpr; import java.util.Objects; import java.util.Optional; /** * Package-private enumeration of Common Expression Language operators. * - *

Equivalent to https://pkg.go.dev/github.com/google/cel-go/common/operators. + *

Equivalent to operators. */ public enum Operator { CONDITIONAL("_?_:_"), @@ -45,7 +45,9 @@ public enum Operator { HAS("has"), ALL("all"), EXISTS("exists"), + @Deprecated // Prefer EXISTS_ONE_NEW. EXISTS_ONE("exists_one"), + EXISTS_ONE_NEW("existsOne"), MAP("map"), FILTER("filter"), NOT_STRICTLY_FALSE("@not_strictly_false"), @@ -96,29 +98,42 @@ String getSymbol() { .buildOrThrow(); /** Lookup an operator by its unmangled name, as used with the source text of an expression. */ - static Optional find(String text) { + public static Optional find(String text) { return Optional.ofNullable(OPERATORS.get(text)); } private static final ImmutableMap REVERSE_OPERATORS = ImmutableMap.builder() .put(ADD.getFunction(), ADD) + .put(ALL.getFunction(), ALL) + .put(CONDITIONAL.getFunction(), CONDITIONAL) .put(DIVIDE.getFunction(), DIVIDE) .put(EQUALS.getFunction(), EQUALS) + .put(EXISTS.getFunction(), EXISTS) + .put(EXISTS_ONE.getFunction(), EXISTS_ONE) + .put(EXISTS_ONE_NEW.getFunction(), EXISTS_ONE_NEW) + .put(FILTER.getFunction(), FILTER) .put(GREATER.getFunction(), GREATER) .put(GREATER_EQUALS.getFunction(), GREATER_EQUALS) + .put(HAS.getFunction(), HAS) .put(IN.getFunction(), IN) + .put(INDEX.getFunction(), INDEX) .put(LESS.getFunction(), LESS) .put(LESS_EQUALS.getFunction(), LESS_EQUALS) .put(LOGICAL_AND.getFunction(), LOGICAL_AND) .put(LOGICAL_NOT.getFunction(), LOGICAL_NOT) .put(LOGICAL_OR.getFunction(), LOGICAL_OR) + .put(MAP.getFunction(), MAP) .put(MODULO.getFunction(), MODULO) .put(MULTIPLY.getFunction(), MULTIPLY) .put(NEGATE.getFunction(), NEGATE) .put(NOT_EQUALS.getFunction(), NOT_EQUALS) - .put(SUBTRACT.getFunction(), SUBTRACT) + .put(NOT_STRICTLY_FALSE.getFunction(), NOT_STRICTLY_FALSE) .put(OLD_IN.getFunction(), OLD_IN) + .put(OLD_NOT_STRICTLY_FALSE.getFunction(), OLD_NOT_STRICTLY_FALSE) + .put(OPTIONAL_INDEX.getFunction(), OPTIONAL_INDEX) + .put(OPTIONAL_SELECT.getFunction(), OPTIONAL_SELECT) + .put(SUBTRACT.getFunction(), SUBTRACT) .buildOrThrow(); // precedence of the operator, where the higher value means higher. @@ -168,8 +183,8 @@ static Optional find(String text) { .put(MODULO.getFunction(), "%") .buildOrThrow(); - /** Lookup an operator by its mangled name, as used within the AST. */ - static Optional findReverse(String op) { + /** Lookup an operator by its mangled name (ex: _&&_), as used within the AST. */ + public static Optional findReverse(String op) { return Optional.ofNullable(REVERSE_OPERATORS.get(op)); } @@ -181,26 +196,23 @@ static Optional findReverseBinaryOperator(String op) { return Optional.ofNullable(REVERSE_OPERATORS.get(op)); } - static int lookupPrecedence(String op) { + /** + * Returns the precedence of the operator. + * + * @return Integer value describing precedence. Higher value means higher precedence. Returns 0 if + * the operator is not found. + */ + public static int lookupPrecedence(String op) { return PRECEDENCES.getOrDefault(op, 0); } - static Optional lookupUnaryOperator(String op) { + /** Looks up the associated unary operator based on its function name (Ex: !_ -> !) */ + public static Optional lookupUnaryOperator(String op) { return Optional.ofNullable(UNARY_OPERATORS.get(op)); } - static Optional lookupBinaryOperator(String op) { + /** Looks up the associated binary operator based on its function name (Ex: _||_ -> ||) */ + public static Optional lookupBinaryOperator(String op) { return Optional.ofNullable(BINARY_OPERATORS.get(op)); } - - static boolean isOperatorLowerPrecedence(String op, CelExpr expr) { - if (!expr.exprKind().getKind().equals(CelExpr.ExprKind.Kind.CALL)) { - return false; - } - return lookupPrecedence(op) < lookupPrecedence(expr.call().function()); - } - - static boolean isOperatorLeftRecursive(String op) { - return !op.equals(LOGICAL_AND.getFunction()) && !op.equals(LOGICAL_OR.getFunction()); - } } diff --git a/common/src/main/java/dev/cel/common/Source.java b/common/src/main/java/dev/cel/common/Source.java new file mode 100644 index 000000000..2d43a9581 --- /dev/null +++ b/common/src/main/java/dev/cel/common/Source.java @@ -0,0 +1,52 @@ +// 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. + +package dev.cel.common; + +import com.google.common.collect.ImmutableMap; +import dev.cel.common.annotations.Internal; +import dev.cel.common.internal.CelCodePointArray; +import java.util.Optional; + +/** + * Common interface definition for source properties. + * + *

CEL Library Internals. Do Not Use. Consumers should instead use the canonical implementations + * such as CelSource. + */ +@Internal +public interface Source { + + /** Gets the original textual content of this source, represented in an array of code points. */ + CelCodePointArray getContent(); + + /** + * Gets the description of this source that may optionally be set (example: location of the file + * containing the source). + */ + String getDescription(); + + /** + * Gets the map of each parsed node ID (ex: expression ID, policy ID) to their source positions. + */ + ImmutableMap getPositionsMap(); + + /** + * Get the text from the source text that corresponds to {@code line}. Snippets are split based on + * the newline ('\n'). + * + * @param line the line number starting from 1. + */ + Optional getSnippet(int line); +} diff --git a/common/src/main/java/dev/cel/common/annotations/BUILD.bazel b/common/src/main/java/dev/cel/common/annotations/BUILD.bazel index 434eede90..6188b7e96 100644 --- a/common/src/main/java/dev/cel/common/annotations/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/annotations/BUILD.bazel @@ -1,3 +1,5 @@ +load("@rules_java//java:defs.bzl", "java_library") + package( default_applicable_licenses = [ "//:license", @@ -9,12 +11,15 @@ package( # keep sorted ANNOTATION_SOURCES = [ + "Beta.java", + "Generated.java", "Internal.java", ] java_library( name = "annotations", srcs = ANNOTATION_SOURCES, + # used_by_android tags = [ ], ) diff --git a/common/src/main/java/dev/cel/common/annotations/Beta.java b/common/src/main/java/dev/cel/common/annotations/Beta.java new file mode 100644 index 000000000..dc2ed809b --- /dev/null +++ b/common/src/main/java/dev/cel/common/annotations/Beta.java @@ -0,0 +1,38 @@ +// 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.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates a public API that can change at any time, and has no guarantee of API stability and + * backward-compatibility. + * + *

Note: This annotation is intended only for CEL library code. Users should not attach this + * annotation to their own code. + */ +@Retention(RetentionPolicy.CLASS) +@Target({ + ElementType.ANNOTATION_TYPE, + ElementType.CONSTRUCTOR, + ElementType.FIELD, + ElementType.METHOD, + ElementType.PACKAGE, + ElementType.TYPE +}) +public @interface Beta {} diff --git a/common/src/main/java/dev/cel/common/annotations/Generated.java b/common/src/main/java/dev/cel/common/annotations/Generated.java new file mode 100644 index 000000000..74c782d80 --- /dev/null +++ b/common/src/main/java/dev/cel/common/annotations/Generated.java @@ -0,0 +1,40 @@ +// 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.annotations; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.CONSTRUCTOR; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.LOCAL_VARIABLE; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PACKAGE; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Annotates that the source code is generated by CEL. + * + *

Note: This annotation is intended only for CEL library code. Users should not attach this + * annotation to their own code. + */ +@Retention(SOURCE) +@Target({PACKAGE, TYPE, ANNOTATION_TYPE, METHOD, CONSTRUCTOR, FIELD, LOCAL_VARIABLE, PARAMETER}) +public @interface Generated { + String[] value(); +} 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 ec79dbed2..3fc709a07 100644 --- a/common/src/main/java/dev/cel/common/ast/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/ast/BUILD.bazel @@ -1,3 +1,6 @@ +load("@rules_java//java:defs.bzl", "java_library") +load("//:cel_android_rules.bzl", "cel_android_library") + package( default_applicable_licenses = [ "//:license", @@ -13,6 +16,7 @@ AST_SOURCES = [ "CelExpr.java", "CelExprFormatter.java", "CelReference.java", + "Expression.java", ] # keep sorted @@ -31,6 +35,12 @@ EXPR_FACTORY_SOURCES = [ "CelExprIdGeneratorFactory.java", ] +# keep sorted +MUTABLE_EXPR_SOURCES = [ + "CelMutableExpr.java", + "CelMutableExprConverter.java", +] + java_library( name = "ast", srcs = AST_SOURCES, @@ -39,10 +49,11 @@ java_library( deps = [ "//:auto_value", "//common/annotations", + "//common/values", + "//common/values:cel_byte_string", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", - "@maven//:org_jspecify_jspecify", ], ) @@ -53,8 +64,28 @@ java_library( ], deps = [ ":ast", - "@cel_spec//proto/cel/expr:expr_java_proto", + "//common/values", + "//common/values:cel_byte_string", + "@cel_spec//proto/cel/expr:checked_java_proto", + "@cel_spec//proto/cel/expr:syntax_java_proto", "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + ], +) + +cel_android_library( + name = "expr_converter_android", + srcs = EXPR_CONVERTER_SOURCES, + tags = [ + ], + deps = [ + ":ast_android", + "//common/values:cel_byte_string", + "//common/values:values_android", + "@cel_spec//proto/cel/expr:checked_java_proto_lite", + "@cel_spec//proto/cel/expr:syntax_java_proto_lite", + "@maven_android//:com_google_guava_guava", + "@maven_android//:com_google_protobuf_protobuf_javalite", ], ) @@ -65,8 +96,11 @@ java_library( ], deps = [ ":ast", + "//common/values", + "//common/values:cel_byte_string", "@com_google_googleapis//google/api/expr/v1alpha1:expr_java_proto", "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", ], ) @@ -77,7 +111,7 @@ java_library( ], deps = [ ":ast", - "//common", + "//common:cel_ast", ], ) @@ -89,23 +123,34 @@ java_library( deps = [ ":ast", "//common/annotations", + "//common/values:cel_byte_string", "@maven//:com_google_guava_guava", - "@maven//:com_google_protobuf_protobuf_java", ], ) java_library( - name = "expr_util", - srcs = ["CelExprUtil.java"], + name = "mutable_expr", + srcs = MUTABLE_EXPR_SOURCES, tags = [ ], deps = [ ":ast", - "//bundle:cel", - "//common", - "//common:compiler_common", - "//runtime", - "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", ], ) + +cel_android_library( + name = "ast_android", + srcs = AST_SOURCES, + tags = [ + ], + deps = [ + "//:auto_value", + "//common/annotations", + "//common/values:cel_byte_string", + "//common/values:values_android", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", + "@maven_android//:com_google_protobuf_protobuf_javalite", + ], +) diff --git a/common/src/main/java/dev/cel/common/ast/CelConstant.java b/common/src/main/java/dev/cel/common/ast/CelConstant.java index f981225e3..c27f4ff8f 100644 --- a/common/src/main/java/dev/cel/common/ast/CelConstant.java +++ b/common/src/main/java/dev/cel/common/ast/CelConstant.java @@ -19,11 +19,13 @@ import com.google.common.collect.ImmutableSet; import com.google.common.primitives.UnsignedLong; import com.google.errorprone.annotations.Immutable; +import com.google.errorprone.annotations.InlineMe; import com.google.protobuf.ByteString; import com.google.protobuf.Duration; -import com.google.protobuf.NullValue; import com.google.protobuf.Timestamp; import dev.cel.common.annotations.Internal; +import dev.cel.common.values.CelByteString; +import dev.cel.common.values.NullValue; /** * Represents a primitive literal. @@ -42,7 +44,7 @@ public abstract class CelConstant { UnsignedLong.class, Double.class, String.class, - ByteString.class); + CelByteString.class); /** Represents the type of the Constant */ public enum Kind { @@ -92,7 +94,7 @@ public abstract static class CelConstantNotSet {} public abstract String stringValue(); - public abstract ByteString bytesValue(); + public abstract CelByteString bytesValue(); /** * @deprecated Do not use. Timestamp is no longer built-in CEL type. @@ -134,10 +136,46 @@ public static CelConstant ofValue(String value) { return AutoOneOf_CelConstant.stringValue(value); } - public static CelConstant ofValue(ByteString value) { + public static CelConstant ofValue(CelByteString value) { return AutoOneOf_CelConstant.bytesValue(value); } + /** + * @deprecated Use native type equivalent {@link #ofValue(NullValue)} instead. + */ + @InlineMe( + replacement = "CelConstant.ofValue(NullValue.NULL_VALUE)", + imports = {"dev.cel.common.ast.CelConstant", "dev.cel.common.values.NullValue"}) + @Deprecated + public static CelConstant ofValue(com.google.protobuf.NullValue unused) { + return ofValue(NullValue.NULL_VALUE); + } + + /** + * @deprecated Use native type equivalent {@link #ofValue(CelByteString)} instead. + */ + @Deprecated + public static CelConstant ofValue(ByteString value) { + CelByteString celByteString = CelByteString.of(value.toByteArray()); + return ofValue(celByteString); + } + + /** + * @deprecated Do not use. Duration is no longer built-in CEL type. + */ + @Deprecated + public static CelConstant ofValue(Duration value) { + return AutoOneOf_CelConstant.durationValue(value); + } + + /** + * @deprecated Do not use. Timestamp is no longer built-in CEL type. + */ + @Deprecated + public static CelConstant ofValue(Timestamp value) { + return AutoOneOf_CelConstant.timestampValue(value); + } + /** Checks whether the provided Java object is a valid CelConstant value. */ public static boolean isConstantValue(Object value) { return CONSTANT_CLASSES.contains(value.getClass()); @@ -163,26 +201,10 @@ public static CelConstant ofObjectValue(Object value) { return ofValue((double) value); } else if (value instanceof String) { return ofValue((String) value); - } else if (value instanceof ByteString) { - return ofValue((ByteString) value); + } else if (value instanceof CelByteString) { + return ofValue((CelByteString) value); } throw new IllegalArgumentException("Value is not a CelConstant: " + value); } - - /** - * @deprecated Do not use. Duration is no longer built-in CEL type. - */ - @Deprecated - public static CelConstant ofValue(Duration value) { - return AutoOneOf_CelConstant.durationValue(value); - } - - /** - * @deprecated Do not use. Timestamp is no longer built-in CEL type. - */ - @Deprecated - public static CelConstant ofValue(Timestamp value) { - return AutoOneOf_CelConstant.timestampValue(value); - } } 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 c4e53906a..cac968686 100644 --- a/common/src/main/java/dev/cel/common/ast/CelExpr.java +++ b/common/src/main/java/dev/cel/common/ast/CelExpr.java @@ -23,112 +23,108 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.CheckReturnValue; import com.google.errorprone.annotations.Immutable; -import dev.cel.common.annotations.Internal; -import dev.cel.common.ast.CelExpr.ExprKind.Kind; import java.util.ArrayList; import java.util.Arrays; -import java.util.List; import java.util.Optional; /** - * An abstract representation of a common expression. + * An abstract representation of a common expression. Refer to {@link Expression} for details. * *

This is the native type equivalent of Expr message in syntax.proto. - * - *

Expressions are abstractly represented as a collection of identifiers, select statements, - * function calls, literals, and comprehensions. All operators with the exception of the '.' - * operator are modelled as function calls. This makes it easy to represent new operators into the - * existing AST. - * - *

All references within expressions must resolve to a [Decl][] provided at type-check for an - * expression to be valid. A reference may either be a bare identifier `name` or a qualified - * identifier `google.api.name`. References may either refer to a value or a function declaration. - * - *

For example, the expression `google.api.name.startsWith('expr')` references the declaration - * `google.api.name` within a [Expr.Select][] expression, and the function declaration `startsWith`. */ @AutoValue -@Internal +@AutoValue.CopyAnnotations @Immutable -public abstract class CelExpr { +@SuppressWarnings("unchecked") // Class ensures only the super type is used +public abstract class CelExpr implements Expression { - /** - * Required. An id assigned to this node by the parser which is unique in a given expression tree. - * This is used to associate type information and other attributes to a node in the parse tree. - */ + @Override public abstract long id(); /** Represents the variant of the expression. */ public abstract ExprKind exprKind(); + @Override + public ExprKind.Kind getKind() { + return exprKind().getKind(); + } + /** - * Gets the underlying constant expression. + * {@inheritDoc} * * @throws UnsupportedOperationException if expression is not {@link Kind#CONSTANT}. */ + @Override public CelConstant constant() { return exprKind().constant(); } /** - * Gets the underlying identifier expression. + * {@inheritDoc} * * @throws UnsupportedOperationException if expression is not {@link Kind#IDENT}. */ + @Override public CelIdent ident() { return exprKind().ident(); } /** - * Gets the underlying select expression. + * {@inheritDoc} * * @throws UnsupportedOperationException if expression is not {@link Kind#SELECT}. */ + @Override public CelSelect select() { return exprKind().select(); } /** - * Gets the underlying call expression. + * {@inheritDoc} * * @throws UnsupportedOperationException if expression is not {@link Kind#CALL}. */ + @Override public CelCall call() { return exprKind().call(); } /** - * Gets the underlying createList expression. + * {@inheritDoc} * - * @throws UnsupportedOperationException if expression is not {@link Kind#CREATE_LIST}. + * @throws UnsupportedOperationException if expression is not {@link Kind#LIST}. */ - public CelCreateList createList() { - return exprKind().createList(); + @Override + public CelList list() { + return exprKind().list(); } /** - * Gets the underlying createStruct expression. + * {@inheritDoc} * - * @throws UnsupportedOperationException if expression is not {@link Kind#CREATE_STRUCT}. + * @throws UnsupportedOperationException if expression is not {@link Kind#STRUCT}. */ - public CelCreateStruct createStruct() { - return exprKind().createStruct(); + @Override + public CelStruct struct() { + return exprKind().struct(); } /** - * Gets the underlying createMap expression. + * {@inheritDoc} * - * @throws UnsupportedOperationException if expression is not {@link Kind#createMap}. + * @throws UnsupportedOperationException if expression is not {@link Kind#MAP}. */ - public CelCreateMap createMap() { - return exprKind().createMap(); + @Override + public CelMap map() { + return exprKind().map(); } /** - * Gets the underlying comprehension expression. + * {@inheritDoc} * * @throws UnsupportedOperationException if expression is not {@link Kind#COMPREHENSION}. */ + @Override public CelComprehension comprehension() { return exprKind().comprehension(); } @@ -174,33 +170,33 @@ public CelCall callOrDefault() { } /** - * Gets the underlying createList expression or a default instance of one if expression is not - * {@link Kind#CREATE_LIST}. + * Gets the underlying list expression or a default instance of one if expression is not {@link + * Kind#LIST}. */ - public CelCreateList createListOrDefault() { - return exprKind().getKind().equals(ExprKind.Kind.CREATE_LIST) - ? exprKind().createList() - : CelCreateList.newBuilder().build(); + public CelList listOrDefault() { + return exprKind().getKind().equals(ExprKind.Kind.LIST) + ? exprKind().list() + : CelList.newBuilder().build(); } /** - * Gets the underlying createStruct expression or a default instance of one if expression is not - * {@link Kind#CREATE_STRUCT}. + * Gets the underlying struct expression or a default instance of one if expression is not {@link + * Kind#STRUCT}. */ - public CelCreateStruct createStructOrDefault() { - return exprKind().getKind().equals(ExprKind.Kind.CREATE_STRUCT) - ? exprKind().createStruct() - : CelCreateStruct.newBuilder().build(); + public CelStruct structOrDefault() { + return exprKind().getKind().equals(ExprKind.Kind.STRUCT) + ? exprKind().struct() + : CelStruct.newBuilder().build(); } /** - * Gets the underlying createMap expression or a default instance of one if expression is not - * {@link Kind#CREATE_MAP}. + * Gets the underlying map expression or a default instance of one if expression is not {@link + * Kind#MAP}. */ - public CelCreateMap createMapOrDefault() { - return exprKind().getKind().equals(ExprKind.Kind.CREATE_MAP) - ? exprKind().createMap() - : CelCreateMap.newBuilder().build(); + public CelMap mapOrDefault() { + return exprKind().getKind().equals(ExprKind.Kind.MAP) + ? exprKind().map() + : CelMap.newBuilder().build(); } /** @@ -262,30 +258,30 @@ public CelCall call() { } /** - * Gets the underlying createList expression. + * Gets the underlying list expression. * - * @throws UnsupportedOperationException if expression is not {@link Kind#CREATE_LIST}. + * @throws UnsupportedOperationException if expression is not {@link Kind#LIST}. */ - public CelCreateList createList() { - return exprKind().createList(); + public CelList list() { + return exprKind().list(); } /** - * Gets the underlying createStruct expression. + * Gets the underlying struct expression. * - * @throws UnsupportedOperationException if expression is not {@link Kind#CREATE_STRUCT}. + * @throws UnsupportedOperationException if expression is not {@link Kind#STRUCT}. */ - public CelCreateStruct createStruct() { - return exprKind().createStruct(); + public CelStruct struct() { + return exprKind().struct(); } /** - * Gets the underlying createMap expression. + * Gets the underlying map expression. * - * @throws UnsupportedOperationException if expression is not {@link Kind#createMap}. + * @throws UnsupportedOperationException if expression is not {@link Kind#MAP}. */ - public CelCreateMap createMap() { - return exprKind().createMap(); + public CelMap map() { + return exprKind().map(); } /** @@ -297,34 +293,42 @@ public CelComprehension comprehension() { return exprKind().comprehension(); } + @CanIgnoreReturnValue public Builder setConstant(CelConstant constant) { return setExprKind(AutoOneOf_CelExpr_ExprKind.constant(constant)); } + @CanIgnoreReturnValue public Builder setIdent(CelIdent ident) { return setExprKind(AutoOneOf_CelExpr_ExprKind.ident(ident)); } + @CanIgnoreReturnValue public Builder setCall(CelCall call) { return setExprKind(AutoOneOf_CelExpr_ExprKind.call(call)); } + @CanIgnoreReturnValue public Builder setSelect(CelSelect select) { return setExprKind(AutoOneOf_CelExpr_ExprKind.select(select)); } - public Builder setCreateList(CelCreateList createList) { - return setExprKind(AutoOneOf_CelExpr_ExprKind.createList(createList)); + @CanIgnoreReturnValue + public Builder setList(CelList list) { + return setExprKind(AutoOneOf_CelExpr_ExprKind.list(list)); } - public Builder setCreateStruct(CelCreateStruct createStruct) { - return setExprKind(AutoOneOf_CelExpr_ExprKind.createStruct(createStruct)); + @CanIgnoreReturnValue + public Builder setStruct(CelStruct struct) { + return setExprKind(AutoOneOf_CelExpr_ExprKind.struct(struct)); } - public Builder setCreateMap(CelCreateMap createMap) { - return setExprKind(AutoOneOf_CelExpr_ExprKind.createMap(createMap)); + @CanIgnoreReturnValue + public Builder setMap(CelMap map) { + return setExprKind(AutoOneOf_CelExpr_ExprKind.map(map)); } + @CanIgnoreReturnValue public Builder setComprehension(CelComprehension comprehension) { return setExprKind(AutoOneOf_CelExpr_ExprKind.comprehension(comprehension)); } @@ -353,9 +357,9 @@ public enum Kind { IDENT, SELECT, CALL, - CREATE_LIST, - CREATE_STRUCT, - CREATE_MAP, + LIST, + STRUCT, + MAP, COMPREHENSION, } @@ -371,11 +375,11 @@ public enum Kind { public abstract CelCall call(); - public abstract CelCreateList createList(); + public abstract CelList list(); - public abstract CelCreateStruct createStruct(); + public abstract CelStruct struct(); - public abstract CelCreateMap createMap(); + public abstract CelMap map(); public abstract CelComprehension comprehension(); } @@ -393,12 +397,9 @@ public abstract static class CelNotSet {} /** An identifier expression. e.g. `request`. */ @AutoValue @Immutable - public abstract static class CelIdent { - /** - * Required. Holds a single, unqualified identifier, possibly preceded by a '.'. - * - *

Qualified names are represented by the [Expr.Select][] expression. - */ + public abstract static class CelIdent implements Ident { + + @Override public abstract String name(); /** Builder for CelIdent. */ @@ -421,29 +422,15 @@ public static Builder newBuilder() { /** A field selection expression. e.g. `request.auth`. */ @AutoValue @Immutable - public abstract static class CelSelect { + public abstract static class CelSelect implements Expression.Select { - /** - * Required. The target of the selection expression. - * - *

For example, in the select expression `request.auth`, the `request` portion of the - * expression is the `operand`. - */ + @Override public abstract CelExpr operand(); - /** - * Required. The name of the field to select. - * - *

For example, in the select expression `request.auth`, the `auth` portion of the expression - * would be the `field`. - */ + @Override public abstract String field(); - /** - * Whether the select is to be interpreted as a field presence test. - * - *

This results from the macro `has(request.auth)`. - */ + @Override public abstract boolean testOnly(); /** Builder for CelSelect. */ @@ -475,31 +462,24 @@ public static Builder newBuilder() { } } - /** - * A call expression, including calls to predefined functions and operators. - * - *

For example, `value == 10`, `size(map_value)`. - */ + /** A call expression. See {@link Expression.Call} */ @AutoValue @Immutable - public abstract static class CelCall { + public abstract static class CelCall implements Expression.Call { - /** - * The target of a method call-style expression. - * - *

For example, `x` in `x.f()`. - */ + @Override public abstract Optional target(); - /** Required. The name of the function or method being called. */ + @Override public abstract String function(); + @Override public abstract ImmutableList args(); /** Builder for CelCall. */ @AutoValue.Builder public abstract static class Builder { - private List mutableArgs = new ArrayList<>(); + private java.util.List mutableArgs = new ArrayList<>(); // Not public. This only exists to make AutoValue.Builder work. abstract ImmutableList args(); @@ -525,6 +505,13 @@ public ImmutableList getArgsBuilders() { return mutableArgs.stream().map(CelExpr::toBuilder).collect(toImmutableList()); } + @CanIgnoreReturnValue + public Builder clearArgs() { + mutableArgs.clear(); + return this; + } + + @CanIgnoreReturnValue public Builder setArg(int index, CelExpr arg) { checkNotNull(arg); mutableArgs.set(index, arg); @@ -572,30 +559,20 @@ public static Builder newBuilder() { } } - /** - * A list creation expression. - * - *

Lists may either be homogenous, e.g. `[1, 2, 3]`, or heterogeneous, e.g. `dyn([1, 'hello', - * 2.0])` - */ + /** A list creation expression. See {@link List} */ @AutoValue @Immutable - public abstract static class CelCreateList { - /** The elements part of the list */ + public abstract static class CelList implements List { + @Override public abstract ImmutableList elements(); - /** - * The indices within the elements list which are marked as optional elements. - * - *

When an optional-typed value is present, the value it contains is included in the list. If - * the optional-typed value is absent, the list element is omitted from the CreateList result. - */ + @Override public abstract ImmutableList optionalIndices(); - /** Builder for CelCreateList. */ + /** Builder for CelList. */ @AutoValue.Builder public abstract static class Builder { - private List mutableElements = new ArrayList<>(); + private java.util.List mutableElements = new ArrayList<>(); // Not public. This only exists to make AutoValue.Builder work. abstract ImmutableList elements(); @@ -651,10 +628,10 @@ public Builder addOptionalIndices(Iterable indices) { } // Not public due to overridden build logic. - abstract CelCreateList autoBuild(); + abstract CelList autoBuild(); @CheckReturnValue - public CelCreateList build() { + public CelList build() { setElements(ImmutableList.copyOf(mutableElements)); return autoBuild(); } @@ -670,77 +647,70 @@ public Builder toBuilder() { } public static Builder newBuilder() { - return new AutoValue_CelExpr_CelCreateList.Builder(); + return new AutoValue_CelExpr_CelList.Builder(); } } - /** - * A message creation expression. - * - *

Messages are constructed with a type name and composed of field ids: `types.MyType{field_id: - * 'value'}`. - */ + /** A message creation expression. See {@link Expression.Struct} */ @AutoValue @Immutable - public abstract static class CelCreateStruct { - /** The type name of the message to be created, empty when creating map literals. */ + public abstract static class CelStruct implements Expression.Struct { + @Override public abstract String messageName(); - /** The entries in the creation expression. */ - public abstract ImmutableList entries(); + @Override + public abstract ImmutableList entries(); - /** Builder for CelCreateStruct. */ + /** Builder for CelStruct. */ @AutoValue.Builder public abstract static class Builder { - private List mutableEntries = new ArrayList<>(); + private java.util.List mutableEntries = new ArrayList<>(); // Not public. This only exists to make AutoValue.Builder work. - abstract ImmutableList entries(); + abstract ImmutableList entries(); @CanIgnoreReturnValue public abstract Builder setMessageName(String value); // Not public. This only exists to make AutoValue.Builder work. @CanIgnoreReturnValue - abstract Builder setEntries(ImmutableList entries); + abstract Builder setEntries(ImmutableList entries); /** Returns an immutable copy of the current mutable entries present in the builder. */ - public ImmutableList getEntries() { + public ImmutableList getEntries() { return ImmutableList.copyOf(mutableEntries); } /** Returns an immutable copy of the builders from the current mutable entries. */ - public ImmutableList getEntriesBuilders() { - return mutableEntries.stream() - .map(CelCreateStruct.Entry::toBuilder) - .collect(toImmutableList()); + public ImmutableList getEntriesBuilders() { + return mutableEntries.stream().map(CelStruct.Entry::toBuilder).collect(toImmutableList()); } @CanIgnoreReturnValue - public Builder setEntry(int index, CelCreateStruct.Entry entry) { + public Builder setEntry(int index, CelStruct.Entry entry) { checkNotNull(entry); mutableEntries.set(index, entry); return this; } @CanIgnoreReturnValue - public Builder addEntries(CelCreateStruct.Entry... entries) { + public Builder addEntries(CelStruct.Entry... entries) { checkNotNull(entries); return addEntries(Arrays.asList(entries)); } @CanIgnoreReturnValue - public Builder addEntries(Iterable entries) { + public Builder addEntries(Iterable entries) { checkNotNull(entries); entries.forEach(mutableEntries::add); return this; } // Not public due to overridden build logic. - abstract CelCreateStruct autoBuild(); + abstract CelStruct autoBuild(); @CheckReturnValue - public CelCreateStruct build() { + public CelStruct build() { setEntries(ImmutableList.copyOf(mutableEntries)); return autoBuild(); } @@ -756,35 +726,27 @@ public Builder toBuilder() { } public static Builder newBuilder() { - return new AutoValue_CelExpr_CelCreateStruct.Builder().setMessageName(""); + return new AutoValue_CelExpr_CelStruct.Builder().setMessageName(""); } /** Represents an entry of the struct */ @AutoValue @Immutable - public abstract static class Entry { - /** - * Required. An id assigned to this node by the parser which is unique in a given expression - * tree. This is used to associate type information and other attributes to the node. - */ + public abstract static class Entry implements Expression.Struct.Entry { + + @Override public abstract long id(); - /** Entry key kind. */ + @Override public abstract String fieldKey(); - /** - * Required. The value assigned to the key. - * - *

If the optional_entry field is true, the expression must resolve to an optional-typed - * value. If the optional value is present, the key will be set; however, if the optional - * value is absent, the key will be unset. - */ + @Override public abstract CelExpr value(); - /** Whether the key-value pair is optional. */ + @Override public abstract boolean optionalEntry(); - /** Builder for CelCreateStruct.Entry. */ + /** Builder for CelStruct.Entry. */ @AutoValue.Builder public abstract static class Builder { @@ -801,80 +763,73 @@ public abstract static class Builder { public abstract Builder setOptionalEntry(boolean value); @CheckReturnValue - public abstract CelCreateStruct.Entry build(); + public abstract CelStruct.Entry build(); } public abstract Builder toBuilder(); public static Builder newBuilder() { - return new AutoValue_CelExpr_CelCreateStruct_Entry.Builder() - .setId(0) - .setOptionalEntry(false); + return new AutoValue_CelExpr_CelStruct_Entry.Builder().setId(0).setOptionalEntry(false); } } } - /** - * A map creation expression. - * - *

Maps are constructed as `{'key_name': 'value'}`. - */ + /** A map creation expression. See {@link Expression.Map} */ @AutoValue @Immutable - public abstract static class CelCreateMap { + public abstract static class CelMap implements Expression.Map { /** The entries in the creation expression. */ - public abstract ImmutableList entries(); + @Override + public abstract ImmutableList entries(); - /** Builder for CelCreateMap. */ + /** Builder for CelMap. */ @AutoValue.Builder public abstract static class Builder { - private List mutableEntries = new ArrayList<>(); + private java.util.List mutableEntries = new ArrayList<>(); // Not public. This only exists to make AutoValue.Builder work. - abstract ImmutableList entries(); + abstract ImmutableList entries(); // Not public. This only exists to make AutoValue.Builder work. @CanIgnoreReturnValue - abstract Builder setEntries(ImmutableList entries); + abstract Builder setEntries(ImmutableList entries); /** Returns an immutable copy of the current mutable entries present in the builder. */ - public ImmutableList getEntries() { + public ImmutableList getEntries() { return ImmutableList.copyOf(mutableEntries); } /** Returns an immutable copy of the builders from the current mutable entries. */ - public ImmutableList getEntriesBuilders() { - return mutableEntries.stream() - .map(CelCreateMap.Entry::toBuilder) - .collect(toImmutableList()); + public ImmutableList getEntriesBuilders() { + return mutableEntries.stream().map(CelMap.Entry::toBuilder).collect(toImmutableList()); } @CanIgnoreReturnValue - public Builder setEntry(int index, CelCreateMap.Entry entry) { + public Builder setEntry(int index, CelMap.Entry entry) { checkNotNull(entry); mutableEntries.set(index, entry); return this; } @CanIgnoreReturnValue - public Builder addEntries(CelCreateMap.Entry... entries) { + public Builder addEntries(CelMap.Entry... entries) { checkNotNull(entries); return addEntries(Arrays.asList(entries)); } @CanIgnoreReturnValue - public Builder addEntries(Iterable entries) { + public Builder addEntries(Iterable entries) { checkNotNull(entries); entries.forEach(mutableEntries::add); return this; } // Not public due to overridden build logic. - abstract CelCreateMap autoBuild(); + abstract CelMap autoBuild(); @CheckReturnValue - public CelCreateMap build() { + public CelMap build() { setEntries(ImmutableList.copyOf(mutableEntries)); return autoBuild(); } @@ -890,35 +845,27 @@ public Builder toBuilder() { } public static Builder newBuilder() { - return new AutoValue_CelExpr_CelCreateMap.Builder(); + return new AutoValue_CelExpr_CelMap.Builder(); } - /** Represents an entry of the map */ + /** Represents an entry of the map. */ @AutoValue @Immutable - public abstract static class Entry { - /** - * Required. An id assigned to this node by the parser which is unique in a given expression - * tree. This is used to associate type information and other attributes to the node. - */ + public abstract static class Entry implements Expression.Map.Entry { + + @Override public abstract long id(); - /** Required. The key. */ + @Override public abstract CelExpr key(); - /** - * Required. The value assigned to the key. - * - *

If the optional_entry field is true, the expression must resolve to an optional-typed - * value. If the optional value is present, the key will be set; however, if the optional - * value is absent, the key will be unset. - */ + @Override public abstract CelExpr value(); - /** Whether the key-value pair is optional. */ + @Override public abstract boolean optionalEntry(); - /** Builder for CelCreateMap.Entry. */ + /** Builder for CelMap.Entry. */ @AutoValue.Builder public abstract static class Builder { public abstract long id(); @@ -927,85 +874,52 @@ public abstract static class Builder { public abstract CelExpr value(); - public abstract CelCreateMap.Entry.Builder setId(long value); + public abstract CelMap.Entry.Builder setId(long value); - public abstract CelCreateMap.Entry.Builder setKey(CelExpr value); + public abstract CelMap.Entry.Builder setKey(CelExpr value); - public abstract CelCreateMap.Entry.Builder setValue(CelExpr value); + public abstract CelMap.Entry.Builder setValue(CelExpr value); - public abstract CelCreateMap.Entry.Builder setOptionalEntry(boolean value); + public abstract CelMap.Entry.Builder setOptionalEntry(boolean value); @CheckReturnValue - public abstract CelCreateMap.Entry build(); + public abstract CelMap.Entry build(); } - public abstract CelCreateMap.Entry.Builder toBuilder(); + public abstract CelMap.Entry.Builder toBuilder(); - public static CelCreateMap.Entry.Builder newBuilder() { - return new AutoValue_CelExpr_CelCreateMap_Entry.Builder().setId(0).setOptionalEntry(false); + public static CelMap.Entry.Builder newBuilder() { + return new AutoValue_CelExpr_CelMap_Entry.Builder().setId(0).setOptionalEntry(false); } } } - /** - * A comprehension expression applied to a list or map. - * - *

Comprehensions are not part of the core syntax, but enabled with macros. A macro matches a - * specific call signature within a parsed AST and replaces the call with an alternate AST block. - * Macro expansion happens at parse time. - * - *

The following macros are supported within CEL: - * - *

Aggregate type macros may be applied to all elements in a list or all keys in a map: - * - *

`all`, `exists`, `exists_one` - test a predicate expression against the inputs and return - * `true` if the predicate is satisfied for all, any, or only one value `list.all(x, x < 10)`. - * `filter` - test a predicate expression against the inputs and return the subset of elements - * which satisfy the predicate: `payments.filter(p, p > 1000)`. `map` - apply an expression to all - * elements in the input and return the output aggregate type: `[1, 2, 3].map(i, i * i)`. - * - *

The `has(m.x)` macro tests whether the property `x` is present in struct `m`. The semantics - * of this macro depend on the type of `m`. For proto2 messages `has(m.x)` is defined as 'defined, - * but not set`. For proto3, the macro tests whether the property is set to its default. For map - * and struct types, the macro tests whether the property `x` is defined on `m`. - * - *

Comprehension evaluation can be best visualized as the following pseudocode: - */ + /** A comprehension expression applied to a list or map. See {@link Expression.Comprehension} */ @AutoValue @Immutable - public abstract static class CelComprehension { - /** The name of the iteration variable. */ + public abstract static class CelComprehension implements Expression.Comprehension { + @Override public abstract String iterVar(); - /** The range over which var iterates. */ + @Override + public abstract String iterVar2(); + + @Override public abstract CelExpr iterRange(); - /** The name of the variable used for accumulation of the result. */ + @Override public abstract String accuVar(); - /** The initial value of the accumulator. */ + @Override public abstract CelExpr accuInit(); - /** - * An expression which can contain iter_var and accu_var. - * - *

Returns false when the result has been computed and may be used as a hint to short-circuit - * the remainder of the comprehension. - */ + @Override public abstract CelExpr loopCondition(); - /** - * An expression which can contain iter_var and accu_var. - * - *

Computes the next value of accu_var. - */ + @Override public abstract CelExpr loopStep(); - /** - * An expression which can contain accu_var. - * - *

Computes the result. - */ + @Override public abstract CelExpr result(); /** Builder for Comprehension. */ @@ -1025,6 +939,8 @@ public abstract static class Builder { public abstract Builder setIterVar(String value); + public abstract Builder setIterVar2(String value); + public abstract Builder setIterRange(CelExpr value); public abstract Builder setAccuVar(String value); @@ -1046,6 +962,7 @@ public abstract static class Builder { public static Builder newBuilder() { return new AutoValue_CelExpr_CelComprehension.Builder() .setIterVar("") + .setIterVar2("") .setIterRange(CelExpr.newBuilder().build()) .setAccuVar("") .setAccuInit(CelExpr.newBuilder().build()) @@ -1062,14 +979,14 @@ public static CelExpr ofNotSet(long id) { .build(); } - public static CelExpr ofConstantExpr(long id, CelConstant celConstant) { + public static CelExpr ofConstant(long id, CelConstant celConstant) { return newBuilder() .setId(id) .setExprKind(AutoOneOf_CelExpr_ExprKind.constant(celConstant)) .build(); } - public static CelExpr ofIdentExpr(long id, String identName) { + public static CelExpr ofIdent(long id, String identName) { return newBuilder() .setId(id) .setExprKind( @@ -1077,8 +994,7 @@ public static CelExpr ofIdentExpr(long id, String identName) { .build(); } - public static CelExpr ofSelectExpr( - long id, CelExpr operandExpr, String field, boolean isTestOnly) { + public static CelExpr ofSelect(long id, CelExpr operandExpr, String field, boolean isTestOnly) { return newBuilder() .setId(id) .setExprKind( @@ -1091,7 +1007,7 @@ public static CelExpr ofSelectExpr( .build(); } - public static CelExpr ofCallExpr( + public static CelExpr ofCall( long id, Optional targetExpr, String function, ImmutableList arguments) { CelCall.Builder celCallBuilder = CelCall.newBuilder().setFunction(function).addArgs(arguments); @@ -1102,44 +1018,40 @@ public static CelExpr ofCallExpr( .build(); } - public static CelExpr ofCreateListExpr( + public static CelExpr ofList( long id, ImmutableList elements, ImmutableList optionalIndices) { return newBuilder() .setId(id) .setExprKind( - AutoOneOf_CelExpr_ExprKind.createList( - CelCreateList.newBuilder() + AutoOneOf_CelExpr_ExprKind.list( + CelList.newBuilder() .addElements(elements) .addOptionalIndices(optionalIndices) .build())) .build(); } - public static CelExpr ofCreateStructExpr( - long id, String messageName, ImmutableList entries) { + public static CelExpr ofStruct( + long id, String messageName, ImmutableList entries) { return newBuilder() .setId(id) .setExprKind( - AutoOneOf_CelExpr_ExprKind.createStruct( - CelCreateStruct.newBuilder() - .setMessageName(messageName) - .addEntries(entries) - .build())) + AutoOneOf_CelExpr_ExprKind.struct( + CelStruct.newBuilder().setMessageName(messageName).addEntries(entries).build())) .build(); } - public static CelExpr ofCreateMapExpr(long id, ImmutableList entries) { + public static CelExpr ofMap(long id, ImmutableList entries) { return newBuilder() .setId(id) .setExprKind( - AutoOneOf_CelExpr_ExprKind.createMap( - CelCreateMap.newBuilder().addEntries(entries).build())) + AutoOneOf_CelExpr_ExprKind.map(CelMap.newBuilder().addEntries(entries).build())) .build(); } - public static CelCreateStruct.Entry ofCreateStructEntryExpr( + public static CelStruct.Entry ofStructEntry( long id, String fieldKey, CelExpr value, boolean isOptionalEntry) { - return CelCreateStruct.Entry.newBuilder() + return CelStruct.Entry.newBuilder() .setId(id) .setFieldKey(fieldKey) .setValue(value) @@ -1147,9 +1059,9 @@ public static CelCreateStruct.Entry ofCreateStructEntryExpr( .build(); } - public static CelCreateMap.Entry ofCreateMapEntryExpr( + public static CelMap.Entry ofMapEntry( long id, CelExpr mapKey, CelExpr value, boolean isOptionalEntry) { - return CelCreateMap.Entry.newBuilder() + return CelMap.Entry.newBuilder() .setId(id) .setKey(mapKey) .setValue(value) @@ -1166,12 +1078,36 @@ public static CelExpr ofComprehension( CelExpr loopCondition, CelExpr loopStep, CelExpr result) { + return ofComprehension( + id, + iterVar, + /* iterVar2= */ "", + iterRange, + accuVar, + accuInit, + loopCondition, + loopStep, + result); + } + + @SuppressWarnings("TooManyParameters") + public static CelExpr ofComprehension( + long id, + String iterVar, + String iterVar2, + CelExpr iterRange, + String accuVar, + CelExpr accuInit, + CelExpr loopCondition, + CelExpr loopStep, + CelExpr result) { return newBuilder() .setId(id) .setExprKind( AutoOneOf_CelExpr_ExprKind.comprehension( CelComprehension.newBuilder() .setIterVar(iterVar) + .setIterVar2(iterVar2) .setIterRange(iterRange) .setAccuVar(accuVar) .setAccuInit(accuInit) diff --git a/common/src/main/java/dev/cel/common/ast/CelExprConverter.java b/common/src/main/java/dev/cel/common/ast/CelExprConverter.java index a17350cc2..6037d901d 100644 --- a/common/src/main/java/dev/cel/common/ast/CelExprConverter.java +++ b/common/src/main/java/dev/cel/common/ast/CelExprConverter.java @@ -30,6 +30,9 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.primitives.UnsignedLong; +import com.google.protobuf.ByteString; +import dev.cel.common.values.CelByteString; +import dev.cel.common.values.NullValue; import java.util.Map; import java.util.Optional; @@ -68,22 +71,23 @@ public static Expr fromCelExpr(CelExpr celExpr) { .addAllArgs(fromCelExprList(celCall.args())); celCall.target().ifPresent(target -> callBuilder.setTarget(fromCelExpr(target))); return expr.setCallExpr(callBuilder).build(); - case CREATE_LIST: - CelExpr.CelCreateList celCreateList = celExprKind.createList(); + case LIST: + CelExpr.CelList celList = celExprKind.list(); return expr.setListExpr( CreateList.newBuilder() - .addAllElements(fromCelExprList(celCreateList.elements())) - .addAllOptionalIndices(celCreateList.optionalIndices())) + .addAllElements(fromCelExprList(celList.elements())) + .addAllOptionalIndices(celList.optionalIndices())) .build(); - case CREATE_STRUCT: - return expr.setStructExpr(celStructToExprStruct(celExprKind.createStruct())).build(); - case CREATE_MAP: - return expr.setStructExpr(celMapToExprStruct(celExprKind.createMap())).build(); + case STRUCT: + return expr.setStructExpr(celStructToExprStruct(celExprKind.struct())).build(); + case MAP: + return expr.setStructExpr(celMapToExprStruct(celExprKind.map())).build(); case COMPREHENSION: CelExpr.CelComprehension celComprehension = celExprKind.comprehension(); return expr.setComprehensionExpr( Comprehension.newBuilder() .setIterVar(celComprehension.iterVar()) + .setIterVar2(celComprehension.iterVar2()) .setIterRange(fromCelExpr(celComprehension.iterRange())) .setAccuVar(celComprehension.accuVar()) .setAccuInit(fromCelExpr(celComprehension.accuInit())) @@ -102,26 +106,26 @@ public static Expr fromCelExpr(CelExpr celExpr) { public static CelExpr fromExpr(Expr expr) { switch (expr.getExprKindCase()) { case CONST_EXPR: - return CelExpr.ofConstantExpr(expr.getId(), exprConstantToCelConstant(expr.getConstExpr())); + return CelExpr.ofConstant(expr.getId(), exprConstantToCelConstant(expr.getConstExpr())); case IDENT_EXPR: - return CelExpr.ofIdentExpr(expr.getId(), expr.getIdentExpr().getName()); + return CelExpr.ofIdent(expr.getId(), expr.getIdentExpr().getName()); case SELECT_EXPR: Select selectExpr = expr.getSelectExpr(); - return CelExpr.ofSelectExpr( + return CelExpr.ofSelect( expr.getId(), fromExpr(selectExpr.getOperand()), selectExpr.getField(), selectExpr.getTestOnly()); case CALL_EXPR: Call callExpr = expr.getCallExpr(); - return CelExpr.ofCallExpr( + return CelExpr.ofCall( expr.getId(), callExpr.hasTarget() ? Optional.of(fromExpr(callExpr.getTarget())) : Optional.empty(), callExpr.getFunction(), fromExprList(callExpr.getArgsList())); case LIST_EXPR: CreateList createListExpr = expr.getListExpr(); - return CelExpr.ofCreateListExpr( + return CelExpr.ofList( expr.getId(), fromExprList(createListExpr.getElementsList()), ImmutableList.copyOf(createListExpr.getOptionalIndicesList())); @@ -132,6 +136,7 @@ public static CelExpr fromExpr(Expr expr) { return CelExpr.ofComprehension( expr.getId(), comprehensionExpr.getIterVar(), + comprehensionExpr.getIterVar2(), fromExpr(comprehensionExpr.getIterRange()), comprehensionExpr.getAccuVar(), fromExpr(comprehensionExpr.getAccuInit()), @@ -177,7 +182,7 @@ public static CelConstant exprConstantToCelConstant(Constant constExpr) { case CONSTANTKIND_NOT_SET: return CelConstant.ofNotSet(); case NULL_VALUE: - return CelConstant.ofValue(constExpr.getNullValue()); + return CelConstant.ofValue(NullValue.NULL_VALUE); case BOOL_VALUE: return CelConstant.ofValue(constExpr.getBoolValue()); case INT64_VALUE: @@ -189,7 +194,8 @@ public static CelConstant exprConstantToCelConstant(Constant constExpr) { case STRING_VALUE: return CelConstant.ofValue(constExpr.getStringValue()); case BYTES_VALUE: - return CelConstant.ofValue(constExpr.getBytesValue()); + ByteString bytesValue = constExpr.getBytesValue(); + return CelConstant.ofValue(CelByteString.of(bytesValue.toByteArray())); case DURATION_VALUE: return CelConstant.ofValue(constExpr.getDurationValue()); case TIMESTAMP_VALUE: @@ -202,37 +208,37 @@ public static CelConstant exprConstantToCelConstant(Constant constExpr) { private static CelExpr exprStructToCelStruct(long id, CreateStruct structExpr) { if (!structExpr.getMessageName().isEmpty()) { - ImmutableList.Builder entries = ImmutableList.builder(); + ImmutableList.Builder entries = ImmutableList.builder(); for (Entry structExprEntry : structExpr.getEntriesList()) { if (!structExprEntry.getKeyKindCase().equals(KeyKindCase.FIELD_KEY)) { throw new IllegalArgumentException( "Unexpected struct key kind case: " + structExprEntry.getKeyKindCase()); } entries.add( - CelExpr.ofCreateStructEntryExpr( + CelExpr.ofStructEntry( structExprEntry.getId(), structExprEntry.getFieldKey(), fromExpr(structExprEntry.getValue()), structExprEntry.getOptionalEntry())); } - return CelExpr.ofCreateStructExpr(id, structExpr.getMessageName(), entries.build()); + return CelExpr.ofStruct(id, structExpr.getMessageName(), entries.build()); } else { - ImmutableList.Builder entries = ImmutableList.builder(); + ImmutableList.Builder entries = ImmutableList.builder(); for (Entry mapExprEntry : structExpr.getEntriesList()) { if (!mapExprEntry.getKeyKindCase().equals(KeyKindCase.MAP_KEY)) { throw new IllegalArgumentException( "Unexpected map key kind case: " + mapExprEntry.getKeyKindCase()); } entries.add( - CelExpr.ofCreateMapEntryExpr( + CelExpr.ofMapEntry( mapExprEntry.getId(), fromExpr(mapExprEntry.getMapKey()), fromExpr(mapExprEntry.getValue()), mapExprEntry.getOptionalEntry())); } - return CelExpr.ofCreateMapExpr(id, entries.build()); + return CelExpr.ofMap(id, entries.build()); } } @@ -248,7 +254,7 @@ public static Constant celConstantToExprConstant(CelConstant celConstant) { case NOT_SET: return Constant.getDefaultInstance(); case NULL_VALUE: - return Constant.newBuilder().setNullValue(celConstant.nullValue()).build(); + return Constant.newBuilder().setNullValue(com.google.protobuf.NullValue.NULL_VALUE).build(); case BOOLEAN_VALUE: return Constant.newBuilder().setBoolValue(celConstant.booleanValue()).build(); case INT64_VALUE: @@ -260,7 +266,10 @@ public static Constant celConstantToExprConstant(CelConstant celConstant) { case STRING_VALUE: return Constant.newBuilder().setStringValue(celConstant.stringValue()).build(); case BYTES_VALUE: - return Constant.newBuilder().setBytesValue(celConstant.bytesValue()).build(); + CelByteString celByteString = celConstant.bytesValue(); + return Constant.newBuilder() + .setBytesValue(ByteString.copyFrom(celByteString.toByteArray())) + .build(); case DURATION_VALUE: return Constant.newBuilder().setDurationValue(celConstant.durationValue()).build(); case TIMESTAMP_VALUE: @@ -270,9 +279,9 @@ public static Constant celConstantToExprConstant(CelConstant celConstant) { throw new IllegalStateException("unsupported constant case: " + celConstant.getKind()); } - private static CreateStruct celStructToExprStruct(CelExpr.CelCreateStruct celCreateStruct) { + private static CreateStruct celStructToExprStruct(CelExpr.CelStruct celStruct) { ImmutableList.Builder entries = ImmutableList.builder(); - for (CelExpr.CelCreateStruct.Entry celStructExprEntry : celCreateStruct.entries()) { + for (CelExpr.CelStruct.Entry celStructExprEntry : celStruct.entries()) { entries.add( CreateStruct.Entry.newBuilder() .setId(celStructExprEntry.id()) @@ -283,14 +292,14 @@ private static CreateStruct celStructToExprStruct(CelExpr.CelCreateStruct celCre } return CreateStruct.newBuilder() - .setMessageName(celCreateStruct.messageName()) + .setMessageName(celStruct.messageName()) .addAllEntries(entries.build()) .build(); } - private static CreateStruct celMapToExprStruct(CelExpr.CelCreateMap celCreateMap) { + private static CreateStruct celMapToExprStruct(CelExpr.CelMap celMap) { ImmutableList.Builder entries = ImmutableList.builder(); - for (CelExpr.CelCreateMap.Entry celMapEntry : celCreateMap.entries()) { + for (CelExpr.CelMap.Entry celMapEntry : celMap.entries()) { CreateStruct.Entry exprMapEntry = CreateStruct.Entry.newBuilder() .setId(celMapEntry.id()) diff --git a/common/src/main/java/dev/cel/common/ast/CelExprFactory.java b/common/src/main/java/dev/cel/common/ast/CelExprFactory.java index 385e72fee..b69dab05c 100644 --- a/common/src/main/java/dev/cel/common/ast/CelExprFactory.java +++ b/common/src/main/java/dev/cel/common/ast/CelExprFactory.java @@ -18,25 +18,19 @@ import static com.google.common.base.Strings.isNullOrEmpty; import com.google.common.primitives.UnsignedLong; -import com.google.protobuf.ByteString; import dev.cel.common.annotations.Internal; +import dev.cel.common.values.CelByteString; import java.util.Arrays; /** Factory for generating expression nodes. */ @Internal public class CelExprFactory { - - private final CelExprIdGeneratorFactory.ExprIdGenerator idGenerator; + private long exprId = 0L; public static CelExprFactory newInstance() { return new CelExprFactory(); } - public static CelExprFactory newInstance( - CelExprIdGeneratorFactory.ExprIdGenerator exprIdGenerator) { - return new CelExprFactory(exprIdGenerator); - } - /** Create a new constant expression. */ public final CelExpr newConstant(CelConstant constant) { return CelExpr.newBuilder().setId(nextExprId()).setConstant(constant).build(); @@ -48,23 +42,18 @@ public final CelExpr newBoolLiteral(boolean value) { } /** Creates a new constant {@link CelExpr} for a bytes value. */ - public final CelExpr newBytesLiteral(ByteString value) { - return newConstant(CelConstant.ofValue(value)); + public final CelExpr newBytesLiteral(String value) { + return newBytesLiteral(CelByteString.copyFromUtf8(value)); } /** Creates a new constant {@link CelExpr} for a bytes value. */ public final CelExpr newBytesLiteral(byte[] value) { - return newBytesLiteral(value, 0, value.length); + return newBytesLiteral(CelByteString.of(value)); } /** Creates a new constant {@link CelExpr} for a bytes value. */ - public final CelExpr newBytesLiteral(byte[] value, int offset, int size) { - return newBytesLiteral(ByteString.copyFrom(value, offset, size)); - } - - /** Creates a new constant {@link CelExpr} for a bytes value. */ - public final CelExpr newBytesLiteral(String value) { - return newBytesLiteral(ByteString.copyFromUtf8(value)); + public final CelExpr newBytesLiteral(CelByteString value) { + return newConstant(CelConstant.ofValue(value)); } /** Creates a new constant {@link CelExpr} for a double value. */ @@ -96,28 +85,26 @@ public final CelExpr newList(CelExpr... elements) { public final CelExpr newList(Iterable elements) { return CelExpr.newBuilder() .setId(nextExprId()) - .setCreateList(CelExpr.CelCreateList.newBuilder().addElements(elements).build()) + .setList(CelExpr.CelList.newBuilder().addElements(elements).build()) .build(); } /** Creates a new map {@link CelExpr} comprised of the entries. */ - public final CelExpr newMap(CelExpr.CelCreateMap.Entry... entries) { + public final CelExpr newMap(CelExpr.CelMap.Entry... entries) { return newMap(Arrays.asList(entries)); } /** Creates a new map {@link CelExpr} comprised of the entries. */ - public final CelExpr newMap(Iterable entries) { + public final CelExpr newMap(Iterable entries) { return CelExpr.newBuilder() .setId(nextExprId()) - .setCreateMap(CelExpr.CelCreateMap.newBuilder().addEntries(entries).build()) + .setMap(CelExpr.CelMap.newBuilder().addEntries(entries).build()) .build(); } - /** - * Creates a new map {@link CelExpr.CelCreateStruct.Entry} comprised of the given key and value. - */ - public final CelExpr.CelCreateMap.Entry newMapEntry(CelExpr key, CelExpr value) { - return CelExpr.CelCreateMap.Entry.newBuilder() + /** Creates a new map {@link CelExpr.CelStruct.Entry} comprised of the given key and value. */ + public final CelExpr.CelMap.Entry newMapEntry(CelExpr key, CelExpr value) { + return CelExpr.CelMap.Entry.newBuilder() .setId(nextExprId()) .setKey(key) .setValue(value) @@ -125,37 +112,33 @@ public final CelExpr.CelCreateMap.Entry newMapEntry(CelExpr key, CelExpr value) } /** Creates a new message {@link CelExpr} of the given type comprised of the given fields. */ - public final CelExpr newMessage(String typeName, CelExpr.CelCreateStruct.Entry... fields) { + public final CelExpr newMessage(String typeName, CelExpr.CelStruct.Entry... fields) { return newMessage(typeName, Arrays.asList(fields)); } /** Creates a new message {@link CelExpr} of the given type comprised of the given fields. */ - public final CelExpr newMessage(String typeName, Iterable fields) { + public final CelExpr newMessage(String typeName, Iterable fields) { checkArgument(!isNullOrEmpty(typeName)); return CelExpr.newBuilder() .setId(nextExprId()) - .setCreateStruct( - CelExpr.CelCreateStruct.newBuilder() - .setMessageName(typeName) - .addEntries(fields) - .build()) + .setStruct( + CelExpr.CelStruct.newBuilder().setMessageName(typeName).addEntries(fields).build()) .build(); } /** - * Creates a new message {@link CelExpr.CelCreateStruct.Entry} comprised of the given field and - * value. + * Creates a new message {@link CelExpr.CelStruct.Entry} comprised of the given field and value. */ - public final CelExpr.CelCreateStruct.Entry newMessageField(String field, CelExpr value) { + public final CelExpr.CelStruct.Entry newMessageField(String field, CelExpr value) { checkArgument(!isNullOrEmpty(field)); - return CelExpr.CelCreateStruct.Entry.newBuilder() + return CelExpr.CelStruct.Entry.newBuilder() .setId(nextExprId()) .setFieldKey(field) .setValue(value) .build(); } - /** Fold creates a fold comprehension instruction. */ + /** Fold creates a fold for one variable comprehension instruction. */ public final CelExpr fold( String iterVar, CelExpr iterRange, @@ -181,306 +164,33 @@ public final CelExpr fold( .build(); } - /** Fold creates a fold comprehension instruction. */ - public final CelExpr fold( - String iterVar, - CelExpr.Builder iterRange, - String accuVar, - CelExpr accuInit, - CelExpr condition, - CelExpr step, - CelExpr result) { - return fold(iterVar, iterRange.build(), accuVar, accuInit, condition, step, result); - } - - /** Fold creates a fold comprehension instruction. */ - public final CelExpr fold( - String iterVar, - CelExpr iterRange, - String accuVar, - CelExpr.Builder accuInit, - CelExpr condition, - CelExpr step, - CelExpr result) { - return fold(iterVar, iterRange, accuVar, accuInit.build(), condition, step, result); - } - - /** Fold creates a fold comprehension instruction. */ - public final CelExpr fold( - String iterVar, - CelExpr iterRange, - String accuVar, - CelExpr accuInit, - CelExpr.Builder condition, - CelExpr step, - CelExpr result) { - return fold(iterVar, iterRange, accuVar, accuInit, condition.build(), step, result); - } - - /** Fold creates a fold comprehension instruction. */ - public final CelExpr fold( - String iterVar, - CelExpr iterRange, - String accuVar, - CelExpr accuInit, - CelExpr condition, - CelExpr.Builder step, - CelExpr result) { - return fold(iterVar, iterRange, accuVar, accuInit, condition, step.build(), result); - } - - /** Fold creates a fold comprehension instruction. */ - public final CelExpr fold( - String iterVar, - CelExpr iterRange, - String accuVar, - CelExpr accuInit, - CelExpr condition, - CelExpr step, - CelExpr.Builder result) { - return fold(iterVar, iterRange, accuVar, accuInit, condition, step, result.build()); - } - - /** Fold creates a fold comprehension instruction. */ - public final CelExpr fold( - String iterVar, - CelExpr.Builder iterRange, - String accuVar, - CelExpr.Builder accuInit, - CelExpr condition, - CelExpr step, - CelExpr result) { - return fold(iterVar, iterRange.build(), accuVar, accuInit.build(), condition, step, result); - } - - /** Fold creates a fold comprehension instruction. */ - public final CelExpr fold( - String iterVar, - CelExpr.Builder iterRange, - String accuVar, - CelExpr accuInit, - CelExpr.Builder condition, - CelExpr step, - CelExpr result) { - return fold(iterVar, iterRange.build(), accuVar, accuInit, condition.build(), step, result); - } - - /** Fold creates a fold comprehension instruction. */ - public final CelExpr fold( - String iterVar, - CelExpr.Builder iterRange, - String accuVar, - CelExpr accuInit, - CelExpr condition, - CelExpr.Builder step, - CelExpr result) { - return fold(iterVar, iterRange.build(), accuVar, accuInit, condition, step.build(), result); - } - - /** Fold creates a fold comprehension instruction. */ - public final CelExpr fold( - String iterVar, - CelExpr.Builder iterRange, - String accuVar, - CelExpr accuInit, - CelExpr condition, - CelExpr step, - CelExpr.Builder result) { - return fold(iterVar, iterRange.build(), accuVar, accuInit, condition, step, result.build()); - } - - /** Fold creates a fold comprehension instruction. */ - public final CelExpr fold( - String iterVar, - CelExpr iterRange, - String accuVar, - CelExpr.Builder accuInit, - CelExpr.Builder condition, - CelExpr step, - CelExpr result) { - return fold(iterVar, iterRange, accuVar, accuInit.build(), condition.build(), step, result); - } - - /** Fold creates a fold comprehension instruction. */ - public final CelExpr fold( - String iterVar, - CelExpr iterRange, - String accuVar, - CelExpr.Builder accuInit, - CelExpr condition, - CelExpr.Builder step, - CelExpr result) { - return fold(iterVar, iterRange, accuVar, accuInit.build(), condition, step.build(), result); - } - - /** Fold creates a fold comprehension instruction. */ - public final CelExpr fold( - String iterVar, - CelExpr iterRange, - String accuVar, - CelExpr.Builder accuInit, - CelExpr condition, - CelExpr step, - CelExpr.Builder result) { - return fold(iterVar, iterRange, accuVar, accuInit.build(), condition, step, result.build()); - } - - /** Fold creates a fold comprehension instruction. */ - public final CelExpr fold( - String iterVar, - CelExpr iterRange, - String accuVar, - CelExpr accuInit, - CelExpr.Builder condition, - CelExpr.Builder step, - CelExpr result) { - return fold(iterVar, iterRange, accuVar, accuInit, condition.build(), step.build(), result); - } - - /** Fold creates a fold comprehension instruction. */ - public final CelExpr fold( - String iterVar, - CelExpr iterRange, - String accuVar, - CelExpr accuInit, - CelExpr.Builder condition, - CelExpr step, - CelExpr.Builder result) { - return fold(iterVar, iterRange, accuVar, accuInit, condition.build(), step, result.build()); - } - - /** Fold creates a fold comprehension instruction. */ + /** Fold creates a fold for two variable comprehension instruction. */ public final CelExpr fold( String iterVar, + String iterVar2, CelExpr iterRange, String accuVar, CelExpr accuInit, CelExpr condition, - CelExpr.Builder step, - CelExpr.Builder result) { - return fold(iterVar, iterRange, accuVar, accuInit, condition, step.build(), result.build()); - } - - /** Fold creates a fold comprehension instruction. */ - public final CelExpr fold( - String iterVar, - CelExpr.Builder iterRange, - String accuVar, - CelExpr accuInit, - CelExpr.Builder condition, - CelExpr.Builder step, - CelExpr result) { - return fold( - iterVar, iterRange.build(), accuVar, accuInit, condition.build(), step.build(), result); - } - - /** Fold creates a fold comprehension instruction. */ - public final CelExpr fold( - String iterVar, - CelExpr.Builder iterRange, - String accuVar, - CelExpr accuInit, - CelExpr.Builder condition, - CelExpr step, - CelExpr.Builder result) { - return fold( - iterVar, iterRange.build(), accuVar, accuInit, condition.build(), step, result.build()); - } - - /** Fold creates a fold comprehension instruction. */ - public final CelExpr fold( - String iterVar, - CelExpr.Builder iterRange, - String accuVar, - CelExpr.Builder accuInit, - CelExpr.Builder condition, - CelExpr.Builder step, - CelExpr result) { - return fold( - iterVar, - iterRange.build(), - accuVar, - accuInit.build(), - condition.build(), - step.build(), - result); - } - - /** Fold creates a fold comprehension instruction. */ - public final CelExpr fold( - String iterVar, - CelExpr.Builder iterRange, - String accuVar, - CelExpr.Builder accuInit, - CelExpr condition, CelExpr step, - CelExpr.Builder result) { - return fold( - iterVar, iterRange.build(), accuVar, accuInit.build(), condition, step, result.build()); - } - - /** Fold creates a fold comprehension instruction. */ - public final CelExpr fold( - String iterVar, - CelExpr.Builder iterRange, - String accuVar, - CelExpr.Builder accuInit, - CelExpr condition, - CelExpr.Builder step, CelExpr result) { - return fold( - iterVar, iterRange.build(), accuVar, accuInit.build(), condition, step.build(), result); - } - - /** Fold creates a fold comprehension instruction. */ - public final CelExpr fold( - String iterVar, - CelExpr.Builder iterRange, - String accuVar, - CelExpr.Builder accuInit, - CelExpr.Builder condition, - CelExpr step, - CelExpr result) { - return fold( - iterVar, iterRange.build(), accuVar, accuInit.build(), condition.build(), step, result); - } - - /** Fold creates a fold comprehension instruction. */ - public final CelExpr fold( - String iterVar, - CelExpr.Builder iterRange, - String accuVar, - CelExpr accuInit, - CelExpr.Builder condition, - CelExpr.Builder step, - CelExpr.Builder result) { - return fold( - iterVar, - iterRange.build(), - accuVar, - accuInit, - condition.build(), - step.build(), - result.build()); - } - - /** Fold creates a fold comprehension instruction. */ - public final CelExpr fold( - String iterVar, - CelExpr.Builder iterRange, - String accuVar, - CelExpr.Builder accuInit, - CelExpr.Builder condition, - CelExpr.Builder step, - CelExpr.Builder result) { - return fold( - iterVar, - iterRange.build(), - accuVar, - accuInit.build(), - condition.build(), - step.build(), - result.build()); + checkArgument(!isNullOrEmpty(iterVar)); + checkArgument(!isNullOrEmpty(iterVar2)); + checkArgument(!isNullOrEmpty(accuVar)); + return CelExpr.newBuilder() + .setId(nextExprId()) + .setComprehension( + CelExpr.CelComprehension.newBuilder() + .setIterVar(iterVar) + .setIterVar2(iterVar2) + .setIterRange(iterRange) + .setAccuVar(accuVar) + .setAccuInit(accuInit) + .setLoopCondition(condition) + .setLoopStep(step) + .setResult(result) + .build()) + .build(); } /** Creates an identifier {@link CelExpr} for the given name. */ @@ -551,19 +261,8 @@ public final CelExpr newSelect(CelExpr operand, String field, boolean testOnly) /** Returns the next unique expression ID. */ protected long nextExprId() { - return idGenerator.generate( - /* exprId= */ -1); // Unconditionally generate next unique ID (i.e: no renumbering). - } - - protected CelExprFactory() { - this(CelExprIdGeneratorFactory.newMonotonicIdGenerator(0)); + return ++exprId; } - private CelExprFactory(CelExprIdGeneratorFactory.MonotonicIdGenerator idGenerator) { - this((unused) -> idGenerator.nextExprId()); - } - - private CelExprFactory(CelExprIdGeneratorFactory.ExprIdGenerator exprIdGenerator) { - idGenerator = exprIdGenerator; - } + protected CelExprFactory() {} } diff --git a/common/src/main/java/dev/cel/common/ast/CelExprFormatter.java b/common/src/main/java/dev/cel/common/ast/CelExprFormatter.java index 32e0233d0..d0eee2f50 100644 --- a/common/src/main/java/dev/cel/common/ast/CelExprFormatter.java +++ b/common/src/main/java/dev/cel/common/ast/CelExprFormatter.java @@ -15,6 +15,9 @@ package dev.cel.common.ast; import com.google.common.collect.ImmutableSet; +import dev.cel.common.values.CelByteString; +import java.nio.charset.StandardCharsets; +import java.util.Locale; /** Provides string formatting support for {@link CelExpr}. */ final class CelExprFormatter { @@ -32,8 +35,8 @@ static String format(CelExpr celExpr) { } private void formatExpr(CelExpr celExpr) { - append(String.format("%s [%d] {", celExpr.exprKind().getKind(), celExpr.id())); CelExpr.ExprKind.Kind exprKind = celExpr.exprKind().getKind(); + append(String.format(Locale.getDefault(), "%s [%d] {", exprKind, celExpr.id())); if (!EXCLUDED_NEWLINE_KINDS.contains(exprKind)) { appendNewline(); } @@ -51,14 +54,14 @@ private void formatExpr(CelExpr celExpr) { case CALL: appendCall(celExpr.call()); break; - case CREATE_LIST: - appendCreateList(celExpr.createList()); + case LIST: + appendList(celExpr.list()); break; - case CREATE_STRUCT: - appendCreateStruct(celExpr.createStruct()); + case STRUCT: + appendStruct(celExpr.struct()); break; - case CREATE_MAP: - appendCreateMap(celExpr.createMap()); + case MAP: + appendMap(celExpr.map()); break; case COMPREHENSION: appendComprehension(celExpr.comprehension()); @@ -103,7 +106,12 @@ private void appendConst(CelConstant celConstant) { appendWithoutIndent("\"" + celConstant.stringValue() + "\""); break; case BYTES_VALUE: - appendWithoutIndent(String.format("b\"%s\"", celConstant.bytesValue().toStringUtf8())); + CelByteString byteString = celConstant.bytesValue(); + appendWithoutIndent( + String.format( + Locale.getDefault(), + "b\"%s\"", + new String(byteString.toByteArray(), StandardCharsets.UTF_8))); break; default: append("Unknown kind: " + celConstant.getKind()); @@ -152,23 +160,23 @@ private void appendCall(CelExpr.CelCall celCall) { outdent(); } - private void appendCreateList(CelExpr.CelCreateList celCreateList) { + private void appendList(CelExpr.CelList celList) { indent(); append("elements: {"); indent(); - for (CelExpr expr : celCreateList.elements()) { + for (CelExpr expr : celList.elements()) { appendNewline(); formatExpr(expr); } outdent(); appendNewline(); append("}"); - if (!celCreateList.optionalIndices().isEmpty()) { + if (!celList.optionalIndices().isEmpty()) { appendNewline(); append("optional_indices: ["); - for (int i = 0; i < celCreateList.optionalIndices().size(); i++) { + for (int i = 0; i < celList.optionalIndices().size(); i++) { appendWithoutIndent(String.valueOf(i)); - if (i != celCreateList.optionalIndices().size() - 1) { + if (i != celList.optionalIndices().size() - 1) { appendWithoutIndent(", "); } } @@ -177,14 +185,14 @@ private void appendCreateList(CelExpr.CelCreateList celCreateList) { outdent(); } - private void appendCreateStruct(CelExpr.CelCreateStruct celCreateStruct) { + private void appendStruct(CelExpr.CelStruct celStruct) { indent(); - appendWithNewline("name: " + celCreateStruct.messageName()); + appendWithNewline("name: " + celStruct.messageName()); append("entries: {"); indent(); - for (CelExpr.CelCreateStruct.Entry entry : celCreateStruct.entries()) { + for (CelExpr.CelStruct.Entry entry : celStruct.entries()) { appendNewline(); - appendWithNewline(String.format("ENTRY [%d] {", entry.id())); + appendWithNewline(String.format(Locale.getDefault(), "ENTRY [%d] {", entry.id())); indent(); appendWithNewline("field_key: " + entry.fieldKey()); if (entry.optionalEntry()) { @@ -205,16 +213,16 @@ private void appendCreateStruct(CelExpr.CelCreateStruct celCreateStruct) { outdent(); } - private void appendCreateMap(CelExpr.CelCreateMap celCreateMap) { + private void appendMap(CelExpr.CelMap celMap) { indent(); boolean firstLine = true; - for (CelExpr.CelCreateMap.Entry entry : celCreateMap.entries()) { + for (CelExpr.CelMap.Entry entry : celMap.entries()) { if (!firstLine) { appendNewline(); } else { firstLine = false; } - appendWithNewline(String.format("MAP_ENTRY [%d] {", entry.id())); + appendWithNewline(String.format(Locale.getDefault(), "MAP_ENTRY [%d] {", entry.id())); indent(); appendWithNewline("key: {"); indent(); @@ -240,6 +248,9 @@ private void appendCreateMap(CelExpr.CelCreateMap celCreateMap) { private void appendComprehension(CelExpr.CelComprehension celComprehension) { indent(); appendWithNewline("iter_var: " + celComprehension.iterVar()); + if (!celComprehension.iterVar2().isEmpty()) { + appendWithNewline("iter_var2: " + celComprehension.iterVar2()); + } // Iter range appendWithNewline("iter_range: {"); indent(); diff --git a/common/src/main/java/dev/cel/common/ast/CelExprUtil.java b/common/src/main/java/dev/cel/common/ast/CelExprUtil.java deleted file mode 100644 index 20217128e..000000000 --- a/common/src/main/java/dev/cel/common/ast/CelExprUtil.java +++ /dev/null @@ -1,71 +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.common.ast; - -import com.google.errorprone.annotations.CanIgnoreReturnValue; -import dev.cel.bundle.Cel; -import dev.cel.common.CelAbstractSyntaxTree; -import dev.cel.common.CelSource; -import dev.cel.common.CelValidationException; -import dev.cel.runtime.CelEvaluationException; - -/** Utility class for working with CelExpr. */ -public final class CelExprUtil { - - /** - * Type-checks and evaluates a CelExpr. This method should be used in the context of validating or - * optimizing an AST. - * - * @return Evaluated result. - * @throws CelValidationException if CelExpr fails to type-check. - * @throws CelEvaluationException if CelExpr fails to evaluate. - */ - @CanIgnoreReturnValue - public static Object evaluateExpr(Cel cel, CelExpr expr) - throws CelValidationException, CelEvaluationException { - CelAbstractSyntaxTree ast = - CelAbstractSyntaxTree.newParsedAst(expr, CelSource.newBuilder().build()); - ast = cel.check(ast).getAst(); - - return cel.createProgram(ast).eval(); - } - - /** - * Type-checks and evaluates a CelExpr. The evaluated result is then checked to see if it's the - * expected result type. - * - *

This method should be used in the context of validating or optimizing an AST. - * - * @return Evaluated result. - * @throws CelValidationException if CelExpr fails to type-check. - * @throws CelEvaluationException if CelExpr fails to evaluate. - * @throws IllegalStateException if the evaluated result is not of type {@code - * expectedResultType}. - */ - @CanIgnoreReturnValue - public static Object evaluateExpr(Cel cel, CelExpr expr, Class expectedResultType) - throws CelValidationException, CelEvaluationException { - Object result = evaluateExpr(cel, expr); - if (!expectedResultType.isInstance(result)) { - throw new IllegalStateException( - String.format( - "Expected %s type but got %s instead", - expectedResultType.getName(), result.getClass().getName())); - } - return result; - } - - private CelExprUtil() {} -} diff --git a/common/src/main/java/dev/cel/common/ast/CelExprV1Alpha1Converter.java b/common/src/main/java/dev/cel/common/ast/CelExprV1Alpha1Converter.java index 377910c04..0e755e7a2 100644 --- a/common/src/main/java/dev/cel/common/ast/CelExprV1Alpha1Converter.java +++ b/common/src/main/java/dev/cel/common/ast/CelExprV1Alpha1Converter.java @@ -30,6 +30,9 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.primitives.UnsignedLong; +import com.google.protobuf.ByteString; +import dev.cel.common.values.CelByteString; +import dev.cel.common.values.NullValue; import java.util.Map; import java.util.Optional; @@ -68,22 +71,23 @@ public static Expr fromCelExpr(CelExpr celExpr) { .addAllArgs(fromCelExprList(celCall.args())); celCall.target().ifPresent(target -> callBuilder.setTarget(fromCelExpr(target))); return expr.setCallExpr(callBuilder).build(); - case CREATE_LIST: - CelExpr.CelCreateList celCreateList = celExprKind.createList(); + case LIST: + CelExpr.CelList celList = celExprKind.list(); return expr.setListExpr( CreateList.newBuilder() - .addAllElements(fromCelExprList(celCreateList.elements())) - .addAllOptionalIndices(celCreateList.optionalIndices())) + .addAllElements(fromCelExprList(celList.elements())) + .addAllOptionalIndices(celList.optionalIndices())) .build(); - case CREATE_STRUCT: - return expr.setStructExpr(celStructToExprStruct(celExprKind.createStruct())).build(); - case CREATE_MAP: - return expr.setStructExpr(celMapToExprStruct(celExprKind.createMap())).build(); + case STRUCT: + return expr.setStructExpr(celStructToExprStruct(celExprKind.struct())).build(); + case MAP: + return expr.setStructExpr(celMapToExprStruct(celExprKind.map())).build(); case COMPREHENSION: CelExpr.CelComprehension celComprehension = celExprKind.comprehension(); return expr.setComprehensionExpr( Comprehension.newBuilder() .setIterVar(celComprehension.iterVar()) + .setIterVar2(celComprehension.iterVar2()) .setIterRange(fromCelExpr(celComprehension.iterRange())) .setAccuVar(celComprehension.accuVar()) .setAccuInit(fromCelExpr(celComprehension.accuInit())) @@ -102,26 +106,26 @@ public static Expr fromCelExpr(CelExpr celExpr) { public static CelExpr fromExpr(Expr expr) { switch (expr.getExprKindCase()) { case CONST_EXPR: - return CelExpr.ofConstantExpr(expr.getId(), exprConstantToCelConstant(expr.getConstExpr())); + return CelExpr.ofConstant(expr.getId(), exprConstantToCelConstant(expr.getConstExpr())); case IDENT_EXPR: - return CelExpr.ofIdentExpr(expr.getId(), expr.getIdentExpr().getName()); + return CelExpr.ofIdent(expr.getId(), expr.getIdentExpr().getName()); case SELECT_EXPR: Select selectExpr = expr.getSelectExpr(); - return CelExpr.ofSelectExpr( + return CelExpr.ofSelect( expr.getId(), fromExpr(selectExpr.getOperand()), selectExpr.getField(), selectExpr.getTestOnly()); case CALL_EXPR: Call callExpr = expr.getCallExpr(); - return CelExpr.ofCallExpr( + return CelExpr.ofCall( expr.getId(), callExpr.hasTarget() ? Optional.of(fromExpr(callExpr.getTarget())) : Optional.empty(), callExpr.getFunction(), fromExprList(callExpr.getArgsList())); case LIST_EXPR: CreateList createListExpr = expr.getListExpr(); - return CelExpr.ofCreateListExpr( + return CelExpr.ofList( expr.getId(), fromExprList(createListExpr.getElementsList()), ImmutableList.copyOf(createListExpr.getOptionalIndicesList())); @@ -132,6 +136,7 @@ public static CelExpr fromExpr(Expr expr) { return CelExpr.ofComprehension( expr.getId(), comprehensionExpr.getIterVar(), + comprehensionExpr.getIterVar2(), fromExpr(comprehensionExpr.getIterRange()), comprehensionExpr.getAccuVar(), fromExpr(comprehensionExpr.getAccuInit()), @@ -177,7 +182,7 @@ public static CelConstant exprConstantToCelConstant(Constant constExpr) { case CONSTANTKIND_NOT_SET: return CelConstant.ofNotSet(); case NULL_VALUE: - return CelConstant.ofValue(constExpr.getNullValue()); + return CelConstant.ofValue(NullValue.NULL_VALUE); case BOOL_VALUE: return CelConstant.ofValue(constExpr.getBoolValue()); case INT64_VALUE: @@ -189,7 +194,8 @@ public static CelConstant exprConstantToCelConstant(Constant constExpr) { case STRING_VALUE: return CelConstant.ofValue(constExpr.getStringValue()); case BYTES_VALUE: - return CelConstant.ofValue(constExpr.getBytesValue()); + ByteString bytesValue = constExpr.getBytesValue(); + return CelConstant.ofValue(CelByteString.of(bytesValue.toByteArray())); case DURATION_VALUE: return CelConstant.ofValue(constExpr.getDurationValue()); case TIMESTAMP_VALUE: @@ -202,37 +208,37 @@ public static CelConstant exprConstantToCelConstant(Constant constExpr) { private static CelExpr exprStructToCelStruct(long id, CreateStruct structExpr) { if (!structExpr.getMessageName().isEmpty()) { - ImmutableList.Builder entries = ImmutableList.builder(); + ImmutableList.Builder entries = ImmutableList.builder(); for (Entry structExprEntry : structExpr.getEntriesList()) { if (!structExprEntry.getKeyKindCase().equals(KeyKindCase.FIELD_KEY)) { throw new IllegalArgumentException( "Unexpected struct key kind case: " + structExprEntry.getKeyKindCase()); } entries.add( - CelExpr.ofCreateStructEntryExpr( + CelExpr.ofStructEntry( structExprEntry.getId(), structExprEntry.getFieldKey(), fromExpr(structExprEntry.getValue()), structExprEntry.getOptionalEntry())); } - return CelExpr.ofCreateStructExpr(id, structExpr.getMessageName(), entries.build()); + return CelExpr.ofStruct(id, structExpr.getMessageName(), entries.build()); } else { - ImmutableList.Builder entries = ImmutableList.builder(); + ImmutableList.Builder entries = ImmutableList.builder(); for (Entry mapExprEntry : structExpr.getEntriesList()) { if (!mapExprEntry.getKeyKindCase().equals(KeyKindCase.MAP_KEY)) { throw new IllegalArgumentException( "Unexpected map key kind case: " + mapExprEntry.getKeyKindCase()); } entries.add( - CelExpr.ofCreateMapEntryExpr( + CelExpr.ofMapEntry( mapExprEntry.getId(), fromExpr(mapExprEntry.getMapKey()), fromExpr(mapExprEntry.getValue()), mapExprEntry.getOptionalEntry())); } - return CelExpr.ofCreateMapExpr(id, entries.build()); + return CelExpr.ofMap(id, entries.build()); } } @@ -248,7 +254,7 @@ public static Constant celConstantToExprConstant(CelConstant celConstant) { case NOT_SET: return Constant.getDefaultInstance(); case NULL_VALUE: - return Constant.newBuilder().setNullValue(celConstant.nullValue()).build(); + return Constant.newBuilder().setNullValue(com.google.protobuf.NullValue.NULL_VALUE).build(); case BOOLEAN_VALUE: return Constant.newBuilder().setBoolValue(celConstant.booleanValue()).build(); case INT64_VALUE: @@ -260,7 +266,10 @@ public static Constant celConstantToExprConstant(CelConstant celConstant) { case STRING_VALUE: return Constant.newBuilder().setStringValue(celConstant.stringValue()).build(); case BYTES_VALUE: - return Constant.newBuilder().setBytesValue(celConstant.bytesValue()).build(); + CelByteString celByteString = celConstant.bytesValue(); + return Constant.newBuilder() + .setBytesValue(ByteString.copyFrom(celByteString.toByteArray())) + .build(); case DURATION_VALUE: return Constant.newBuilder().setDurationValue(celConstant.durationValue()).build(); case TIMESTAMP_VALUE: @@ -270,9 +279,9 @@ public static Constant celConstantToExprConstant(CelConstant celConstant) { throw new IllegalStateException("unsupported constant case: " + celConstant.getKind()); } - private static CreateStruct celStructToExprStruct(CelExpr.CelCreateStruct celCreateStruct) { + private static CreateStruct celStructToExprStruct(CelExpr.CelStruct celStruct) { ImmutableList.Builder entries = ImmutableList.builder(); - for (CelExpr.CelCreateStruct.Entry celStructExprEntry : celCreateStruct.entries()) { + for (CelExpr.CelStruct.Entry celStructExprEntry : celStruct.entries()) { entries.add( CreateStruct.Entry.newBuilder() .setId(celStructExprEntry.id()) @@ -283,14 +292,14 @@ private static CreateStruct celStructToExprStruct(CelExpr.CelCreateStruct celCre } return Expr.CreateStruct.newBuilder() - .setMessageName(celCreateStruct.messageName()) + .setMessageName(celStruct.messageName()) .addAllEntries(entries.build()) .build(); } - private static CreateStruct celMapToExprStruct(CelExpr.CelCreateMap celCreateMap) { + private static CreateStruct celMapToExprStruct(CelExpr.CelMap celMap) { ImmutableList.Builder entries = ImmutableList.builder(); - for (CelExpr.CelCreateMap.Entry celMapEntry : celCreateMap.entries()) { + for (CelExpr.CelMap.Entry celMapEntry : celMap.entries()) { CreateStruct.Entry exprMapEntry = CreateStruct.Entry.newBuilder() .setId(celMapEntry.id()) diff --git a/common/src/main/java/dev/cel/common/ast/CelExprVisitor.java b/common/src/main/java/dev/cel/common/ast/CelExprVisitor.java index ca0911364..a5a6cdf17 100644 --- a/common/src/main/java/dev/cel/common/ast/CelExprVisitor.java +++ b/common/src/main/java/dev/cel/common/ast/CelExprVisitor.java @@ -17,11 +17,11 @@ import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.ast.CelExpr.CelCall; import dev.cel.common.ast.CelExpr.CelComprehension; -import dev.cel.common.ast.CelExpr.CelCreateList; -import dev.cel.common.ast.CelExpr.CelCreateMap; -import dev.cel.common.ast.CelExpr.CelCreateStruct; import dev.cel.common.ast.CelExpr.CelIdent; +import dev.cel.common.ast.CelExpr.CelList; +import dev.cel.common.ast.CelExpr.CelMap; import dev.cel.common.ast.CelExpr.CelSelect; +import dev.cel.common.ast.CelExpr.CelStruct; /** CEL expression visitor implementation using Cel native types. */ public class CelExprVisitor { @@ -60,14 +60,14 @@ public void visit(CelExpr expr) { case CALL: visit(expr, expr.call()); break; - case CREATE_LIST: - visit(expr, expr.createList()); + case LIST: + visit(expr, expr.list()); break; - case CREATE_STRUCT: - visit(expr, expr.createStruct()); + case STRUCT: + visit(expr, expr.struct()); break; - case CREATE_MAP: - visit(expr, expr.createMap()); + case MAP: + visit(expr, expr.map()); break; case COMPREHENSION: visit(expr, expr.comprehension()); @@ -107,24 +107,24 @@ protected void visit(CelExpr expr, CelCall call) { } } - /** Visit a {@code CelCreateStruct} expression. */ - protected void visit(CelExpr expr, CelCreateStruct createStruct) { - for (CelCreateStruct.Entry entry : createStruct.entries()) { + /** Visit a {@code CelStruct} expression. */ + protected void visit(CelExpr expr, CelStruct struct) { + for (CelStruct.Entry entry : struct.entries()) { visit(entry.value()); } } - /** Visit a {@code CelCreateMap} expression. */ - protected void visit(CelExpr expr, CelCreateMap createMap) { - for (CelCreateMap.Entry entry : createMap.entries()) { + /** Visit a {@code CelMap} expression. */ + protected void visit(CelExpr expr, CelMap map) { + for (CelMap.Entry entry : map.entries()) { visit(entry.key()); visit(entry.value()); } } - /** Visit a {@code CelCreateList} expression. */ - protected void visit(CelExpr expr, CelCreateList createList) { - for (CelExpr elem : createList.elements()) { + /** Visit a {@code CelList} expression. */ + protected void visit(CelExpr expr, CelList list) { + for (CelExpr elem : list.elements()) { visit(elem); } } diff --git a/common/src/main/java/dev/cel/common/ast/CelMutableExpr.java b/common/src/main/java/dev/cel/common/ast/CelMutableExpr.java new file mode 100644 index 000000000..e0fabc400 --- /dev/null +++ b/common/src/main/java/dev/cel/common/ast/CelMutableExpr.java @@ -0,0 +1,1209 @@ +// 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. + +package dev.cel.common.ast; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import dev.cel.common.ast.CelExpr.CelNotSet; +import dev.cel.common.ast.CelExpr.ExprKind; +import dev.cel.common.ast.CelExpr.ExprKind.Kind; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Optional; + +/** + * An abstract representation of a common expression that allows mutation in any of its properties. + * The expressions are semantically the same as that of the immutable {@link CelExpr}. Refer to + * {@link Expression} for details. + * + *

This allows for an efficient optimization of an AST without having to traverse and rebuild the + * entire tree. + * + *

This class is not thread-safe by design. + */ +@SuppressWarnings("unchecked") // Class ensures only the super type is used +public final class CelMutableExpr implements Expression { + private long id; + private ExprKind.Kind exprKind; + private Object exprValue; + + @Override + public long id() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + @Override + public ExprKind.Kind getKind() { + return exprKind; + } + + public CelNotSet notSet() { + checkExprKind(Kind.NOT_SET); + return (CelNotSet) exprValue; + } + + @Override + public CelConstant constant() { + checkExprKind(Kind.CONSTANT); + return (CelConstant) exprValue; + } + + @Override + public CelMutableIdent ident() { + checkExprKind(Kind.IDENT); + return (CelMutableIdent) exprValue; + } + + @Override + public CelMutableSelect select() { + checkExprKind(Kind.SELECT); + return (CelMutableSelect) exprValue; + } + + @Override + public CelMutableCall call() { + checkExprKind(Kind.CALL); + return (CelMutableCall) exprValue; + } + + @Override + public CelMutableList list() { + checkExprKind(Kind.LIST); + return (CelMutableList) exprValue; + } + + @Override + public CelMutableStruct struct() { + checkExprKind(Kind.STRUCT); + return (CelMutableStruct) exprValue; + } + + @Override + public CelMutableMap map() { + checkExprKind(Kind.MAP); + return (CelMutableMap) exprValue; + } + + @Override + public CelMutableComprehension comprehension() { + checkExprKind(Kind.COMPREHENSION); + return (CelMutableComprehension) exprValue; + } + + public void setConstant(CelConstant constant) { + this.exprKind = ExprKind.Kind.CONSTANT; + this.exprValue = checkNotNull(constant); + } + + public void setIdent(CelMutableIdent ident) { + this.exprKind = ExprKind.Kind.IDENT; + this.exprValue = checkNotNull(ident); + } + + public void setSelect(CelMutableSelect select) { + this.exprKind = ExprKind.Kind.SELECT; + this.exprValue = checkNotNull(select); + } + + public void setCall(CelMutableCall call) { + this.exprKind = ExprKind.Kind.CALL; + this.exprValue = checkNotNull(call); + } + + public void setList(CelMutableList list) { + this.exprKind = ExprKind.Kind.LIST; + this.exprValue = checkNotNull(list); + } + + public void setStruct(CelMutableStruct struct) { + this.exprKind = ExprKind.Kind.STRUCT; + this.exprValue = checkNotNull(struct); + } + + public void setMap(CelMutableMap map) { + this.exprKind = ExprKind.Kind.MAP; + this.exprValue = checkNotNull(map); + } + + public void setComprehension(CelMutableComprehension comprehension) { + this.exprKind = ExprKind.Kind.COMPREHENSION; + this.exprValue = checkNotNull(comprehension); + } + + /** A mutable identifier expression. */ + public static final class CelMutableIdent implements Ident { + private String name = ""; + + @Override + public String name() { + return name; + } + + public void setName(String name) { + this.name = checkNotNull(name); + } + + public static CelMutableIdent create(String name) { + return new CelMutableIdent(name); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof CelMutableIdent) { + CelMutableIdent that = (CelMutableIdent) obj; + return this.name.equals(that.name); + } + + return false; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + private CelMutableIdent deepCopy() { + return new CelMutableIdent(name); + } + + private CelMutableIdent(String name) { + this.name = checkNotNull(name); + } + } + + /** A mutable field selection expression. e.g. `request.auth`. */ + public static final class CelMutableSelect implements Expression.Select { + private CelMutableExpr operand; + private String field = ""; + private boolean testOnly; + + @Override + public CelMutableExpr operand() { + return operand; + } + + public void setOperand(CelMutableExpr operand) { + this.operand = checkNotNull(operand); + } + + @Override + public String field() { + return field; + } + + public void setField(String field) { + this.field = checkNotNull(field); + } + + @Override + public boolean testOnly() { + return testOnly; + } + + public void setTestOnly(boolean testOnly) { + this.testOnly = testOnly; + } + + private CelMutableSelect deepCopy() { + return create(newInstance(operand()), field, testOnly); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof CelMutableSelect) { + CelMutableSelect that = (CelMutableSelect) obj; + return this.operand.equals(that.operand()) + && this.field.equals(that.field()) + && this.testOnly == that.testOnly(); + } + return false; + } + + @Override + public int hashCode() { + int h = 1; + h *= 1000003; + h ^= operand.hashCode(); + h *= 1000003; + h ^= field.hashCode(); + h *= 1000003; + h ^= testOnly ? 1231 : 1237; + return h; + } + + public static CelMutableSelect create(CelMutableExpr operand, String field) { + return new CelMutableSelect(operand, field, false); + } + + public static CelMutableSelect create(CelMutableExpr operand, String field, boolean testOnly) { + return new CelMutableSelect(operand, field, testOnly); + } + + private CelMutableSelect(CelMutableExpr operand, String field, boolean testOnly) { + this.operand = checkNotNull(operand); + this.field = checkNotNull(field); + this.testOnly = testOnly; + } + } + + /** A mutable call expression. See {@link Expression.Call} */ + public static final class CelMutableCall implements Expression.Call { + private Optional target; + private String function; + private java.util.List args; + + @Override + public Optional target() { + return target; + } + + public void setTarget(CelMutableExpr target) { + this.target = Optional.of(target); + } + + @Override + public String function() { + return function; + } + + public void setFunction(String function) { + this.function = checkNotNull(function); + } + + @Override + public java.util.List args() { + return args; + } + + public void clearArgs() { + args.clear(); + } + + public void addArgs(CelMutableExpr... exprs) { + addArgs(Arrays.asList(checkNotNull(exprs))); + } + + public void addArgs(Iterable exprs) { + exprs.forEach(e -> args.add(checkNotNull(e))); + } + + public void setArgs(Collection exprs) { + this.args = new ArrayList<>(checkNotNull(exprs)); + } + + public void setArg(int index, CelMutableExpr arg) { + checkArgument(index >= 0 && index < args.size()); + args.set(index, checkNotNull(arg)); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof CelMutableCall) { + CelMutableCall that = (CelMutableCall) obj; + return this.target.equals(that.target()) + && this.function.equals(that.function()) + && this.args.equals(that.args()); + } + + return false; + } + + @Override + public int hashCode() { + int h = 1; + h *= 1000003; + h ^= target.hashCode(); + h *= 1000003; + h ^= function.hashCode(); + h *= 1000003; + h ^= args.hashCode(); + return h; + } + + private CelMutableCall deepCopy() { + java.util.List copiedArgs = deepCopyList(args); + return target().isPresent() + ? create(newInstance(target.get()), function, copiedArgs) + : create(function, copiedArgs); + } + + public static CelMutableCall create(String function, CelMutableExpr... args) { + return create(function, Arrays.asList(checkNotNull(args))); + } + + public static CelMutableCall create(String function, java.util.List args) { + return new CelMutableCall(function, args); + } + + public static CelMutableCall create( + CelMutableExpr target, String function, CelMutableExpr... args) { + return create(target, function, Arrays.asList(checkNotNull(args))); + } + + public static CelMutableCall create( + CelMutableExpr target, String function, java.util.List args) { + return new CelMutableCall(target, function, args); + } + + private CelMutableCall(String function, java.util.List args) { + this.target = Optional.empty(); + this.function = checkNotNull(function); + this.args = new ArrayList<>(checkNotNull(args)); + } + + private CelMutableCall( + CelMutableExpr target, String function, java.util.List args) { + this(function, args); + this.target = Optional.of(target); + } + } + + /** A mutable list creation expression. See {@link List} */ + public static final class CelMutableList implements List { + private final java.util.List elements; + private final java.util.List optionalIndices; + + @Override + public java.util.List elements() { + return elements; + } + + public void setElement(int index, CelMutableExpr element) { + checkArgument(index >= 0 && index < elements().size()); + elements.set(index, checkNotNull(element)); + } + + @Override + public java.util.List optionalIndices() { + return optionalIndices; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof CelMutableList) { + CelMutableList that = (CelMutableList) obj; + return this.elements.equals(that.elements()) + && this.optionalIndices.equals(that.optionalIndices()); + } + + return false; + } + + @Override + public int hashCode() { + int h = 1; + h *= 1000003; + h ^= elements.hashCode(); + h *= 1000003; + h ^= optionalIndices.hashCode(); + + return h; + } + + private CelMutableList deepCopy() { + return create(deepCopyList(elements), optionalIndices); + } + + public static CelMutableList create(CelMutableExpr... elements) { + return create(Arrays.asList(checkNotNull(elements))); + } + + public static CelMutableList create(java.util.List elements) { + return create(elements, new ArrayList<>()); + } + + public static CelMutableList create( + java.util.List mutableExprList, java.util.List optionalIndices) { + return new CelMutableList(mutableExprList, optionalIndices); + } + + private CelMutableList( + java.util.List mutableExprList, java.util.List optionalIndices) { + this.elements = new ArrayList<>(checkNotNull(mutableExprList)); + this.optionalIndices = new ArrayList<>(checkNotNull(optionalIndices)); + } + } + + /** A mutable list creation expression. See {@link Expression.Struct} */ + public static final class CelMutableStruct implements Expression.Struct { + private String messageName = ""; + private java.util.List entries; + + @Override + public String messageName() { + return messageName; + } + + public void setMessageName(String messageName) { + this.messageName = checkNotNull(messageName); + } + + @Override + public java.util.List entries() { + return entries; + } + + public void setEntries(java.util.List entries) { + this.entries = checkNotNull(entries); + } + + public void setEntry(int index, CelMutableStruct.Entry entry) { + checkArgument(index >= 0 && index < entries().size()); + entries.set(index, checkNotNull(entry)); + } + + /** Represents a mutable entry of the struct. */ + public static final class Entry implements Expression.Struct.Entry { + private long id; + private String fieldKey = ""; + private CelMutableExpr value; + private boolean optionalEntry; + + @Override + public long id() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + @Override + public String fieldKey() { + return fieldKey; + } + + public void setFieldKey(String fieldKey) { + this.fieldKey = checkNotNull(fieldKey); + } + + @Override + public CelMutableExpr value() { + return value; + } + + public void setValue(CelMutableExpr value) { + this.value = checkNotNull(value); + } + + @Override + public boolean optionalEntry() { + return optionalEntry; + } + + public void setOptionalEntry(boolean optionalEntry) { + this.optionalEntry = optionalEntry; + } + + private Entry deepCopy() { + return create(id, fieldKey, newInstance(value), optionalEntry); + } + + public static Entry create(long id, String fieldKey, CelMutableExpr value) { + return create(id, fieldKey, value, false); + } + + public static Entry create( + long id, String fieldKey, CelMutableExpr value, boolean optionalEntry) { + return new Entry(id, fieldKey, value, optionalEntry); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof Entry) { + Entry that = (Entry) obj; + return this.id == that.id() + && this.fieldKey.equals(that.fieldKey()) + && this.value.equals(that.value()) + && this.optionalEntry == that.optionalEntry(); + } + return false; + } + + @Override + public int hashCode() { + int h = 1; + h *= 1000003; + h ^= (int) ((id >>> 32) ^ id); + h *= 1000003; + h ^= fieldKey.hashCode(); + h *= 1000003; + h ^= value.hashCode(); + h *= 1000003; + h ^= optionalEntry ? 1231 : 1237; + return h; + } + + private Entry(long id, String fieldKey, CelMutableExpr value, boolean optionalEntry) { + this.id = id; + this.fieldKey = checkNotNull(fieldKey); + this.value = checkNotNull(value); + this.optionalEntry = optionalEntry; + } + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof CelMutableStruct) { + CelMutableStruct that = (CelMutableStruct) obj; + return this.messageName.equals(that.messageName()) && this.entries.equals(that.entries()); + } + return false; + } + + @Override + public int hashCode() { + int h = 1; + h *= 1000003; + h ^= messageName.hashCode(); + h *= 1000003; + h ^= entries.hashCode(); + return h; + } + + private CelMutableStruct deepCopy() { + ArrayList copiedEntries = new ArrayList<>(); + for (CelMutableStruct.Entry entry : entries) { + copiedEntries.add(entry.deepCopy()); + } + + return create(messageName, copiedEntries); + } + + public static CelMutableStruct create( + String messageName, java.util.List entries) { + return new CelMutableStruct(messageName, entries); + } + + private CelMutableStruct(String messageName, java.util.List entries) { + this.messageName = checkNotNull(messageName); + this.entries = new ArrayList<>(checkNotNull(entries)); + } + } + + /** A mutable map creation expression. See {@link Expression.Map} */ + public static final class CelMutableMap implements Expression.Map { + private java.util.List entries; + + @Override + public java.util.List entries() { + return entries; + } + + public void setEntries(java.util.List entries) { + this.entries = checkNotNull(entries); + } + + public void setEntry(int index, CelMutableMap.Entry entry) { + checkArgument(index >= 0 && index < entries().size()); + entries.set(index, checkNotNull(entry)); + } + + /** Represents an entry of the map */ + public static final class Entry implements Expression.Map.Entry { + private long id; + private CelMutableExpr key; + private CelMutableExpr value; + private boolean optionalEntry; + + @Override + public long id() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + @Override + public CelMutableExpr key() { + return key; + } + + public void setKey(CelMutableExpr key) { + this.key = checkNotNull(key); + } + + @Override + public CelMutableExpr value() { + return value; + } + + public void setValue(CelMutableExpr value) { + this.value = checkNotNull(value); + } + + @Override + public boolean optionalEntry() { + return optionalEntry; + } + + public void setOptionalEntry(boolean optionalEntry) { + this.optionalEntry = optionalEntry; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof Entry) { + Entry that = (Entry) obj; + return this.id == that.id() + && this.key.equals(that.key()) + && this.value.equals(that.value()) + && this.optionalEntry == that.optionalEntry(); + } + return false; + } + + @Override + public int hashCode() { + int h = 1; + h *= 1000003; + h ^= (int) ((id >>> 32) ^ id); + h *= 1000003; + h ^= key.hashCode(); + h *= 1000003; + h ^= value.hashCode(); + h *= 1000003; + h ^= optionalEntry ? 1231 : 1237; + return h; + } + + private Entry deepCopy() { + return create(id, newInstance(key), newInstance(value), optionalEntry); + } + + public static Entry create(CelMutableExpr key, CelMutableExpr value) { + return create(0, key, value, false); + } + + public static Entry create(long id, CelMutableExpr key, CelMutableExpr value) { + return create(id, key, value, false); + } + + public static Entry create( + long id, CelMutableExpr key, CelMutableExpr value, boolean optionalEntry) { + return new Entry(id, key, value, optionalEntry); + } + + private Entry(long id, CelMutableExpr key, CelMutableExpr value, boolean optionalEntry) { + this.id = id; + this.key = checkNotNull(key); + this.value = checkNotNull(value); + this.optionalEntry = optionalEntry; + } + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof CelMutableMap) { + CelMutableMap that = (CelMutableMap) obj; + return this.entries.equals(that.entries()); + } + return false; + } + + @Override + public int hashCode() { + int h = 1; + h *= 1000003; + h ^= entries.hashCode(); + return h; + } + + private CelMutableMap deepCopy() { + ArrayList copiedEntries = new ArrayList<>(); + for (CelMutableMap.Entry entry : entries) { + copiedEntries.add(entry.deepCopy()); + } + + return create(copiedEntries); + } + + public static CelMutableMap create(java.util.List entries) { + return new CelMutableMap(new ArrayList<>(entries)); + } + + private CelMutableMap(java.util.List entries) { + this.entries = checkNotNull(entries); + } + } + + /** + * A mutable comprehension expression applied to a list or map. See {@link + * Expression.Comprehension} + */ + public static final class CelMutableComprehension + implements Expression.Comprehension { + + private String iterVar; + + private String iterVar2; + + private CelMutableExpr iterRange; + + private String accuVar; + + private CelMutableExpr accuInit; + + private CelMutableExpr loopCondition; + + private CelMutableExpr loopStep; + + private CelMutableExpr result; + + @Override + public String iterVar() { + return iterVar; + } + + @Override + public String iterVar2() { + return iterVar2; + } + + public void setIterVar(String iterVar) { + this.iterVar = checkNotNull(iterVar); + } + + public void setIterVar2(String iterVar2) { + this.iterVar2 = checkNotNull(iterVar2); + } + + @Override + public CelMutableExpr iterRange() { + return iterRange; + } + + public void setIterRange(CelMutableExpr iterRange) { + this.iterRange = checkNotNull(iterRange); + } + + @Override + public String accuVar() { + return accuVar; + } + + public void setAccuVar(String accuVar) { + this.accuVar = checkNotNull(accuVar); + } + + @Override + public CelMutableExpr accuInit() { + return accuInit; + } + + public void setAccuInit(CelMutableExpr accuInit) { + this.accuInit = checkNotNull(accuInit); + } + + @Override + public CelMutableExpr loopCondition() { + return loopCondition; + } + + public void setLoopCondition(CelMutableExpr loopCondition) { + this.loopCondition = checkNotNull(loopCondition); + } + + @Override + public CelMutableExpr loopStep() { + return loopStep; + } + + public void setLoopStep(CelMutableExpr loopStep) { + this.loopStep = checkNotNull(loopStep); + } + + @Override + public CelMutableExpr result() { + return result; + } + + public void setResult(CelMutableExpr result) { + this.result = checkNotNull(result); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof CelMutableComprehension) { + CelMutableComprehension that = (CelMutableComprehension) obj; + return this.iterVar.equals(that.iterVar()) + && this.iterVar2.equals(that.iterVar2()) + && this.accuVar.equals(that.accuVar()) + && this.iterRange.equals(that.iterRange()) + && this.accuInit.equals(that.accuInit()) + && this.loopCondition.equals(that.loopCondition()) + && this.loopStep.equals(that.loopStep()) + && this.result.equals(that.result()); + } + return false; + } + + @Override + public int hashCode() { + int h = 1; + h *= 1000003; + h ^= iterVar.hashCode(); + h *= 1000003; + h ^= iterVar2.hashCode(); + h *= 1000003; + h ^= iterRange.hashCode(); + h *= 1000003; + h ^= accuVar.hashCode(); + h *= 1000003; + h ^= accuInit.hashCode(); + h *= 1000003; + h ^= loopCondition.hashCode(); + h *= 1000003; + h ^= loopStep.hashCode(); + h *= 1000003; + h ^= result.hashCode(); + return h; + } + + private CelMutableComprehension deepCopy() { + return create( + iterVar, + iterVar2, + newInstance(iterRange), + accuVar, + newInstance(accuInit), + newInstance(loopCondition), + newInstance(loopStep), + newInstance(result)); + } + + public static CelMutableComprehension create( + String iterVar, + CelMutableExpr iterRange, + String accuVar, + CelMutableExpr accuInit, + CelMutableExpr loopCondition, + CelMutableExpr loopStep, + CelMutableExpr result) { + return create( + iterVar, + /* iterVar2= */ "", + iterRange, + accuVar, + accuInit, + loopCondition, + loopStep, + result); + } + + public static CelMutableComprehension create( + String iterVar, + String iterVar2, + CelMutableExpr iterRange, + String accuVar, + CelMutableExpr accuInit, + CelMutableExpr loopCondition, + CelMutableExpr loopStep, + CelMutableExpr result) { + return new CelMutableComprehension( + iterVar, iterVar2, iterRange, accuVar, accuInit, loopCondition, loopStep, result); + } + + private CelMutableComprehension( + String iterVar, + String iterVar2, + CelMutableExpr iterRange, + String accuVar, + CelMutableExpr accuInit, + CelMutableExpr loopCondition, + CelMutableExpr loopStep, + CelMutableExpr result) { + this.iterVar = checkNotNull(iterVar); + this.iterVar2 = checkNotNull(iterVar2); + this.iterRange = checkNotNull(iterRange); + this.accuVar = checkNotNull(accuVar); + this.accuInit = checkNotNull(accuInit); + this.loopCondition = checkNotNull(loopCondition); + this.loopStep = checkNotNull(loopStep); + this.result = checkNotNull(result); + } + } + + public static CelMutableExpr ofNotSet() { + return ofNotSet(0L); + } + + public static CelMutableExpr ofNotSet(long id) { + return new CelMutableExpr(id); + } + + public static CelMutableExpr ofConstant(CelConstant constant) { + return ofConstant(0L, constant); + } + + public static CelMutableExpr ofConstant(long id, CelConstant constant) { + return new CelMutableExpr(id, constant); + } + + public static CelMutableExpr ofIdent(String name) { + return ofIdent(0, name); + } + + public static CelMutableExpr ofIdent(long id, String name) { + return new CelMutableExpr(id, CelMutableIdent.create(name)); + } + + public static CelMutableExpr ofSelect(CelMutableSelect mutableSelect) { + return ofSelect(0, mutableSelect); + } + + public static CelMutableExpr ofSelect(long id, CelMutableSelect mutableSelect) { + return new CelMutableExpr(id, mutableSelect); + } + + public static CelMutableExpr ofCall(CelMutableCall mutableCall) { + return ofCall(0, mutableCall); + } + + public static CelMutableExpr ofCall(long id, CelMutableCall mutableCall) { + return new CelMutableExpr(id, mutableCall); + } + + public static CelMutableExpr ofList(CelMutableList mutableList) { + return ofList(0, mutableList); + } + + public static CelMutableExpr ofList(long id, CelMutableList mutableList) { + return new CelMutableExpr(id, mutableList); + } + + public static CelMutableExpr ofStruct(CelMutableStruct mutableStruct) { + return ofStruct(0, mutableStruct); + } + + public static CelMutableExpr ofStruct(long id, CelMutableStruct mutableStruct) { + return new CelMutableExpr(id, mutableStruct); + } + + public static CelMutableExpr ofMap(CelMutableMap mutableMap) { + return ofMap(0, mutableMap); + } + + public static CelMutableExpr ofMap(long id, CelMutableMap mutableMap) { + return new CelMutableExpr(id, mutableMap); + } + + public static CelMutableExpr ofComprehension(CelMutableComprehension mutableComprehension) { + return ofComprehension(0, mutableComprehension); + } + + public static CelMutableExpr ofComprehension( + long id, CelMutableComprehension mutableComprehension) { + return new CelMutableExpr(id, mutableComprehension); + } + + /** Constructs a deep copy of the mutable expression. */ + public static CelMutableExpr newInstance(CelMutableExpr other) { + return new CelMutableExpr(other); + } + + private CelMutableExpr(long id, CelConstant mutableConstant) { + this.id = id; + setConstant(mutableConstant); + } + + private CelMutableExpr(long id, CelMutableIdent mutableIdent) { + this.id = id; + setIdent(mutableIdent); + } + + private CelMutableExpr(long id, CelMutableSelect mutableSelect) { + this.id = id; + setSelect(mutableSelect); + } + + private CelMutableExpr(long id, CelMutableCall mutableCall) { + this.id = id; + setCall(mutableCall); + } + + private CelMutableExpr(long id, CelMutableList mutableList) { + this.id = id; + setList(mutableList); + } + + private CelMutableExpr(long id, CelMutableStruct mutableStruct) { + this.id = id; + setStruct(mutableStruct); + } + + private CelMutableExpr(long id, CelMutableMap mutableMap) { + this.id = id; + setMap(mutableMap); + } + + private CelMutableExpr(long id, CelMutableComprehension mutableComprehension) { + this.id = id; + setComprehension(mutableComprehension); + } + + private CelMutableExpr(long id) { + this(); + this.id = id; + } + + private CelMutableExpr() { + this.exprValue = CelExpr.newBuilder().build().exprKind().notSet(); + this.exprKind = ExprKind.Kind.NOT_SET; + } + + private CelMutableExpr(CelMutableExpr other) { + checkNotNull(other); + this.id = other.id; + this.exprKind = other.exprKind; + switch (other.getKind()) { + case NOT_SET: + this.exprValue = CelExpr.newBuilder().build().exprKind().notSet(); + break; + case CONSTANT: + this.exprValue = other.exprValue; // Constant is immutable. + break; + case IDENT: + this.exprValue = other.ident().deepCopy(); + break; + case SELECT: + this.exprValue = other.select().deepCopy(); + break; + case CALL: + this.exprValue = other.call().deepCopy(); + break; + case LIST: + this.exprValue = other.list().deepCopy(); + break; + case STRUCT: + this.exprValue = other.struct().deepCopy(); + break; + case MAP: + this.exprValue = other.map().deepCopy(); + break; + case COMPREHENSION: + this.exprValue = other.comprehension().deepCopy(); + break; + default: + throw new IllegalStateException("Unexpected expr kind: " + this.exprKind); + } + } + + private Object exprValue() { + switch (this.exprKind) { + case NOT_SET: + return notSet(); + case CONSTANT: + return constant(); + case IDENT: + return ident(); + case SELECT: + return select(); + case CALL: + return call(); + case LIST: + return list(); + case STRUCT: + return struct(); + case MAP: + return map(); + case COMPREHENSION: + return comprehension(); + } + + throw new IllegalStateException("Unexpected expr kind: " + this.exprKind); + } + + private static java.util.List deepCopyList( + java.util.List elements) { + ArrayList copiedArgs = new ArrayList<>(); + for (CelMutableExpr arg : elements) { + copiedArgs.add(newInstance(arg)); + } + + return copiedArgs; + } + + private void checkExprKind(ExprKind.Kind exprKind) { + checkArgument(this.exprKind.equals(exprKind), "Invalid ExprKind: %s", exprKind); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof CelMutableExpr) { + CelMutableExpr that = (CelMutableExpr) obj; + if (this.id != that.id() || !this.exprKind.equals(that.getKind())) { + return false; + } + + return this.exprValue().equals(that.exprValue()); + } + + return false; + } + + @Override + public String toString() { + return CelMutableExprConverter.fromMutableExpr(this).toString(); + } + + @Override + public int hashCode() { + int h = 1; + h *= 1000003; + h ^= (int) ((id >>> 32) ^ id); + h *= 1000003; + h ^= this.exprValue().hashCode(); + + return h; + } +} diff --git a/common/src/main/java/dev/cel/common/ast/CelMutableExprConverter.java b/common/src/main/java/dev/cel/common/ast/CelMutableExprConverter.java new file mode 100644 index 000000000..3e9ebc9c3 --- /dev/null +++ b/common/src/main/java/dev/cel/common/ast/CelMutableExprConverter.java @@ -0,0 +1,231 @@ +// 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. + +package dev.cel.common.ast; + +import static com.google.common.collect.ImmutableList.toImmutableList; +import static java.util.stream.Collectors.toCollection; + +import com.google.common.collect.ImmutableList; +import dev.cel.common.ast.CelExpr.CelCall; +import dev.cel.common.ast.CelExpr.CelComprehension; +import dev.cel.common.ast.CelExpr.CelList; +import dev.cel.common.ast.CelExpr.CelMap; +import dev.cel.common.ast.CelExpr.CelSelect; +import dev.cel.common.ast.CelExpr.CelStruct; +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.CelMutableSelect; +import dev.cel.common.ast.CelMutableExpr.CelMutableStruct; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * Converts a mutable Expression Tree {@link CelMutableExpr} into the CEL native representation of + * Expression tree {@link CelExpr} and vice versa. + */ +public final class CelMutableExprConverter { + + public static CelMutableExpr fromCelExpr(CelExpr celExpr) { + CelExpr.ExprKind celExprKind = celExpr.exprKind(); + switch (celExprKind.getKind()) { + case CONSTANT: + return CelMutableExpr.ofConstant(celExpr.id(), celExpr.constant()); + case IDENT: + return CelMutableExpr.ofIdent(celExpr.id(), celExpr.ident().name()); + case SELECT: + CelSelect select = celExpr.select(); + CelMutableExpr operand = fromCelExpr(select.operand()); + return CelMutableExpr.ofSelect( + celExpr.id(), CelMutableSelect.create(operand, select.field(), select.testOnly())); + case CALL: + CelCall celCall = celExprKind.call(); + List args = + celCall.args().stream() + .map(CelMutableExprConverter::fromCelExpr) + .collect(toCollection(ArrayList::new)); + CelMutableCall mutableCall = + celCall.target().isPresent() + ? CelMutableCall.create( + fromCelExpr(celCall.target().get()), celCall.function(), args) + : CelMutableCall.create(celCall.function(), args); + + return CelMutableExpr.ofCall(celExpr.id(), mutableCall); + case LIST: + CelList createList = celExpr.list(); + return CelMutableExpr.ofList( + celExpr.id(), + CelMutableList.create( + fromCelExprList(createList.elements()), createList.optionalIndices())); + case STRUCT: + return CelMutableExpr.ofStruct( + celExpr.id(), fromCelStructToMutableStruct(celExpr.struct())); + case MAP: + return CelMutableExpr.ofMap(celExpr.id(), fromCelMapToMutableMap(celExpr.map())); + case COMPREHENSION: + CelComprehension celComprehension = celExprKind.comprehension(); + CelMutableComprehension mutableComprehension = + CelMutableComprehension.create( + celComprehension.iterVar(), + celComprehension.iterVar2(), + fromCelExpr(celComprehension.iterRange()), + celComprehension.accuVar(), + fromCelExpr(celComprehension.accuInit()), + fromCelExpr(celComprehension.loopCondition()), + fromCelExpr(celComprehension.loopStep()), + fromCelExpr(celComprehension.result())); + return CelMutableExpr.ofComprehension(celExpr.id(), mutableComprehension); + case NOT_SET: + return CelMutableExpr.ofNotSet(celExpr.id()); + } + + throw new IllegalArgumentException( + "Unexpected expression kind case: " + celExpr.exprKind().getKind()); + } + + private static List fromCelExprList(Iterable celExprList) { + ArrayList mutableExprList = new ArrayList<>(); + for (CelExpr celExpr : celExprList) { + mutableExprList.add(fromCelExpr(celExpr)); + } + return mutableExprList; + } + + private static CelMutableStruct fromCelStructToMutableStruct(CelStruct celStruct) { + List entries = new ArrayList<>(); + for (CelStruct.Entry celStructExprEntry : celStruct.entries()) { + entries.add( + CelMutableStruct.Entry.create( + celStructExprEntry.id(), + celStructExprEntry.fieldKey(), + fromCelExpr(celStructExprEntry.value()), + celStructExprEntry.optionalEntry())); + } + + return CelMutableStruct.create(celStruct.messageName(), entries); + } + + private static CelMutableMap fromCelMapToMutableMap(CelMap celMap) { + List entries = new ArrayList<>(); + for (CelMap.Entry celMapExprEntry : celMap.entries()) { + entries.add( + CelMutableMap.Entry.create( + celMapExprEntry.id(), + fromCelExpr(celMapExprEntry.key()), + fromCelExpr(celMapExprEntry.value()), + celMapExprEntry.optionalEntry())); + } + + return CelMutableMap.create(entries); + } + + public static CelExpr fromMutableExpr(CelMutableExpr mutableExpr) { + long id = mutableExpr.id(); + switch (mutableExpr.getKind()) { + case CONSTANT: + return CelExpr.ofConstant(id, mutableExpr.constant()); + case IDENT: + return CelExpr.ofIdent(id, mutableExpr.ident().name()); + case SELECT: + CelMutableSelect select = mutableExpr.select(); + CelExpr operand = fromMutableExpr(select.operand()); + return CelExpr.ofSelect(id, operand, select.field(), select.testOnly()); + case CALL: + CelMutableCall mutableCall = mutableExpr.call(); + ImmutableList args = + mutableCall.args().stream() + .map(CelMutableExprConverter::fromMutableExpr) + .collect(toImmutableList()); + Optional targetExpr = + mutableCall.target().map(CelMutableExprConverter::fromMutableExpr); + return CelExpr.ofCall(id, targetExpr, mutableCall.function(), args); + case LIST: + CelMutableList mutableList = mutableExpr.list(); + return CelExpr.ofList( + id, + fromMutableExprList(mutableList.elements()), + ImmutableList.copyOf(mutableList.optionalIndices())); + case STRUCT: + CelMutableStruct mutableStruct = mutableExpr.struct(); + return CelExpr.newBuilder() + .setId(id) + .setStruct(fromMutableStructToCelStruct(mutableStruct)) + .build(); + case MAP: + CelMutableMap mutableMap = mutableExpr.map(); + return CelExpr.newBuilder().setId(id).setMap(fromMutableMapToCelMap(mutableMap)).build(); + case COMPREHENSION: + CelMutableComprehension mutableComprehension = mutableExpr.comprehension(); + return CelExpr.ofComprehension( + id, + mutableComprehension.iterVar(), + mutableComprehension.iterVar2(), + fromMutableExpr(mutableComprehension.iterRange()), + mutableComprehension.accuVar(), + fromMutableExpr(mutableComprehension.accuInit()), + fromMutableExpr(mutableComprehension.loopCondition()), + fromMutableExpr(mutableComprehension.loopStep()), + fromMutableExpr(mutableComprehension.result())); + case NOT_SET: + return CelExpr.ofNotSet(id); + } + + throw new IllegalArgumentException("Unexpected expression kind case: " + mutableExpr.getKind()); + } + + private static ImmutableList fromMutableExprList( + Iterable mutableExprList) { + ImmutableList.Builder celExprList = ImmutableList.builder(); + for (CelMutableExpr mutableExpr : mutableExprList) { + celExprList.add(fromMutableExpr(mutableExpr)); + } + return celExprList.build(); + } + + private static CelStruct fromMutableStructToCelStruct(CelMutableStruct mutableStruct) { + List entries = new ArrayList<>(); + for (CelMutableStruct.Entry mutableStructEntry : mutableStruct.entries()) { + entries.add( + CelExpr.ofStructEntry( + mutableStructEntry.id(), + mutableStructEntry.fieldKey(), + fromMutableExpr(mutableStructEntry.value()), + mutableStructEntry.optionalEntry())); + } + + return CelStruct.newBuilder() + .setMessageName(mutableStruct.messageName()) + .addEntries(entries) + .build(); + } + + private static CelMap fromMutableMapToCelMap(CelMutableMap mutableMap) { + List entries = new ArrayList<>(); + for (CelMutableMap.Entry mutableMapEntry : mutableMap.entries()) { + entries.add( + CelExpr.ofMapEntry( + mutableMapEntry.id(), + fromMutableExpr(mutableMapEntry.key()), + fromMutableExpr(mutableMapEntry.value()), + mutableMapEntry.optionalEntry())); + } + + return CelMap.newBuilder().addEntries(entries).build(); + } + + private CelMutableExprConverter() {} +} diff --git a/common/src/main/java/dev/cel/common/ast/Expression.java b/common/src/main/java/dev/cel/common/ast/Expression.java new file mode 100644 index 000000000..b32598857 --- /dev/null +++ b/common/src/main/java/dev/cel/common/ast/Expression.java @@ -0,0 +1,282 @@ +// 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. + +package dev.cel.common.ast; + +import dev.cel.common.annotations.Internal; +import java.util.Optional; + +/** + * An abstract representation of a common expression. + * + *

Expressions are abstractly represented as a collection of identifiers, select statements, + * function calls, literals, and comprehensions. All operators with the exception of the '.' + * operator are modelled as function calls. This makes it easy to represent new operators into the + * existing AST. + * + *

All references within expressions must resolve to a [Decl][] provided at type-check for an + * expression to be valid. A reference may either be a bare identifier `name` or a qualified + * identifier `google.api.name`. References may either refer to a value or a function declaration. + * + *

For example, the expression `google.api.name.startsWith('expr')` references the declaration + * `google.api.name` within a [Expr.Select][] expression, and the function declaration `startsWith`. + */ +@Internal +public interface Expression { + + /** + * Required. An id assigned to this node by the parser which is unique in a given expression tree. + * This is used to associate type information and other attributes to a node in the parse tree. + */ + long id(); + + /** Represents the enumeration value for the underlying expression kind. */ + CelExpr.ExprKind.Kind getKind(); + + /** Gets the underlying constant expression. */ + CelConstant constant(); + + /** Gets the underlying identifier expression. */ + Ident ident(); + + /** Gets the underlying call expression. */ + Call call(); + + /** Gets the underlying identifier expression. */ + List list(); + + /** Gets the underlying select expression. */ + Select select(); + + /** Gets the underlying struct expression. */ + Struct> struct(); + + /** Gets the underlying map expression. */ + Map> map(); + + /** Gets the underlying comprehension expression. */ + Comprehension comprehension(); + + /** An identifier expression. e.g. `request`. */ + interface Ident { + + /** + * Required. Holds a single, unqualified identifier, possibly preceded by a '.'. + * + *

Qualified names are represented by the [Expr.Select][] expression. + */ + String name(); + } + + /** A call expression, including calls to predefined functions and operators. */ + interface Call { + /** + * The target of a method call-style expression. + * + *

For example, `x` in `x.f()`. + */ + Optional target(); + + /** Required. The name of the function or method being called. */ + String function(); + + /** + * Arguments to the call. + * + *

For example, `foo` in `f(foo)` or `x.f(foo)`. + */ + java.util.List args(); + } + + /** + * A list creation expression. + * + *

Lists may either be homogenous, e.g. `[1, 2, 3]`, or heterogeneous, e.g. `dyn([1, 'hello', + * 2.0])` + */ + interface List { + + /** The elements part of the list */ + java.util.List elements(); + + /** + * The indices within the elements list which are marked as optional elements. + * + *

When an optional-typed value is present, the value it contains is included in the list. If + * the optional-typed value is absent, the list element is omitted from the list result. + */ + java.util.List optionalIndices(); + } + + /** A field selection expression. e.g. `request.auth`. */ + interface Select { + /** + * Required. The target of the selection expression. + * + *

For example, in the select expression `request.auth`, the `request` portion of the + * expression is the `operand`. + */ + E operand(); + + /** + * Required. The name of the field to select. + * + *

For example, in the select expression `request.auth`, the `auth` portion of the expression + * would be the `field`. + */ + String field(); + + /** + * Whether the select is to be interpreted as a field presence test. + * + *

This results from the macro `has(request.auth)`. + */ + boolean testOnly(); + } + + /** + * A message creation expression. + * + *

Messages are constructed with a type name and composed of field ids: `types.MyType{field_id: + * 'value'}`. + */ + interface Struct> { + + /** The type name of the message to be created, empty when creating map literals. */ + String messageName(); + + /** The entries in the creation expression. */ + java.util.List entries(); + + /** Represents an entry of the struct */ + interface Entry { + /** + * Required. An id assigned to this node by the parser which is unique in a given expression + * tree. This is used to associate type information and other attributes to the node. + */ + long id(); + + /** Entry key kind. */ + String fieldKey(); + + /** + * Required. The value assigned to the key. + * + *

If the optional_entry field is true, the expression must resolve to an optional-typed + * value. If the optional value is present, the key will be set; however, if the optional + * value is absent, the key will be unset. + */ + T value(); + + /** Whether the key-value pair is optional. */ + boolean optionalEntry(); + } + } + + /** + * A map creation expression. + * + *

Maps are constructed as `{'key_name': 'value'}`. + */ + interface Map> { + + java.util.List entries(); + + /** Represents an entry of the map. */ + interface Entry { + /** + * Required. An id assigned to this node by the parser which is unique in a given expression + * tree. This is used to associate type information and other attributes to the node. + */ + long id(); + + /** Required. The key. */ + T key(); + + /** + * Required. The value assigned to the key. + * + *

If the optional_entry field is true, the expression must resolve to an optional-typed + * value. If the optional value is present, the key will be set; however, if the optional + * value is absent, the key will be unset. + */ + T value(); + + boolean optionalEntry(); + } + } + + /** + * A comprehension expression applied to a list or map. + * + *

Comprehensions are not part of the core syntax, but enabled with macros. A macro matches a + * specific call signature within a parsed AST and replaces the call with an alternate AST block. + * Macro expansion happens at parse time. + * + *

The following macros are supported within CEL: + * + *

Aggregate type macros may be applied to all elements in a list or all keys in a map: + * + *

`all`, `exists`, `exists_one` - test a predicate expression against the inputs and return + * `true` if the predicate is satisfied for all, any, or only one value `list.all(x, x < 10)`. + * `filter` - test a predicate expression against the inputs and return the subset of elements + * which satisfy the predicate: `payments.filter(p, p > 1000)`. `map` - apply an expression to all + * elements in the input and return the output aggregate type: `[1, 2, 3].map(i, i * i)`. + * + *

The `has(m.x)` macro tests whether the property `x` is present in struct `m`. The semantics + * of this macro depend on the type of `m`. For proto2 messages `has(m.x)` is defined as 'defined, + * but not set`. For proto3, the macro tests whether the property is set to its default. For map + * and struct types, the macro tests whether the property `x` is defined on `m`. + * + *

Comprehension evaluation can be best visualized as the following pseudocode: + */ + interface Comprehension { + /** The name of the iteration variable. */ + String iterVar(); + + /** The name of the second iteration variable. */ + String iterVar2(); + + /** The range over which var iterates. */ + E iterRange(); + + /** The name of the variable used for accumulation of the result. */ + String accuVar(); + + /** The initial value of the accumulator. */ + E accuInit(); + + /** + * An expression which can contain iter_var and accu_var. + * + *

Returns false when the result has been computed and may be used as a hint to short-circuit + * the remainder of the comprehension. + */ + E loopCondition(); + + /** + * An expression which can contain iter_var and accu_var. + * + *

Computes the next value of accu_var. + */ + E loopStep(); + + /** + * An expression which can contain accu_var. + * + *

Computes the result. + */ + E result(); + } +} diff --git a/common/src/main/java/dev/cel/common/exceptions/BUILD.bazel b/common/src/main/java/dev/cel/common/exceptions/BUILD.bazel new file mode 100644 index 000000000..672238cf0 --- /dev/null +++ b/common/src/main/java/dev/cel/common/exceptions/BUILD.bazel @@ -0,0 +1,138 @@ +load("@rules_java//java:defs.bzl", "java_library") + +package( + default_applicable_licenses = ["//:license"], + default_visibility = [ + "//common/exceptions:__pkg__", + "//publish:__pkg__", + ], +) + +java_library( + name = "runtime_exception", + srcs = ["CelRuntimeException.java"], + # used_by_android + tags = [ + ], + deps = [ + "//common:error_codes", + "//common/annotations", + ], +) + +java_library( + name = "attribute_not_found", + srcs = ["CelAttributeNotFoundException.java"], + # used_by_android + tags = [ + ], + deps = [ + ":runtime_exception", + "//common:error_codes", + "//common/annotations", + ], +) + +java_library( + name = "divide_by_zero", + srcs = ["CelDivideByZeroException.java"], + # used_by_android + tags = [ + ], + deps = [ + ":runtime_exception", + "//common:error_codes", + "//common/annotations", + ], +) + +java_library( + name = "index_out_of_bounds", + srcs = ["CelIndexOutOfBoundsException.java"], + # used_by_android + tags = [ + ], + deps = [ + ":runtime_exception", + "//common:error_codes", + "//common/annotations", + ], +) + +java_library( + name = "bad_format", + srcs = ["CelBadFormatException.java"], + # used_by_android + tags = [ + ], + deps = [ + ":runtime_exception", + "//common:error_codes", + "//common/annotations", + ], +) + +java_library( + name = "numeric_overflow", + srcs = ["CelNumericOverflowException.java"], + # used_by_android + tags = [ + ], + deps = [ + ":runtime_exception", + "//common:error_codes", + "//common/annotations", + ], +) + +java_library( + name = "invalid_argument", + srcs = ["CelInvalidArgumentException.java"], + # used_by_android + tags = [ + ], + deps = [ + ":runtime_exception", + "//common:error_codes", + "//common/annotations", + ], +) + +java_library( + name = "iteration_budget_exceeded", + srcs = ["CelIterationLimitExceededException.java"], + # used_by_android + tags = [ + ], + deps = [ + ":runtime_exception", + "//common:error_codes", + "//common/annotations", + ], +) + +java_library( + name = "duplicate_key", + srcs = ["CelDuplicateKeyException.java"], + # used_by_android + tags = [ + ], + deps = [ + ":runtime_exception", + "//common:error_codes", + "//common/annotations", + ], +) + +java_library( + name = "overload_not_found", + srcs = ["CelOverloadNotFoundException.java"], + # used_by_android + tags = [ + ], + deps = [ + ":runtime_exception", + "//common:error_codes", + "//common/annotations", + ], +) diff --git a/common/src/main/java/dev/cel/common/exceptions/CelAttributeNotFoundException.java b/common/src/main/java/dev/cel/common/exceptions/CelAttributeNotFoundException.java new file mode 100644 index 000000000..d805c9cf4 --- /dev/null +++ b/common/src/main/java/dev/cel/common/exceptions/CelAttributeNotFoundException.java @@ -0,0 +1,61 @@ +// 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.exceptions; + +import dev.cel.common.CelErrorCode; +import dev.cel.common.annotations.Internal; +import java.util.Arrays; +import java.util.Collection; + +/** Indicates an attempt to access a map or object using an invalid attribute or key. */ +@Internal +public final class CelAttributeNotFoundException extends CelRuntimeException { + + public static CelAttributeNotFoundException of(String message) { + return new CelAttributeNotFoundException(message); + } + + public static CelAttributeNotFoundException forMissingMapKey(String key) { + return new CelAttributeNotFoundException(String.format("key '%s' is not present in map.", key)); + } + + public static CelAttributeNotFoundException forFieldResolution(String... fields) { + return forFieldResolution(Arrays.asList(fields)); + } + + public static CelAttributeNotFoundException forFieldResolution(Collection fields) { + return new CelAttributeNotFoundException(formatErrorMessage(fields)); + } + + public static CelAttributeNotFoundException forMissingAttributes(Collection attributes) { + return new CelAttributeNotFoundException( + "No such attribute(s): " + String.join(", ", attributes)); + } + + private static String formatErrorMessage(Collection fields) { + String maybePlural = ""; + if (fields.size() > 1) { + maybePlural = "s"; + } + + return String.format( + "Error resolving field%s '%s'. Field selections must be performed on messages or maps.", + maybePlural, String.join(", ", fields)); + } + + private CelAttributeNotFoundException(String message) { + super(message, CelErrorCode.ATTRIBUTE_NOT_FOUND); + } +} diff --git a/common/src/main/java/dev/cel/common/exceptions/CelBadFormatException.java b/common/src/main/java/dev/cel/common/exceptions/CelBadFormatException.java new file mode 100644 index 000000000..ba4db602a --- /dev/null +++ b/common/src/main/java/dev/cel/common/exceptions/CelBadFormatException.java @@ -0,0 +1,31 @@ +// 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.exceptions; + +import dev.cel.common.CelErrorCode; +import dev.cel.common.annotations.Internal; + +/** Indicates that a data conversion failed due to a mismatch in the format specification. */ +@Internal +public final class CelBadFormatException extends CelRuntimeException { + + public CelBadFormatException(Throwable cause) { + super(cause, CelErrorCode.BAD_FORMAT); + } + + public CelBadFormatException(String errorMessage) { + super(errorMessage, CelErrorCode.BAD_FORMAT); + } +} diff --git a/common/src/main/java/dev/cel/common/exceptions/CelDivideByZeroException.java b/common/src/main/java/dev/cel/common/exceptions/CelDivideByZeroException.java new file mode 100644 index 000000000..c507797c5 --- /dev/null +++ b/common/src/main/java/dev/cel/common/exceptions/CelDivideByZeroException.java @@ -0,0 +1,31 @@ +// 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.exceptions; + +import dev.cel.common.CelErrorCode; +import dev.cel.common.annotations.Internal; + +/** Indicates that a division by zero occurred. */ +@Internal +public final class CelDivideByZeroException extends CelRuntimeException { + + public CelDivideByZeroException() { + super("/ by zero", CelErrorCode.DIVIDE_BY_ZERO); + } + + public CelDivideByZeroException(Throwable cause) { + super(cause, CelErrorCode.DIVIDE_BY_ZERO); + } +} diff --git a/common/src/main/java/dev/cel/common/exceptions/CelDuplicateKeyException.java b/common/src/main/java/dev/cel/common/exceptions/CelDuplicateKeyException.java new file mode 100644 index 000000000..f4c99d516 --- /dev/null +++ b/common/src/main/java/dev/cel/common/exceptions/CelDuplicateKeyException.java @@ -0,0 +1,31 @@ +// 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.exceptions; + +import dev.cel.common.CelErrorCode; +import dev.cel.common.annotations.Internal; + +/** Indicates an attempt to create a map using duplicate keys. */ +@Internal +public final class CelDuplicateKeyException extends CelRuntimeException { + + public static CelDuplicateKeyException of(Object key) { + return new CelDuplicateKeyException(String.format("duplicate map key [%s]", key)); + } + + private CelDuplicateKeyException(String message) { + super(message, CelErrorCode.DUPLICATE_ATTRIBUTE); + } +} diff --git a/common/src/main/java/dev/cel/common/exceptions/CelIndexOutOfBoundsException.java b/common/src/main/java/dev/cel/common/exceptions/CelIndexOutOfBoundsException.java new file mode 100644 index 000000000..72a6cd1c0 --- /dev/null +++ b/common/src/main/java/dev/cel/common/exceptions/CelIndexOutOfBoundsException.java @@ -0,0 +1,27 @@ +// 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.exceptions; + +import dev.cel.common.CelErrorCode; +import dev.cel.common.annotations.Internal; + +/** Indicates that a list index access was attempted using an index that is out of bounds. */ +@Internal +public final class CelIndexOutOfBoundsException extends CelRuntimeException { + + public CelIndexOutOfBoundsException(Object index) { + super("Index out of bounds: " + index, CelErrorCode.INDEX_OUT_OF_BOUNDS); + } +} diff --git a/common/src/main/java/dev/cel/common/exceptions/CelInvalidArgumentException.java b/common/src/main/java/dev/cel/common/exceptions/CelInvalidArgumentException.java new file mode 100644 index 000000000..6a2b4ab72 --- /dev/null +++ b/common/src/main/java/dev/cel/common/exceptions/CelInvalidArgumentException.java @@ -0,0 +1,31 @@ +// 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.exceptions; + +import dev.cel.common.CelErrorCode; +import dev.cel.common.annotations.Internal; + +/** Indicates that an invalid argument was supplied to a function. */ +@Internal +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/exceptions/CelIterationLimitExceededException.java b/common/src/main/java/dev/cel/common/exceptions/CelIterationLimitExceededException.java new file mode 100644 index 000000000..cfaa25f35 --- /dev/null +++ b/common/src/main/java/dev/cel/common/exceptions/CelIterationLimitExceededException.java @@ -0,0 +1,30 @@ +// 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.exceptions; + +import dev.cel.common.CelErrorCode; +import dev.cel.common.annotations.Internal; +import java.util.Locale; + +/** Indicates that the iteration budget for a comprehension has been exceeded. */ +@Internal +public final class CelIterationLimitExceededException extends CelRuntimeException { + + public CelIterationLimitExceededException(int budget) { + super( + String.format(Locale.US, "Iteration budget exceeded: %d", budget), + CelErrorCode.ITERATION_BUDGET_EXCEEDED); + } +} diff --git a/common/src/main/java/dev/cel/common/exceptions/CelNumericOverflowException.java b/common/src/main/java/dev/cel/common/exceptions/CelNumericOverflowException.java new file mode 100644 index 000000000..78fbe807e --- /dev/null +++ b/common/src/main/java/dev/cel/common/exceptions/CelNumericOverflowException.java @@ -0,0 +1,34 @@ +// 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.exceptions; + +import dev.cel.common.CelErrorCode; +import dev.cel.common.annotations.Internal; + +/** + * Indicates that a numeric overflow occurred due to arithmetic operations or conversions resulting + * in a value outside the representable range. + */ +@Internal +public final class CelNumericOverflowException extends CelRuntimeException { + + public CelNumericOverflowException(String message) { + super(message, CelErrorCode.NUMERIC_OVERFLOW); + } + + public CelNumericOverflowException(Throwable cause) { + super(cause, CelErrorCode.NUMERIC_OVERFLOW); + } +} diff --git a/common/src/main/java/dev/cel/common/exceptions/CelOverloadNotFoundException.java b/common/src/main/java/dev/cel/common/exceptions/CelOverloadNotFoundException.java new file mode 100644 index 000000000..d7c4a01a8 --- /dev/null +++ b/common/src/main/java/dev/cel/common/exceptions/CelOverloadNotFoundException.java @@ -0,0 +1,43 @@ +// 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.exceptions; + +import dev.cel.common.CelErrorCode; +import dev.cel.common.annotations.Internal; +import java.util.Collection; +import java.util.Collections; + +/** Indicates that a matching overload could not be found during function dispatch. */ +@Internal +public final class CelOverloadNotFoundException extends CelRuntimeException { + + public CelOverloadNotFoundException(String functionName) { + this(functionName, Collections.emptyList()); + } + + public CelOverloadNotFoundException(String functionName, Collection overloadIds) { + super(formatErrorMessage(functionName, overloadIds), CelErrorCode.OVERLOAD_NOT_FOUND); + } + + private static String formatErrorMessage(String functionName, Collection overloadIds) { + StringBuilder sb = new StringBuilder(); + sb.append("No matching overload for function '").append(functionName).append("'."); + if (!overloadIds.isEmpty()) { + sb.append(" Overload candidates: ").append(String.join(", ", overloadIds)); + } + + return sb.toString(); + } +} diff --git a/common/src/main/java/dev/cel/common/CelRuntimeException.java b/common/src/main/java/dev/cel/common/exceptions/CelRuntimeException.java similarity index 80% rename from common/src/main/java/dev/cel/common/CelRuntimeException.java rename to common/src/main/java/dev/cel/common/exceptions/CelRuntimeException.java index 6f194c474..c87e192bd 100644 --- a/common/src/main/java/dev/cel/common/CelRuntimeException.java +++ b/common/src/main/java/dev/cel/common/exceptions/CelRuntimeException.java @@ -12,8 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -package dev.cel.common; +package dev.cel.common.exceptions; +import dev.cel.common.CelErrorCode; import dev.cel.common.annotations.Internal; /** @@ -21,14 +22,16 @@ * *

Note: This is not to be confused with the notion of CEL Runtime. Use {@code * CelEvaluationException} instead to signify an evaluation error. - * - *

TODO: Make this class abstract and define specific exception classes that - * corresponds to the CelErrorCode. */ @Internal -public class CelRuntimeException extends RuntimeException { +public abstract class CelRuntimeException extends RuntimeException { private final CelErrorCode errorCode; + CelRuntimeException(String errorMessage, CelErrorCode errorCode) { + super(errorMessage); + this.errorCode = errorCode; + } + public CelRuntimeException(Throwable cause, CelErrorCode errorCode) { super(cause); this.errorCode = errorCode; diff --git a/common/src/main/java/dev/cel/common/formats/BUILD.bazel b/common/src/main/java/dev/cel/common/formats/BUILD.bazel new file mode 100644 index 000000000..e906c8ba2 --- /dev/null +++ b/common/src/main/java/dev/cel/common/formats/BUILD.bazel @@ -0,0 +1,81 @@ +load("@rules_java//java:defs.bzl", "java_library") + +package( + default_applicable_licenses = ["//:license"], + default_visibility = ["//common/formats:__pkg__"], +) + +java_library( + name = "yaml_helper", + srcs = [ + "YamlHelper.java", + ], + tags = [ + ], + deps = [ + ":parser_context", + "@maven//:com_google_guava_guava", + "@maven//:org_yaml_snakeyaml", + ], +) + +java_library( + name = "value_string", + srcs = [ + "ValueString.java", + ], + tags = [ + ], + deps = [ + "//:auto_value", + ], +) + +java_library( + name = "parser_context", + srcs = [ + "ParserContext.java", + ], + tags = [ + ], + deps = [ + ":value_string", + "//common:compiler_common", + ], +) + +java_library( + name = "yaml_parser_context_impl", + srcs = [ + "YamlParserContextImpl.java", + ], + tags = [ + ], + deps = [ + "//common:compiler_common", + "//common:source", + "//common:source_location", + "//common/annotations", + "//common/formats:parser_context", + "//common/formats:value_string", + "//common/formats:yaml_helper", + "@maven//:com_google_guava_guava", + "@maven//:org_yaml_snakeyaml", + ], +) + +java_library( + name = "file_source", + srcs = ["CelFileSource.java"], + tags = [ + ], + deps = [ + "//:auto_value", + "//common:cel_source_helper", + "//common:source", + "//common:source_location", + "//common/internal", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) diff --git a/common/src/main/java/dev/cel/common/formats/CelFileSource.java b/common/src/main/java/dev/cel/common/formats/CelFileSource.java new file mode 100644 index 000000000..7f946d1b6 --- /dev/null +++ b/common/src/main/java/dev/cel/common/formats/CelFileSource.java @@ -0,0 +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.common.formats; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableMap; +import com.google.errorprone.annotations.CheckReturnValue; +import dev.cel.common.CelSourceHelper; +import dev.cel.common.CelSourceLocation; +import dev.cel.common.Source; +import dev.cel.common.internal.CelCodePointArray; +import java.util.Map; +import java.util.Optional; + +/** + * CelFileSource represents the source content of a generic configuration and its related metadata. + * This object is amenable to being serialized into YAML, textproto or other formats as needed. + */ +@AutoValue +public abstract class CelFileSource implements Source { + + @Override + public abstract CelCodePointArray getContent(); + + @Override + public abstract String getDescription(); + + @Override + public abstract ImmutableMap getPositionsMap(); + + @Override + public Optional getSnippet(int line) { + return CelSourceHelper.getSnippet(getContent(), line); + } + + /** + * Get the line and column in the source expression text for the given code point {@code offset}. + */ + public Optional getOffsetLocation(int offset) { + return CelSourceHelper.getOffsetLocation(getContent(), offset); + } + + /** Builder for {@link CelFileSource}. */ + @AutoValue.Builder + public abstract static class Builder { + + public abstract Builder setContent(CelCodePointArray content); + + public abstract Builder setDescription(String description); + + public abstract Builder setPositionsMap(Map value); + + @CheckReturnValue + public abstract CelFileSource build(); + } + + public abstract Builder toBuilder(); + + public static Builder newBuilder(CelCodePointArray celCodePointArray) { + return new AutoValue_CelFileSource.Builder() + .setDescription("") + .setContent(celCodePointArray) + .setPositionsMap(ImmutableMap.of()); + } +} diff --git a/common/src/main/java/dev/cel/common/formats/ParserContext.java b/common/src/main/java/dev/cel/common/formats/ParserContext.java new file mode 100644 index 000000000..17eff473f --- /dev/null +++ b/common/src/main/java/dev/cel/common/formats/ParserContext.java @@ -0,0 +1,73 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.common.formats; + +import dev.cel.common.CelIssue; +import java.util.List; +import java.util.Map; + +/** + * ParserContext declares a set of interfaces for managing metadata, such as node IDs, parsing + * errors and source offsets. + */ +public interface ParserContext { + + /** + * NextID returns a monotonically increasing identifier for a source fragment. This ID is + * implicitly created and tracked within the CollectMetadata method. + */ + long nextId(); + + /** + * CollectMetadata records the source position information of a given node, and returns the id + * associated with the source metadata which is returned in the Policy SourceInfo object. + */ + long collectMetadata(T node); + + void reportError(long id, String message); + + List getIssues(); + + Map getIdToOffsetMap(); + + /** + * @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/ValueString.java b/common/src/main/java/dev/cel/common/formats/ValueString.java new file mode 100644 index 000000000..86df60a88 --- /dev/null +++ b/common/src/main/java/dev/cel/common/formats/ValueString.java @@ -0,0 +1,55 @@ +// 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. + +package dev.cel.common.formats; + +import com.google.auto.value.AutoValue; + +/** ValueString contains an identifier corresponding to source metadata and a simple string. */ +@AutoValue +public abstract class ValueString { + + /** A unique identifier. This is populated by the parser. */ + public abstract long id(); + + /** String value of the {@code ValueString} */ + public abstract String value(); + + /** Builder for {@link ValueString}. */ + @AutoValue.Builder + public abstract static class Builder { + + /** Set the identifier for the string to associate it back to collected source metadata. */ + public abstract Builder setId(long id); + + /** Set the string value. */ + public abstract Builder setValue(String value); + + /** Build the {@code ValueString}. */ + public abstract ValueString build(); + } + + /** Convert the {@code ValueString} to a {@code Builder}. */ + public abstract Builder toBuilder(); + + /** Builder for {@link ValueString}. */ + public static Builder newBuilder() { + return new AutoValue_ValueString.Builder().setId(0).setValue(""); + } + + /** Creates a new {@link ValueString} instance with the specified ID and string value. */ + public static ValueString of(long id, String value) { + return newBuilder().setId(id).setValue(value).build(); + } +} diff --git a/common/src/main/java/dev/cel/common/formats/YamlHelper.java b/common/src/main/java/dev/cel/common/formats/YamlHelper.java new file mode 100644 index 000000000..c16126f95 --- /dev/null +++ b/common/src/main/java/dev/cel/common/formats/YamlHelper.java @@ -0,0 +1,143 @@ +// 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. + +package dev.cel.common.formats; + +import static com.google.common.collect.ImmutableMap.toImmutableMap; +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.joining; + +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableMap; +import java.io.StringReader; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.SafeConstructor; +import org.yaml.snakeyaml.nodes.Node; +import org.yaml.snakeyaml.nodes.ScalarNode; + +/** Helper class for parsing YAML. */ +public final class YamlHelper { + public static final String ERROR = "*error*"; + + /** Enum for YAML node types. */ + public enum YamlNodeType { + MAP("tag:yaml.org,2002:map"), + STRING("tag:yaml.org,2002:str"), + BOOLEAN("tag:yaml.org,2002:bool"), + INTEGER("tag:yaml.org,2002:int"), + DOUBLE("tag:yaml.org,2002:float"), + TEXT("!txt"), + LIST("tag:yaml.org,2002:seq"), + ; + + private static final ImmutableMap TAG_TO_NODE_TYPE = + stream(YamlNodeType.values()) + .collect(toImmutableMap(YamlNodeType::tag, Function.identity())); + + private final String tag; + + public static Optional nodeType(String tag) { + return Optional.ofNullable(TAG_TO_NODE_TYPE.get(tag)); + } + + public String tag() { + return tag; + } + + YamlNodeType(String tag) { + this.tag = tag; + } + } + + /** Assert that a given YAML node matches one of the provided {@code YamlNodeType} values. */ + public static boolean assertYamlType( + ParserContext ctx, long id, Node node, YamlNodeType... expectedNodeTypes) { + if (validateYamlType(node, expectedNodeTypes)) { + return true; + } + String nodeTag = node.getTag().getValue(); + + ctx.reportError( + id, + String.format( + "Got yaml node type %s, wanted type(s) [%s]", + nodeTag, stream(expectedNodeTypes).map(YamlNodeType::tag).collect(joining(" ")))); + return false; + } + + public static Optional parseYamlSource(String policyContent) { + Yaml yaml = new Yaml(new SafeConstructor(new LoaderOptions())); + return Optional.ofNullable(yaml.compose(new StringReader(policyContent))); + } + + public static boolean assertRequiredFields( + ParserContext ctx, long id, List missingRequiredFields) { + if (missingRequiredFields.isEmpty()) { + return true; + } + + ctx.reportError( + id, + String.format( + "Missing required attribute(s): %s", Joiner.on(", ").join(missingRequiredFields))); + return false; + } + + public static boolean validateYamlType(Node node, YamlNodeType... expectedNodeTypes) { + String nodeTag = node.getTag().getValue(); + for (YamlNodeType expectedNodeType : expectedNodeTypes) { + if (expectedNodeType.tag().equals(nodeTag)) { + return true; + } + } + return false; + } + + public static Double newDouble(ParserContext ctx, Node node) { + long id = ctx.collectMetadata(node); + if (!assertYamlType(ctx, id, node, YamlNodeType.DOUBLE)) { + return 0.0; + } + + return Double.parseDouble(((ScalarNode) node).getValue()); + } + + public static Integer newInteger(ParserContext ctx, Node node) { + long id = ctx.collectMetadata(node); + if (!assertYamlType(ctx, id, node, YamlNodeType.INTEGER)) { + return 0; + } + + return Integer.parseInt(((ScalarNode) node).getValue()); + } + + public static boolean newBoolean(ParserContext ctx, Node node) { + long id = ctx.collectMetadata(node); + if (!assertYamlType(ctx, id, node, YamlNodeType.BOOLEAN)) { + return false; + } + + return Boolean.parseBoolean(((ScalarNode) node).getValue()); + } + + public static String newString(ParserContext ctx, Node node) { + 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 new file mode 100644 index 000000000..9f6077562 --- /dev/null +++ b/common/src/main/java/dev/cel/common/formats/YamlParserContextImpl.java @@ -0,0 +1,168 @@ +// 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. + +package dev.cel.common.formats; + +import static dev.cel.common.formats.YamlHelper.ERROR; +import static dev.cel.common.formats.YamlHelper.assertYamlType; + +import com.google.common.base.Strings; +import dev.cel.common.CelIssue; +import dev.cel.common.CelSourceLocation; +import dev.cel.common.Source; +import dev.cel.common.annotations.Internal; +import dev.cel.common.formats.YamlHelper.YamlNodeType; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.DumperOptions.ScalarStyle; +import org.yaml.snakeyaml.nodes.Node; +import org.yaml.snakeyaml.nodes.ScalarNode; + +/** + * Class to assist with storing generic configuration parsing context. + * + *

CEL Library Internals. Do not use. + */ +@Internal +public final class YamlParserContextImpl implements ParserContext { + + private final ArrayList issues; + private final HashMap idToLocationMap; + private final HashMap idToOffsetMap; + private final Source policySource; + private long id; + + @Override + public void reportError(long id, String message) { + issues.add(CelIssue.formatError(idToLocationMap.get(id), message)); + } + + @Override + public List getIssues() { + return issues; + } + + @Override + public Map getIdToOffsetMap() { + return idToOffsetMap; + } + + @Override + 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); + } + + ScalarNode scalarNode = (ScalarNode) node; + ScalarStyle style = scalarNode.getScalarStyle(); + if (style.equals(ScalarStyle.FOLDED) || style.equals(ScalarStyle.LITERAL)) { + CelSourceLocation location = idToLocationMap.get(id); + int line = location.getLine(); + int column = location.getColumn(); + + String indent = Strings.padStart("", column, ' '); + String text = policySource.getSnippet(line).orElse(""); + StringBuilder raw = new StringBuilder(); + while (text.startsWith(indent)) { + line++; + raw.append(text); + text = policySource.getSnippet(line).orElse(""); + if (text.isEmpty()) { + break; + } + if (text.startsWith(indent)) { + raw.append("\n"); + } + } + + idToOffsetMap.compute(id, (k, offset) -> offset - column); + + return ValueString.of(id, raw.toString()); + } + + return ValueString.of(id, scalarNode.getValue()); + } + + @Override + public long collectMetadata(Node node) { + long id = nextId(); + int line = node.getStartMark().getLine() + 1; // Yaml lines are 0 indexed + int column = node.getStartMark().getColumn(); + if (node instanceof ScalarNode) { + DumperOptions.ScalarStyle style = ((ScalarNode) node).getScalarStyle(); + switch (style) { + case SINGLE_QUOTED: + case DOUBLE_QUOTED: + column++; + break; + case LITERAL: + case FOLDED: + // For multi-lines, actual string content begins on next line + line++; + // Columns must be computed from the indentation + column = 0; + String snippet = policySource.getSnippet(line).orElse(""); + for (char c : snippet.toCharArray()) { + if (!Character.isWhitespace(c)) { + break; + } + column++; + } + break; + default: + break; + } + } + idToLocationMap.put(id, CelSourceLocation.of(line, column)); + + int offset = 0; + if (line > 1) { + offset = policySource.getContent().lineOffsets().get(line - 2) + column; + } + idToOffsetMap.put(id, offset); + + return id; + } + + @Override + public long nextId() { + return ++id; + } + + public static ParserContext newInstance(Source source) { + return new YamlParserContextImpl(source); + } + + private YamlParserContextImpl(Source source) { + this.issues = new ArrayList<>(); + this.idToLocationMap = new HashMap<>(); + this.idToOffsetMap = new HashMap<>(); + this.policySource = source; + } +} diff --git a/common/src/main/java/dev/cel/common/internal/AdaptingTypes.java b/common/src/main/java/dev/cel/common/internal/AdaptingTypes.java index 53fe59503..48158bd91 100644 --- a/common/src/main/java/dev/cel/common/internal/AdaptingTypes.java +++ b/common/src/main/java/dev/cel/common/internal/AdaptingTypes.java @@ -23,7 +23,7 @@ import java.util.ListIterator; import java.util.Map; import java.util.Set; -import org.jspecify.nullness.Nullable; +import org.jspecify.annotations.Nullable; /** * Collection of types which support bidirectional adaptation between CEL and Java native value 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 e373f442b..58b15b103 100644 --- a/common/src/main/java/dev/cel/common/internal/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/internal/BUILD.bazel @@ -1,9 +1,13 @@ +load("@rules_java//java:defs.bzl", "java_library") +load("//:cel_android_rules.bzl", "cel_android_library") + package( default_applicable_licenses = [ "//:license", ], default_visibility = [ "//common/internal:__pkg__", + "//publish:__pkg__", ], ) @@ -11,7 +15,6 @@ package( INTERNAL_SOURCES = [ "BasicCodePointArray.java", "CelCodePointArray.java", - "CodePointStream.java", "Constants.java", "EmptyCodePointArray.java", "Latin1CodePointArray.java", @@ -31,6 +34,19 @@ CEL_DESCRIPTOR_POOL_SOURCES = [ "DefaultDescriptorPool.java", ] +java_library( + name = "code_point_stream", + srcs = ["CodePointStream.java"], + tags = [ + ], + deps = [ + ":internal", + "//common/annotations", + "@maven//:com_google_guava_guava", + "@maven//:org_antlr_antlr4_runtime", + ], +) + java_library( name = "internal", srcs = INTERNAL_SOURCES, @@ -40,10 +56,29 @@ java_library( "//:auto_value", "//common/annotations", "//common/ast", + "//common/values", + "//common/values:cel_byte_string", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", - "@maven//:org_antlr_antlr4_runtime", + ], +) + +cel_android_library( + name = "internal_android", + srcs = INTERNAL_SOURCES, + tags = [ + ], + deps = [ + "//:auto_value", + "//common/annotations", + "//common/ast:ast_android", + "//common/values:cel_byte_string", + "//common/values:values_android", + "@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", ], ) @@ -60,6 +95,18 @@ java_library( ], ) +cel_android_library( + name = "comparison_functions_android", + srcs = ["ComparisonFunctions.java"], + tags = [ + ], + deps = [ + "//common/annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", + ], +) + java_library( name = "converter", srcs = [ @@ -67,6 +114,7 @@ java_library( "BidiConverter.java", "Converter.java", ], + # used_by_android tags = [ ], deps = [ @@ -98,17 +146,32 @@ DYNAMIC_PROTO_SOURCES = [ java_library( name = "default_instance_message_factory", - srcs = ["DefaultInstanceMessageFactory.java"], + srcs = [ + "DefaultInstanceMessageFactory.java", + "DefaultInstanceMessageLiteFactory.java", + ], tags = [ ], deps = [ + ":reflection_util", "//common/annotations", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", ], ) -# keep sorted +java_library( + name = "default_instance_message_lite_factory", + srcs = ["DefaultInstanceMessageLiteFactory.java"], + tags = [ + ], + deps = [ + ":reflection_util", + "//common/annotations", + "@maven//:com_google_guava_guava", + "@maven_android//:com_google_protobuf_protobuf_javalite", + ], +) java_library( name = "dynamic_proto", @@ -117,18 +180,38 @@ java_library( ], deps = [ ":converter", + ":proto_lite_adapter", ":proto_message_factory", ":well_known_proto", - "//:auto_value", - "//common:error_codes", - "//common:runtime_exception", + "//common:options", "//common/annotations", - "@cel_spec//proto/cel/expr:expr_java_proto", + "//common/exceptions:numeric_overflow", + "//common/values", + "//common/values:cel_byte_string", "@maven//:com_google_code_findbugs_annotations", "@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 = "proto_lite_adapter", + srcs = ["ProtoLiteAdapter.java"], + tags = [ + ], + deps = [ + ":well_known_proto", + "//common:options", + "//common:proto_json_adapter", + "//common/annotations", + "//common/exceptions:numeric_overflow", + "//common/internal:proto_time_utils", + "//common/values", + "//common/values:cel_byte_string", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", ], ) @@ -156,7 +239,9 @@ java_library( tags = [ ], deps = [ + ":cel_descriptor_pools", "//:auto_value", + "//common/internal:well_known_proto", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", @@ -170,7 +255,8 @@ java_library( ], deps = [ "//common/annotations", - "@cel_spec//proto/cel/expr:expr_java_proto", + "//parser:macro", + "@cel_spec//proto/cel/expr:checked_java_proto", ], ) @@ -186,6 +272,18 @@ java_library( ], ) +cel_android_library( + name = "well_known_proto_android", + srcs = ["WellKnownProto.java"], + tags = [ + ], + deps = [ + "//common/annotations", + "@maven_android//:com_google_guava_guava", + "@maven_android//:com_google_protobuf_protobuf_javalite", + ], +) + java_library( name = "default_message_factory", srcs = ["DefaultMessageFactory.java"], @@ -220,7 +318,7 @@ java_library( ], deps = [ ":well_known_proto", - "//common", + "//common:cel_descriptors", "//common/annotations", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", @@ -228,9 +326,67 @@ java_library( ], ) +java_library( + name = "cel_lite_descriptor_pool", + srcs = ["CelLiteDescriptorPool.java"], + tags = [ + ], + deps = [ + "//protobuf:cel_lite_descriptor", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_protobuf_protobuf_javalite", + ], +) + +cel_android_library( + name = "cel_lite_descriptor_pool_android", + srcs = ["CelLiteDescriptorPool.java"], + tags = [ + ], + deps = [ + "//protobuf:cel_lite_descriptor", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_protobuf_protobuf_javalite", + ], +) + +java_library( + name = "default_lite_descriptor_pool", + srcs = ["DefaultLiteDescriptorPool.java"], + tags = [ + ], + deps = [ + ":cel_lite_descriptor_pool", + ":well_known_proto", + "//common/annotations", + "//protobuf:cel_lite_descriptor", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + "@maven_android//:com_google_protobuf_protobuf_javalite", + ], +) + +cel_android_library( + name = "default_lite_descriptor_pool_android", + srcs = ["DefaultLiteDescriptorPool.java"], + tags = [ + ], + deps = [ + ":cel_lite_descriptor_pool_android", + ":well_known_proto_android", + "//common/annotations", + "//protobuf:cel_lite_descriptor", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", + "@maven_android//:com_google_protobuf_protobuf_javalite", + ], +) + java_library( name = "safe_string_formatter", srcs = ["SafeStringFormatter.java"], + # used_by_android tags = [ ], deps = [ @@ -238,3 +394,67 @@ java_library( "@maven//:com_google_re2j_re2j", ], ) + +java_library( + name = "reflection_util", + srcs = ["ReflectionUtil.java"], + tags = [ + ], + deps = [ + "//common/annotations", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "proto_time_utils", + srcs = ["ProtoTimeUtils.java"], + tags = [ + ], + deps = [ + "//common/annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + ], +) + +cel_android_library( + name = "proto_time_utils_android", + srcs = ["ProtoTimeUtils.java"], + tags = [ + ], + deps = [ + "//common/annotations", + "@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", + ], +) + +java_library( + name = "date_time_helpers", + srcs = ["DateTimeHelpers.java"], + tags = [ + ], + deps = [ + "//common/annotations", + "//common/exceptions:bad_format", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + ], +) + +cel_android_library( + name = "date_time_helpers_android", + srcs = ["DateTimeHelpers.java"], + tags = [ + ], + deps = [ + "//common/annotations", + "//common/exceptions:bad_format", + "@maven_android//:com_google_guava_guava", + "@maven_android//:com_google_protobuf_protobuf_javalite", + ], +) 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 f4f9ef6c3..a54fb65d7 100644 --- a/common/src/main/java/dev/cel/common/internal/BasicCodePointArray.java +++ b/common/src/main/java/dev/cel/common/internal/BasicCodePointArray.java @@ -18,7 +18,8 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkPositionIndexes; -import com.google.common.annotations.VisibleForTesting; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.Immutable; import dev.cel.common.annotations.Internal; @@ -29,45 +30,41 @@ *

CEL Library Internals. Do Not Use. */ @Immutable -@VisibleForTesting @Internal -public final class BasicCodePointArray extends CelCodePointArray { +@AutoValue +@AutoValue.CopyAnnotations +@SuppressWarnings("Immutable") // char[] is not exposed externally, thus cannot be mutated. +public abstract class BasicCodePointArray extends CelCodePointArray { - @SuppressWarnings("Immutable") - private final char[] codePoints; + @SuppressWarnings("mutable") + abstract char[] codePoints(); - private final int offset; - private final int size; + abstract int offset(); - BasicCodePointArray(char[] codePoints, int size) { - this(codePoints, 0, size); + static BasicCodePointArray create( + char[] codePoints, int size, ImmutableList lineOffsets) { + return create(codePoints, 0, lineOffsets, size); } - BasicCodePointArray(char[] codePoints, int offset, int size) { - this.codePoints = checkNotNull(codePoints); - this.offset = offset; - this.size = size; + static BasicCodePointArray create( + char[] codePoints, int offset, ImmutableList lineOffsets, int size) { + return new AutoValue_BasicCodePointArray(size, checkNotNull(lineOffsets), codePoints, offset); } @Override public BasicCodePointArray slice(int i, int j) { checkPositionIndexes(i, j, size()); - return new BasicCodePointArray(codePoints, offset + i, j - i); + return create(codePoints(), offset() + i, lineOffsets(), j - i); } @Override public int get(int index) { checkElementIndex(index, size()); - return codePoints[offset + index] & 0xffff; + return codePoints()[offset() + index] & 0xffff; } @Override - public int size() { - return size; - } - - @Override - public String toString() { - return new String(codePoints, offset, size); + public final String toString() { + return new String(codePoints(), offset(), size()); } } 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 178b2ac55..1f3124c93 100644 --- a/common/src/main/java/dev/cel/common/internal/CelCodePointArray.java +++ b/common/src/main/java/dev/cel/common/internal/CelCodePointArray.java @@ -16,6 +16,7 @@ import static com.google.common.base.Strings.isNullOrEmpty; +import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.Immutable; import dev.cel.common.annotations.Internal; import java.util.PrimitiveIterator; @@ -41,6 +42,9 @@ public abstract class CelCodePointArray { /** Returns the number of code points. */ public abstract int size(); + /** Returns the line offsets. */ + public abstract ImmutableList lineOffsets(); + public final int length() { return size(); } @@ -60,8 +64,11 @@ public static CelCodePointArray fromString(String text) { PrimitiveIterator.OfInt codePoints = text.codePoints().iterator(); byte[] byteArray = new byte[text.length()]; int byteIndex = 0; + + LineOffsetContext lineOffsetContext = new LineOffsetContext(); while (codePoints.hasNext()) { int codePoint = codePoints.nextInt(); + lineOffsetContext.process(codePoint); if (codePoint <= 0xff) { byteArray[byteIndex++] = (byte) codePoint; continue; @@ -76,6 +83,7 @@ public static CelCodePointArray fromString(String text) { charArray[charIndex++] = (char) codePoint; while (codePoints.hasNext()) { codePoint = codePoints.nextInt(); + lineOffsetContext.process(codePoint); if (codePoint <= 0xffff) { charArray[charIndex++] = (char) codePoint; continue; @@ -89,11 +97,16 @@ public static CelCodePointArray fromString(String text) { intArray[intIndex++] = codePoint; while (codePoints.hasNext()) { codePoint = codePoints.nextInt(); + lineOffsetContext.process(codePoint); intArray[intIndex++] = codePoint; } - return new SupplementalCodePointArray(intArray, intIndex); + + return SupplementalCodePointArray.create( + intArray, intIndex, lineOffsetContext.buildLineOffsets()); } - return new BasicCodePointArray(charArray, charIndex); + + return BasicCodePointArray.create( + charArray, charIndex, lineOffsetContext.buildLineOffsets()); } int[] intArray = new int[text.length()]; int intIndex = 0; @@ -104,10 +117,36 @@ public static CelCodePointArray fromString(String text) { intArray[intIndex++] = codePoint; while (codePoints.hasNext()) { codePoint = codePoints.nextInt(); + lineOffsetContext.process(codePoint); intArray[intIndex++] = codePoint; } - return new SupplementalCodePointArray(intArray, intIndex); + + return SupplementalCodePointArray.create( + intArray, intIndex, lineOffsetContext.buildLineOffsets()); + } + + return Latin1CodePointArray.create(byteArray, byteIndex, lineOffsetContext.buildLineOffsets()); + } + + private static class LineOffsetContext { + private static final int NEWLINE_CODE_POINT = 10; + + private final ImmutableList.Builder lineOffsetBuilder; + private int lineOffsetCodePoints; + + private void process(int codePoint) { + lineOffsetCodePoints++; + if (codePoint == NEWLINE_CODE_POINT) { + lineOffsetBuilder.add(lineOffsetCodePoints); + } + } + + private ImmutableList buildLineOffsets() { + return lineOffsetBuilder.add(lineOffsetCodePoints + 1).build(); + } + + private LineOffsetContext() { + this.lineOffsetBuilder = ImmutableList.builder(); } - return new Latin1CodePointArray(byteArray, byteIndex); } } diff --git a/common/src/main/java/dev/cel/common/values/TypeValue.java b/common/src/main/java/dev/cel/common/internal/CelLiteDescriptorPool.java similarity index 51% rename from common/src/main/java/dev/cel/common/values/TypeValue.java rename to common/src/main/java/dev/cel/common/internal/CelLiteDescriptorPool.java index cacffcd48..73fce4fbb 100644 --- a/common/src/main/java/dev/cel/common/values/TypeValue.java +++ b/common/src/main/java/dev/cel/common/internal/CelLiteDescriptorPool.java @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// 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. @@ -12,30 +12,21 @@ // See the License for the specific language governing permissions and // limitations under the License. -package dev.cel.common.values; +package dev.cel.common.internal; -import com.google.auto.value.AutoValue; import com.google.errorprone.annotations.Immutable; -import dev.cel.common.types.CelType; -import dev.cel.common.types.TypeType; +import com.google.protobuf.MessageLite; +import dev.cel.protobuf.CelLiteDescriptor.MessageLiteDescriptor; +import java.util.Optional; -/** TypeValue holds the CEL type information for the underlying CelValue. */ -@AutoValue +/** + * CelLiteDescriptorPool allows lookup of {@link MessageLiteDescriptor} by its fully qualified name. + */ @Immutable -public abstract class TypeValue extends CelValue { +public interface CelLiteDescriptorPool { + Optional findDescriptor(String protoTypeName); - @Override - public abstract CelType value(); + Optional findDescriptor(MessageLite messageLite); - @Override - public boolean isZeroValue() { - return false; - } - - @Override - public abstract TypeType celType(); - - public static TypeValue create(CelType value) { - return new AutoValue_TypeValue(value, TypeType.create(value)); - } + MessageLiteDescriptor getDescriptorOrThrow(String protoTypeName); } diff --git a/common/src/main/java/dev/cel/common/internal/ComparisonFunctions.java b/common/src/main/java/dev/cel/common/internal/ComparisonFunctions.java index f70e3b6e4..4d498d526 100644 --- a/common/src/main/java/dev/cel/common/internal/ComparisonFunctions.java +++ b/common/src/main/java/dev/cel/common/internal/ComparisonFunctions.java @@ -15,6 +15,7 @@ package dev.cel.common.internal; import com.google.common.primitives.UnsignedLong; +import com.google.common.primitives.UnsignedLongs; import com.google.errorprone.annotations.CheckReturnValue; import dev.cel.common.annotations.Internal; @@ -80,7 +81,8 @@ public static int compareUintInt(UnsignedLong ul, long l) { public static boolean numericEquals(Number x, Number y) { if (x instanceof Double) { if (y instanceof Double) { - return !(Double.isNaN((Double) x) || Double.isNaN((Double) y)) && x.equals(y); + return !(Double.isNaN((Double) x) || Double.isNaN((Double) y)) + && x.doubleValue() == y.doubleValue(); } if (y instanceof Long) { return compareDoubleInt((Double) x, (Long) y) == 0; @@ -114,5 +116,44 @@ public static boolean numericEquals(Number x, Number y) { return false; } + /** Compare two numeric values of any type (double, int, uint). */ + public static int numericCompare(Number x, Number y) { + if (x instanceof Double) { + if (y instanceof Double) { + return ((Double) x).compareTo((Double) y); + } + if (y instanceof Long) { + return compareDoubleInt((Double) x, (Long) y); + } + if (y instanceof UnsignedLong) { + return compareDoubleUint((Double) x, (UnsignedLong) y); + } + } + if (x instanceof Long) { + if (y instanceof Long) { + return Long.compare((Long) x, (Long) y); + } + if (y instanceof Double) { + return compareIntDouble((Long) x, (Double) y); + } + if (y instanceof UnsignedLong) { + return compareIntUint((Long) x, (UnsignedLong) y); + } + } + if (x instanceof UnsignedLong) { + if (y instanceof UnsignedLong) { + return UnsignedLongs.compare(x.longValue(), y.longValue()); + } + if (y instanceof Double) { + return compareUintDouble((UnsignedLong) x, (Double) y); + } + if (y instanceof Long) { + return compareUintInt((UnsignedLong) x, (Long) y); + } + } + throw new UnsupportedOperationException( + "Unsupported argument types: " + x.getClass() + ", " + y.getClass()); + } + private ComparisonFunctions() {} } 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 f699639d4..49bca7489 100644 --- a/common/src/main/java/dev/cel/common/internal/Constants.java +++ b/common/src/main/java/dev/cel/common/internal/Constants.java @@ -19,14 +19,15 @@ import com.google.common.primitives.UnsignedLong; import com.google.protobuf.ByteString; -import com.google.protobuf.NullValue; import dev.cel.common.annotations.Internal; import dev.cel.common.ast.CelConstant; +import dev.cel.common.values.CelByteString; +import dev.cel.common.values.NullValue; import java.text.ParseException; import java.util.PrimitiveIterator; /** - * Internal utility class for working with {@link com.google.api.expr.Constant}. + * Internal utility class for working with {@link dev.cel.expr.Constant}. * *

CEL Library Internals. Do Not Use. */ @@ -153,7 +154,7 @@ public static CelConstant parseBytes(String text) throws ParseException { text = text.substring(0, text.length() - quote.length()); DecodeBuffer buffer = new DecodeByteStringBuffer(text.length()); decodeString(offset, text, buffer, isRawLiteral, true); - return CelConstant.ofValue(buffer.toDecodedValue()); + return CelConstant.ofValue(CelByteString.of(buffer.toDecodedValue().toByteArray())); } public static CelConstant parseString(String text) throws ParseException { @@ -206,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'. @@ -230,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; } @@ -497,7 +504,15 @@ private static void checkForClosingQuote(String text, String quote) throws Parse while (position + quote.length() <= text.length()) { char codeUnit = text.charAt(position); if (codeUnit != '\\') { - if (text.substring(position).startsWith(quote)) { + boolean quoteMatches = true; + for (int i = 0; i < quote.length(); i++) { + if (text.charAt(position + i) != quote.charAt(i)) { + quoteMatches = false; + break; + } + } + + if (quoteMatches) { isClosed = position + quote.length() == text.length(); break; } diff --git a/common/src/main/java/dev/cel/common/internal/DateTimeHelpers.java b/common/src/main/java/dev/cel/common/internal/DateTimeHelpers.java new file mode 100644 index 000000000..805a9bd75 --- /dev/null +++ b/common/src/main/java/dev/cel/common/internal/DateTimeHelpers.java @@ -0,0 +1,260 @@ +// 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.common.base.Strings; +import com.google.protobuf.Timestamp; +import dev.cel.common.annotations.Internal; +import dev.cel.common.exceptions.CelBadFormatException; +import java.time.DateTimeException; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.util.Locale; + +/** Collection of utility methods for CEL datetime handlings. */ +@Internal +@SuppressWarnings("JavaInstantGetSecondsGetNano") // Intended within CEL. +public final class DateTimeHelpers { + public static final String UTC = "UTC"; + + // Timestamp for "0001-01-01T00:00:00Z" + private static final long TIMESTAMP_SECONDS_MIN = -62135596800L; + // Timestamp for "9999-12-31T23:59:59Z" + private static final long TIMESTAMP_SECONDS_MAX = 253402300799L; + + private static final long DURATION_SECONDS_MIN = -315576000000L; + private static final long DURATION_SECONDS_MAX = 315576000000L; + private static final int NANOS_PER_SECOND = 1000000000; + + /** + * Constructs a new {@link LocalDateTime} instance + * + * @param ts Timestamp protobuf object + * @param tz Timezone based on the CEL specification. This is either the canonical name from tz + * database or a standard offset represented in (+/-)HH:MM. Few valid examples are: + *

+ * + * @return If an Invalid timezone is supplied. + */ + public static LocalDateTime newLocalDateTime(Timestamp ts, String tz) { + return Instant.ofEpochSecond(ts.getSeconds(), ts.getNanos()) + .atZone(timeZone(tz)) + .toLocalDateTime(); + } + + /** + * Constructs a new {@link LocalDateTime} instance from a Java Instant. + * + * @param instant Instant object + * @param tz Timezone based on the CEL specification. This is either the canonical name from tz + * database or a standard offset represented in (+/-)HH:MM. Few valid examples are: + *
    + *
  • UTC + *
  • America/Los_Angeles + *
  • -09:30 or -9:30 (Leading zeroes can be omitted though not allowed by spec) + *
+ * + * @return A new {@link LocalDateTime} instance. + */ + public static LocalDateTime newLocalDateTime(Instant instant, String tz) { + return instant.atZone(timeZone(tz)).toLocalDateTime(); + } + + /** + * Parse from RFC 3339 date string to {@link java.time.Instant}. + * + *

Example of accepted format: "1972-01-01T10:00:20.021-05:00" + */ + public static Instant parse(String text) { + OffsetDateTime offsetDateTime = OffsetDateTime.parse(text); + Instant instant = offsetDateTime.toInstant(); + checkValid(instant); + + return instant; + } + + /** Adds a duration to an instant. */ + public static Instant add(Instant ts, Duration dur) { + Instant newInstant = ts.plus(dur); + checkValid(newInstant); + + return newInstant; + } + + /** Adds two durations */ + public static Duration add(Duration d1, Duration d2) { + Duration newDuration = d1.plus(d2); + checkValid(newDuration); + + return newDuration; + } + + /** Subtracts a duration to an instant. */ + public static Instant subtract(Instant ts, Duration dur) { + Instant newInstant = ts.minus(dur); + checkValid(newInstant); + + return newInstant; + } + + /** Subtract a duration from another. */ + public static Duration subtract(Duration d1, Duration d2) { + Duration newDuration = d1.minus(d2); + checkValid(newDuration); + + return newDuration; + } + + /** + * Formats a {@link Duration} into a minimal seconds-based representation. + * + *

Note: follows {@code ProtoTimeUtils#toString(Duration)} implementation + */ + public static String toString(Duration duration) { + if (duration.isZero()) { + return "0s"; + } + + long totalNanos = duration.toNanos(); + StringBuilder sb = new StringBuilder(); + + if (totalNanos < 0) { + sb.append('-'); + totalNanos = -totalNanos; + } + + long seconds = totalNanos / 1_000_000_000; + int nanos = (int) (totalNanos % 1_000_000_000); + + sb.append(seconds); + + // Follows ProtoTimeUtils.toString(Duration) implementation + if (nanos > 0) { + sb.append('.'); + if (nanos % 1_000_000 == 0) { + // Millisecond precision (3 digits) + int millis = nanos / 1_000_000; + sb.append(String.format(Locale.US, "%03d", millis)); + } else if (nanos % 1_000 == 0) { + // Microsecond precision (6 digits) + int micros = nanos / 1_000; + sb.append(String.format(Locale.US, "%06d", micros)); + } else { + // Nanosecond precision (9 digits) + sb.append(String.format(Locale.US, "%09d", nanos)); + } + } + + sb.append('s'); + return sb.toString(); + } + + /** + * Get the DateTimeZone Instance. + * + * @param tz the ID of the datetime zone + * @return the ZoneId object + */ + private static ZoneId timeZone(String tz) { + try { + return ZoneId.of(tz); + } catch (DateTimeException e) { + // If timezone is not a string name (for example, 'US/Central'), it should be a numerical + // offset from UTC in the format [+/-]HH:MM. + try { + int ind = tz.indexOf(":"); + if (ind == -1) { + throw new CelBadFormatException(e); + } + + int hourOffset = Integer.parseInt(tz.substring(0, ind)); + int minOffset = Integer.parseInt(tz.substring(ind + 1)); + // Ensures that the offset are properly formatted in [+/-]HH:MM to conform with + // ZoneOffset's format requirements. + // Example: "-9:30" -> "-09:30" and "9:30" -> "+09:30" + String formattedOffset = + ((hourOffset < 0) ? "-" : "+") + + String.format(Locale.US, "%02d:%02d", Math.abs(hourOffset), minOffset); + + return ZoneId.of(formattedOffset); + + } catch (DateTimeException e2) { + throw new CelBadFormatException(e2); + } + } + } + + /** Throws an {@link IllegalArgumentException} if the given {@link Timestamp} is not valid. */ + public static void checkValid(Instant instant) { + long seconds = instant.getEpochSecond(); + + if (seconds < TIMESTAMP_SECONDS_MIN || seconds > TIMESTAMP_SECONDS_MAX) { + throw new IllegalArgumentException( + Strings.lenientFormat( + "Timestamp is not valid. " + + "Seconds (%s) must be in range [-62,135,596,800, +253,402,300,799]. " + + "Nanos (%s) must be in range [0, +999,999,999].", + seconds, instant.getNano())); + } + } + + /** Throws an {@link IllegalArgumentException} if the given {@link Duration} is not valid. */ + private static void checkValid(Duration duration) { + long seconds = duration.getSeconds(); + int nanos = duration.getNano(); + if (!isDurationValid(seconds, nanos)) { + throw new IllegalArgumentException( + Strings.lenientFormat( + "Duration is not valid. " + + "Seconds (%s) must be in range [-315,576,000,000, +315,576,000,000]. " + + "Nanos (%s) must be in range [-999,999,999, +999,999,999]. " + + "Nanos must have the same sign as seconds", + seconds, nanos)); + } + } + + /** + * Returns true if the given number of seconds and nanos is a valid {@link Duration}. The {@code + * seconds} value must be in the range [-315,576,000,000, +315,576,000,000]. The {@code nanos} + * value must be in the range [-999,999,999, +999,999,999]. + * + *

Note: Durations less than one second are represented with a 0 {@code seconds} field + * and a positive or negative {@code nanos} field. For durations of one second or more, a non-zero + * value for the {@code nanos} field must be of the same sign as the {@code seconds} field. + */ + private static boolean isDurationValid(long seconds, int nanos) { + if (seconds < DURATION_SECONDS_MIN || seconds > DURATION_SECONDS_MAX) { + return false; + } + if (nanos < -999999999L || nanos >= NANOS_PER_SECOND) { + return false; + } + if (seconds < 0 || nanos < 0) { + if (seconds > 0 || nanos > 0) { + return false; + } + } + return true; + } + + private DateTimeHelpers() {} +} diff --git a/common/src/main/java/dev/cel/common/internal/DefaultDescriptorPool.java b/common/src/main/java/dev/cel/common/internal/DefaultDescriptorPool.java index fc703c905..a4614a2cb 100644 --- a/common/src/main/java/dev/cel/common/internal/DefaultDescriptorPool.java +++ b/common/src/main/java/dev/cel/common/internal/DefaultDescriptorPool.java @@ -22,9 +22,26 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMultimap; import com.google.errorprone.annotations.Immutable; +import com.google.protobuf.Any; +import com.google.protobuf.BoolValue; +import com.google.protobuf.BytesValue; import com.google.protobuf.Descriptors.Descriptor; import com.google.protobuf.Descriptors.FieldDescriptor; +import com.google.protobuf.DoubleValue; +import com.google.protobuf.Duration; +import com.google.protobuf.Empty; import com.google.protobuf.ExtensionRegistry; +import com.google.protobuf.FieldMask; +import com.google.protobuf.FloatValue; +import com.google.protobuf.Int32Value; +import com.google.protobuf.Int64Value; +import com.google.protobuf.ListValue; +import com.google.protobuf.StringValue; +import com.google.protobuf.Struct; +import com.google.protobuf.Timestamp; +import com.google.protobuf.UInt32Value; +import com.google.protobuf.UInt64Value; +import com.google.protobuf.Value; import dev.cel.common.CelDescriptors; import dev.cel.common.annotations.Internal; import java.util.HashMap; @@ -40,14 +57,36 @@ @Immutable @Internal public final class DefaultDescriptorPool implements CelDescriptorPool { - private static final ImmutableMap WELL_KNOWN_TYPE_DESCRIPTORS = + + private static final ImmutableMap WELL_KNOWN_PROTO_TO_DESCRIPTORS = + ImmutableMap.builder() + .put(WellKnownProto.ANY_VALUE, Any.getDescriptor()) + .put(WellKnownProto.BOOL_VALUE, BoolValue.getDescriptor()) + .put(WellKnownProto.BYTES_VALUE, BytesValue.getDescriptor()) + .put(WellKnownProto.DOUBLE_VALUE, DoubleValue.getDescriptor()) + .put(WellKnownProto.DURATION, Duration.getDescriptor()) + .put(WellKnownProto.FLOAT_VALUE, FloatValue.getDescriptor()) + .put(WellKnownProto.INT32_VALUE, Int32Value.getDescriptor()) + .put(WellKnownProto.INT64_VALUE, Int64Value.getDescriptor()) + .put(WellKnownProto.STRING_VALUE, StringValue.getDescriptor()) + .put(WellKnownProto.TIMESTAMP, Timestamp.getDescriptor()) + .put(WellKnownProto.UINT32_VALUE, UInt32Value.getDescriptor()) + .put(WellKnownProto.UINT64_VALUE, UInt64Value.getDescriptor()) + .put(WellKnownProto.JSON_LIST_VALUE, ListValue.getDescriptor()) + .put(WellKnownProto.JSON_STRUCT_VALUE, Struct.getDescriptor()) + .put(WellKnownProto.JSON_VALUE, Value.getDescriptor()) + .put(WellKnownProto.EMPTY, Empty.getDescriptor()) + .put(WellKnownProto.FIELD_MASK, FieldMask.getDescriptor()) + .buildOrThrow(); + + private static final ImmutableMap WELL_KNOWN_TYPE_NAME_TO_DESCRIPTORS = stream(WellKnownProto.values()) - .collect(toImmutableMap(WellKnownProto::typeName, WellKnownProto::descriptor)); + .collect(toImmutableMap(WellKnownProto::typeName, WELL_KNOWN_PROTO_TO_DESCRIPTORS::get)); /** A DefaultDescriptorPool instance with just well known types loaded. */ public static final DefaultDescriptorPool INSTANCE = new DefaultDescriptorPool( - WELL_KNOWN_TYPE_DESCRIPTORS, + WELL_KNOWN_TYPE_NAME_TO_DESCRIPTORS, ImmutableMultimap.of(), ExtensionRegistry.getEmptyRegistry()); @@ -67,8 +106,8 @@ public static DefaultDescriptorPool create(CelDescriptors celDescriptors) { public static DefaultDescriptorPool create( CelDescriptors celDescriptors, ExtensionRegistry extensionRegistry) { - Map descriptorMap = new HashMap<>(); // Using a hashmap to allow deduping - stream(WellKnownProto.values()).forEach(d -> descriptorMap.put(d.typeName(), d.descriptor())); + Map descriptorMap = + new HashMap<>(WELL_KNOWN_TYPE_NAME_TO_DESCRIPTORS); // Using a hashmap to allow deduping for (Descriptor descriptor : celDescriptors.messageTypeDescriptors()) { descriptorMap.putIfAbsent(descriptor.getFullName(), descriptor); @@ -80,6 +119,10 @@ public static DefaultDescriptorPool create( extensionRegistry); } + public static Descriptor getWellKnownProtoDescriptor(WellKnownProto wellKnownProto) { + return WELL_KNOWN_PROTO_TO_DESCRIPTORS.get(wellKnownProto); + } + @Override public Optional findDescriptor(String name) { return Optional.ofNullable(descriptorMap.get(name)); 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 6466ee9c9..163d0273e 100644 --- a/common/src/main/java/dev/cel/common/internal/DefaultInstanceMessageFactory.java +++ b/common/src/main/java/dev/cel/common/internal/DefaultInstanceMessageFactory.java @@ -14,23 +14,12 @@ package dev.cel.common.internal; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.CaseFormat; -import com.google.common.base.Joiner; -import com.google.common.base.Strings; -import com.google.common.io.Files; -import com.google.protobuf.DescriptorProtos.FileOptions; import com.google.protobuf.Descriptors.Descriptor; -import com.google.protobuf.Descriptors.EnumDescriptor; -import com.google.protobuf.Descriptors.FileDescriptor; -import com.google.protobuf.Descriptors.ServiceDescriptor; +import com.google.protobuf.GeneratorNames; import com.google.protobuf.Message; +import com.google.protobuf.MessageLite; import dev.cel.common.annotations.Internal; -import java.lang.reflect.InvocationTargetException; -import java.util.ArrayDeque; -import java.util.Map; import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; /** * Singleton factory for creating default messages from a protobuf descriptor. @@ -38,20 +27,12 @@ *

CEL Library Internals. Do Not Use. */ @Internal -final class DefaultInstanceMessageFactory { - - // Controls how many times we should recursively inspect a nested message for building fully - // qualified java class name before aborting. - public static final int SAFE_RECURSE_LIMIT = 50; - - private static final DefaultInstanceMessageFactory instance = new DefaultInstanceMessageFactory(); - - private final Map messageByDescriptorName = - new ConcurrentHashMap<>(); +public final class DefaultInstanceMessageFactory { + private static final DefaultInstanceMessageFactory INSTANCE = new DefaultInstanceMessageFactory(); /** Gets a single instance of this MessageFactory */ public static DefaultInstanceMessageFactory getInstance() { - return instance; + return INSTANCE; } /** @@ -63,182 +44,27 @@ public static DefaultInstanceMessageFactory getInstance() { * descriptor class isn't loaded in the binary. */ public Optional getPrototype(Descriptor descriptor) { - String descriptorName = descriptor.getFullName(); - LazyGeneratedMessageDefaultInstance lazyDefaultInstance = - messageByDescriptorName.computeIfAbsent( - descriptorName, - (unused) -> - new LazyGeneratedMessageDefaultInstance( - getFullyQualifiedJavaClassName(descriptor))); - - Message defaultInstance = lazyDefaultInstance.getDefaultInstance(); + MessageLite defaultInstance = + DefaultInstanceMessageLiteFactory.getInstance() + .getPrototype(descriptor.getFullName(), GeneratorNames.getBytecodeClassName(descriptor)) + .orElse(null); if (defaultInstance == null) { return Optional.empty(); } - // Reference equality is intended. We want to make sure the descriptors are equal - // to guarantee types to be hermetic if linked types is disabled. - if (defaultInstance.getDescriptorForType() != descriptor) { - return Optional.empty(); - } - return Optional.of(defaultInstance); - } - - /** - * Retrieves the full Java class name from the given descriptor - * - * @return fully qualified class name. - *

Example 1: com.google.api.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) - */ - private String getFullyQualifiedJavaClassName(Descriptor descriptor) { - StringBuilder fullClassName = new StringBuilder(); - fullClassName.append(getJavaPackageName(descriptor)); - - String javaOuterClass = getJavaOuterClassName(descriptor); - if (!Strings.isNullOrEmpty(javaOuterClass)) { - fullClassName.append(javaOuterClass).append("$"); - } - - // Recursively build the target class name in case if the message is nested. - ArrayDeque classNames = new ArrayDeque<>(); - Descriptor d = descriptor; - - int recurseCount = 0; - while (d != null) { - classNames.push(d.getName()); - d = d.getContainingType(); - recurseCount++; - if (recurseCount >= SAFE_RECURSE_LIMIT) { - throw new IllegalStateException( - String.format( - "Recursion limit of %d hit while inspecting descriptor: %s", - SAFE_RECURSE_LIMIT, descriptor.getFullName())); - } - } - - Joiner.on("$").appendTo(fullClassName, classNames); - - return fullClassName.toString(); - } - - /** - * 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 - */ - private String getJavaPackageName(Descriptor descriptor) { - FileOptions options = descriptor.getFile().getOptions(); - StringBuilder javaPackageName = new StringBuilder(); - if (options.hasJavaPackage()) { - javaPackageName.append(descriptor.getFile().getOptions().getJavaPackage()).append("."); - } else { - javaPackageName - // CEL-Internal-1 - .append(descriptor.getFile().getPackage()) - .append("."); + if (!(defaultInstance instanceof Message)) { + throw new IllegalArgumentException( + "Expected a full protobuf message, but got: " + defaultInstance.getClass()); } - // CEL-Internal-2 + Message fullMessage = (Message) defaultInstance; - return javaPackageName.toString(); - } - - /** - * Gets a wrapping outer class name from the descriptor. The outer class name differs depending on - * the proto options set. See - * https://developers.google.com/protocol-buffers/docs/reference/java-generated#invocation - */ - private String getJavaOuterClassName(Descriptor descriptor) { - FileOptions options = descriptor.getFile().getOptions(); - - if (options.getJavaMultipleFiles()) { - // If java_multiple_files is enabled, protoc does not generate a wrapper outer class - return ""; - } - - if (options.hasJavaOuterClassname()) { - return options.getJavaOuterClassname(); - } else { - // If an outer class name is not explicitly set, the name is converted into - // Pascal case based on the snake cased file name - // Ex: messages_proto.proto becomes MessagesProto - String protoFileNameWithoutExtension = - Files.getNameWithoutExtension(descriptor.getFile().getFullName()); - String outerClassName = - CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, protoFileNameWithoutExtension); - if (hasConflictingClassName(descriptor.getFile(), outerClassName)) { - outerClassName += "OuterClass"; - } - return outerClassName; - } - } - - private boolean hasConflictingClassName(FileDescriptor file, String name) { - for (EnumDescriptor enumDesc : file.getEnumTypes()) { - if (name.equals(enumDesc.getName())) { - return true; - } - } - for (ServiceDescriptor serviceDesc : file.getServices()) { - if (name.equals(serviceDesc.getName())) { - return true; - } - } - for (Descriptor messageDesc : file.getMessageTypes()) { - if (name.equals(messageDesc.getName())) { - return true; - } - } - return false; - } - - /** A placeholder to lazily load the generated messages' defaultInstances. */ - private static final class LazyGeneratedMessageDefaultInstance { - private final String fullClassName; - private volatile Message defaultInstance = null; - private volatile boolean loaded = false; - - public LazyGeneratedMessageDefaultInstance(String fullClassName) { - this.fullClassName = fullClassName; - } - - public Message getDefaultInstance() { - if (!loaded) { - synchronized (this) { - if (!loaded) { - loadDefaultInstance(); - loaded = true; - } - } - } - return defaultInstance; - } - - private void loadDefaultInstance() { - try { - defaultInstance = - (Message) Class.forName(fullClassName).getMethod("getDefaultInstance").invoke(null); - } catch (IllegalAccessException | InvocationTargetException e) { - throw new LinkageError( - String.format("getDefaultInstance for class: %s failed.", fullClassName), e); - } catch (NoSuchMethodException e) { - throw new LinkageError( - String.format("getDefaultInstance method does not exist in class: %s.", fullClassName), - e); - } catch (ClassNotFoundException e) { - // The class may not exist in some instances (Ex: evaluating a checked expression from a - // cached source). - } + // Reference equality is intended. We want to make sure the descriptors are equal + // to guarantee types to be hermetic if linked types is disabled. + if (fullMessage.getDescriptorForType() != descriptor) { + return Optional.empty(); } - } - - /** Clears the descriptor map. This should not be used outside testing. */ - @VisibleForTesting - void resetDescriptorMapForTesting() { - messageByDescriptorName.clear(); + return Optional.of(fullMessage); } private DefaultInstanceMessageFactory() {} diff --git a/common/src/main/java/dev/cel/common/internal/DefaultInstanceMessageLiteFactory.java b/common/src/main/java/dev/cel/common/internal/DefaultInstanceMessageLiteFactory.java new file mode 100644 index 000000000..8adb79248 --- /dev/null +++ b/common/src/main/java/dev/cel/common/internal/DefaultInstanceMessageLiteFactory.java @@ -0,0 +1,109 @@ +// 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.common.annotations.VisibleForTesting; +import com.google.protobuf.MessageLite; +import dev.cel.common.annotations.Internal; +import java.lang.reflect.Method; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Singleton factory for creating default messages from a fully qualified protobuf type name and its + * java class name. + * + *

CEL Library Internals. Do Not Use. + */ +@Internal +public final class DefaultInstanceMessageLiteFactory { + + private static final DefaultInstanceMessageLiteFactory INSTANCE = + new DefaultInstanceMessageLiteFactory(); + + private final Map messageByTypeName = + new ConcurrentHashMap<>(); + + /** Gets a single instance of this DefaultInstanceMessageLiteFactory */ + public static DefaultInstanceMessageLiteFactory getInstance() { + return INSTANCE; + } + + /** + * Creates a default instance of a protobuf message given a fully qualified type name. This is + * essentially the same as calling FooMessage.getDefaultInstance(), except reflection is + * leveraged. + * + * @return Default instance of a type. Returns an empty optional if the java class for the + * protobuf message isn't linked in the binary. + */ + public Optional getPrototype(String protoFqn, String protoJavaClassFqn) { + LazyGeneratedMessageDefaultInstance lazyDefaultInstance = + messageByTypeName.computeIfAbsent( + protoFqn, (unused) -> new LazyGeneratedMessageDefaultInstance(protoJavaClassFqn)); + + MessageLite defaultInstance = lazyDefaultInstance.getDefaultInstance(); + + return Optional.ofNullable(defaultInstance); + } + + /** A placeholder to lazily load the generated messages' defaultInstances. */ + private static final class LazyGeneratedMessageDefaultInstance { + private final String fullClassName; + private volatile MessageLite defaultInstance = null; + private volatile boolean loaded = false; + + public LazyGeneratedMessageDefaultInstance(String fullClassName) { + this.fullClassName = fullClassName; + } + + public MessageLite getDefaultInstance() { + if (!loaded) { + synchronized (this) { + if (!loaded) { + loadDefaultInstance(); + loaded = true; + } + } + } + return defaultInstance; + } + + private void loadDefaultInstance() { + Class clazz; + try { + clazz = Class.forName(fullClassName); + } catch (ClassNotFoundException e) { + // The class may not exist in cases where the java class for the generated message was not + // linked into the binary (Ex: evaluating a checked expression from a + // cached source), or a dynamic descriptor was explicitly used. CEL will return a dynamic + // message in such cases. + return; + } + + Method method = ReflectionUtil.getMethod(clazz, "getDefaultInstance"); + defaultInstance = (MessageLite) ReflectionUtil.invoke(method, null); + } + } + + /** Clears the descriptor map. This should not be used outside testing. */ + @VisibleForTesting + void resetTypeMap() { + messageByTypeName.clear(); + } + + private DefaultInstanceMessageLiteFactory() {} +} diff --git a/common/src/main/java/dev/cel/common/internal/DefaultLiteDescriptorPool.java b/common/src/main/java/dev/cel/common/internal/DefaultLiteDescriptorPool.java new file mode 100644 index 000000000..0078e5c13 --- /dev/null +++ b/common/src/main/java/dev/cel/common/internal/DefaultLiteDescriptorPool.java @@ -0,0 +1,325 @@ +// 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.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.errorprone.annotations.Immutable; +import com.google.protobuf.Any; +import com.google.protobuf.BoolValue; +import com.google.protobuf.BytesValue; +import com.google.protobuf.DoubleValue; +import com.google.protobuf.Duration; +import com.google.protobuf.Empty; +import com.google.protobuf.FieldMask; +import com.google.protobuf.FloatValue; +import com.google.protobuf.Int32Value; +import com.google.protobuf.Int64Value; +import com.google.protobuf.ListValue; +import com.google.protobuf.MessageLite; +import com.google.protobuf.StringValue; +import com.google.protobuf.Struct; +import com.google.protobuf.Timestamp; +import com.google.protobuf.UInt32Value; +import com.google.protobuf.UInt64Value; +import com.google.protobuf.Value; +import dev.cel.common.annotations.Internal; +import dev.cel.protobuf.CelLiteDescriptor; +import dev.cel.protobuf.CelLiteDescriptor.FieldLiteDescriptor; +import dev.cel.protobuf.CelLiteDescriptor.FieldLiteDescriptor.EncodingType; +import dev.cel.protobuf.CelLiteDescriptor.MessageLiteDescriptor; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.function.Supplier; + +/** Descriptor pool for {@link CelLiteDescriptor}s. */ +@Immutable +@Internal +public final class DefaultLiteDescriptorPool implements CelLiteDescriptorPool { + private final ImmutableMap protoFqnToMessageInfo; + private final ImmutableMap, MessageLiteDescriptor> classToMessageInfo; + + public static DefaultLiteDescriptorPool newInstance(CelLiteDescriptor... descriptors) { + return newInstance(ImmutableSet.copyOf(descriptors)); + } + + public static DefaultLiteDescriptorPool newInstance(ImmutableSet descriptors) { + return new DefaultLiteDescriptorPool(descriptors); + } + + @Override + public Optional findDescriptor(MessageLite messageLite) { + return Optional.ofNullable(classToMessageInfo.get(messageLite.getClass())); + } + + @Override + public Optional findDescriptor(String protoTypeName) { + return Optional.ofNullable(protoFqnToMessageInfo.get(protoTypeName)); + } + + @Override + public MessageLiteDescriptor getDescriptorOrThrow(String protoTypeName) { + return findDescriptor(protoTypeName) + .orElseThrow( + () -> new NoSuchElementException("Could not find a descriptor for: " + protoTypeName)); + } + + private static MessageLiteDescriptor newMessageInfo(WellKnownProto wellKnownProto) { + ImmutableList.Builder fieldDescriptors = ImmutableList.builder(); + Supplier messageBuilder = null; + switch (wellKnownProto) { + case ANY_VALUE: + messageBuilder = Any::newBuilder; + fieldDescriptors + .add( + newPrimitiveFieldDescriptor( + 1, + "type_url", + FieldLiteDescriptor.JavaType.STRING, + FieldLiteDescriptor.Type.STRING)) + .add( + newPrimitiveFieldDescriptor( + 2, + "value", + FieldLiteDescriptor.JavaType.BYTE_STRING, + FieldLiteDescriptor.Type.BYTES)); + break; + case FIELD_MASK: + messageBuilder = FieldMask::newBuilder; + fieldDescriptors.add( + new FieldLiteDescriptor( + /* fieldNumber= */ 1, + /* fieldName= */ "paths", + /* javaType= */ FieldLiteDescriptor.JavaType.STRING, + /* encodingType= */ EncodingType.LIST, + /* protoFieldType= */ FieldLiteDescriptor.Type.STRING, + /* isPacked= */ false, + /* fieldProtoTypeName= */ "")); + break; + case BOOL_VALUE: + messageBuilder = BoolValue::newBuilder; + fieldDescriptors.add( + newPrimitiveFieldDescriptor( + 1, "value", FieldLiteDescriptor.JavaType.BOOLEAN, FieldLiteDescriptor.Type.BOOL)); + break; + case BYTES_VALUE: + messageBuilder = BytesValue::newBuilder; + fieldDescriptors.add( + newPrimitiveFieldDescriptor( + 1, + "value", + FieldLiteDescriptor.JavaType.BYTE_STRING, + FieldLiteDescriptor.Type.BYTES)); + break; + case DOUBLE_VALUE: + messageBuilder = DoubleValue::newBuilder; + fieldDescriptors.add( + newPrimitiveFieldDescriptor( + 1, "value", FieldLiteDescriptor.JavaType.DOUBLE, FieldLiteDescriptor.Type.DOUBLE)); + break; + case FLOAT_VALUE: + messageBuilder = FloatValue::newBuilder; + fieldDescriptors.add( + newPrimitiveFieldDescriptor( + 1, "value", FieldLiteDescriptor.JavaType.FLOAT, FieldLiteDescriptor.Type.FLOAT)); + break; + case INT32_VALUE: + messageBuilder = Int32Value::newBuilder; + fieldDescriptors.add( + newPrimitiveFieldDescriptor( + 1, "value", FieldLiteDescriptor.JavaType.INT, FieldLiteDescriptor.Type.INT32)); + break; + case INT64_VALUE: + messageBuilder = Int64Value::newBuilder; + fieldDescriptors.add( + newPrimitiveFieldDescriptor( + 1, "value", FieldLiteDescriptor.JavaType.LONG, FieldLiteDescriptor.Type.INT64)); + break; + case STRING_VALUE: + messageBuilder = StringValue::newBuilder; + fieldDescriptors.add( + newPrimitiveFieldDescriptor( + 1, "value", FieldLiteDescriptor.JavaType.STRING, FieldLiteDescriptor.Type.STRING)); + break; + case UINT32_VALUE: + messageBuilder = UInt32Value::newBuilder; + fieldDescriptors.add( + newPrimitiveFieldDescriptor( + 1, "value", FieldLiteDescriptor.JavaType.INT, FieldLiteDescriptor.Type.UINT32)); + break; + case UINT64_VALUE: + messageBuilder = UInt64Value::newBuilder; + fieldDescriptors.add( + newPrimitiveFieldDescriptor( + 1, "value", FieldLiteDescriptor.JavaType.LONG, FieldLiteDescriptor.Type.UINT64)); + break; + case JSON_STRUCT_VALUE: + messageBuilder = Struct::newBuilder; + fieldDescriptors.add( + new FieldLiteDescriptor( + /* fieldNumber= */ 1, + /* fieldName= */ "fields", + /* javaType= */ FieldLiteDescriptor.JavaType.MESSAGE, + /* encodingType= */ EncodingType.MAP, + /* protoFieldType= */ FieldLiteDescriptor.Type.MESSAGE, + /* isPacked= */ false, + /* fieldProtoTypeName= */ "google.protobuf.Struct.FieldsEntry")); + break; + case JSON_VALUE: + messageBuilder = Value::newBuilder; + fieldDescriptors.add( + new FieldLiteDescriptor( + /* fieldNumber= */ 1, + /* fieldName= */ "null_value", + /* javaType= */ FieldLiteDescriptor.JavaType.ENUM, + /* encodingType= */ EncodingType.SINGULAR, + /* protoFieldType= */ FieldLiteDescriptor.Type.ENUM, + /* isPacked= */ false, + /* fieldProtoTypeName= */ "google.protobuf.NullValue")); + fieldDescriptors.add( + new FieldLiteDescriptor( + /* fieldNumber= */ 2, + /* fieldName= */ "number_value", + /* javaType= */ FieldLiteDescriptor.JavaType.DOUBLE, + /* encodingType= */ EncodingType.SINGULAR, + /* protoFieldType= */ FieldLiteDescriptor.Type.DOUBLE, + /* isPacked= */ false, + /* fieldProtoTypeName= */ "")); + fieldDescriptors.add( + new FieldLiteDescriptor( + /* fieldNumber= */ 3, + /* fieldName= */ "string_value", + /* javaType= */ FieldLiteDescriptor.JavaType.STRING, + /* encodingType= */ EncodingType.SINGULAR, + /* protoFieldType= */ FieldLiteDescriptor.Type.STRING, + /* isPacked= */ false, + /* fieldProtoTypeName= */ "")); + fieldDescriptors.add( + new FieldLiteDescriptor( + /* fieldNumber= */ 4, + /* fieldName= */ "bool_value", + /* javaType= */ FieldLiteDescriptor.JavaType.BOOLEAN, + /* encodingType= */ EncodingType.SINGULAR, + /* protoFieldType= */ FieldLiteDescriptor.Type.BOOL, + /* isPacked= */ false, + /* fieldProtoTypeName= */ "")); + fieldDescriptors.add( + new FieldLiteDescriptor( + /* fieldNumber= */ 5, + /* fieldName= */ "struct_value", + /* javaType= */ FieldLiteDescriptor.JavaType.MESSAGE, + /* encodingType= */ EncodingType.SINGULAR, + /* protoFieldType= */ FieldLiteDescriptor.Type.MESSAGE, + /* isPacked= */ false, + /* fieldProtoTypeName= */ "google.protobuf.Struct")); + fieldDescriptors.add( + new FieldLiteDescriptor( + /* fieldNumber= */ 6, + /* fieldName= */ "list_value", + /* javaType= */ FieldLiteDescriptor.JavaType.MESSAGE, + /* encodingType= */ EncodingType.SINGULAR, + /* protoFieldType= */ FieldLiteDescriptor.Type.MESSAGE, + /* isPacked= */ false, + /* fieldProtoTypeName= */ "google.protobuf.ListValue")); + break; + case JSON_LIST_VALUE: + messageBuilder = ListValue::newBuilder; + fieldDescriptors.add( + new FieldLiteDescriptor( + /* fieldNumber= */ 1, + /* fieldName= */ "values", + /* javaType= */ FieldLiteDescriptor.JavaType.MESSAGE, + /* encodingType= */ EncodingType.LIST, + /* protoFieldType= */ FieldLiteDescriptor.Type.MESSAGE, + /* isPacked= */ false, + /* fieldProtoTypeName= */ "google.protobuf.Value")); + break; + case DURATION: + messageBuilder = Duration::newBuilder; + fieldDescriptors + .add( + newPrimitiveFieldDescriptor( + 1, + "seconds", + FieldLiteDescriptor.JavaType.LONG, + FieldLiteDescriptor.Type.INT64)) + .add( + newPrimitiveFieldDescriptor( + 2, "nanos", FieldLiteDescriptor.JavaType.INT, FieldLiteDescriptor.Type.INT32)); + break; + case TIMESTAMP: + messageBuilder = Timestamp::newBuilder; + fieldDescriptors + .add( + newPrimitiveFieldDescriptor( + 1, + "seconds", + FieldLiteDescriptor.JavaType.LONG, + FieldLiteDescriptor.Type.INT64)) + .add( + newPrimitiveFieldDescriptor( + 2, "nanos", FieldLiteDescriptor.JavaType.INT, FieldLiteDescriptor.Type.INT32)); + break; + case EMPTY: + messageBuilder = Empty::newBuilder; + } + + return new MessageLiteDescriptor( + wellKnownProto.typeName(), fieldDescriptors.build(), messageBuilder); + } + + private static FieldLiteDescriptor newPrimitiveFieldDescriptor( + int fieldNumber, + String fieldName, + FieldLiteDescriptor.JavaType javaType, + FieldLiteDescriptor.Type protoFieldType) { + return new FieldLiteDescriptor( + /* fieldNumber= */ fieldNumber, + /* fieldName= */ fieldName, + /* javaType= */ javaType, + /* encodingType= */ EncodingType.SINGULAR, + /* protoFieldType= */ protoFieldType, + /* isPacked= */ false, + /* fieldProtoTypeName= */ ""); + } + + private DefaultLiteDescriptorPool(ImmutableSet descriptors) { + ImmutableMap.Builder protoFqnMapBuilder = ImmutableMap.builder(); + ImmutableMap.Builder, MessageLiteDescriptor> classMapBuilder = ImmutableMap.builder(); + for (WellKnownProto wellKnownProto : WellKnownProto.values()) { + MessageLiteDescriptor wktMessageInfo = newMessageInfo(wellKnownProto); + protoFqnMapBuilder.put(wellKnownProto.typeName(), wktMessageInfo); + classMapBuilder.put(wellKnownProto.messageClass(), wktMessageInfo); + } + + for (CelLiteDescriptor descriptor : descriptors) { + protoFqnMapBuilder.putAll(descriptor.getProtoTypeNamesToDescriptors()); + + for (MessageLiteDescriptor messageLiteDescriptor : + descriptor.getProtoTypeNamesToDescriptors().values()) { + // Note: message builder is null for proto maps. + Optional.ofNullable(messageLiteDescriptor.newMessageBuilder()) + .ifPresent( + builder -> + classMapBuilder.put( + builder.getDefaultInstanceForType().getClass(), messageLiteDescriptor)); + } + } + + this.protoFqnToMessageInfo = protoFqnMapBuilder.buildOrThrow(); + this.classToMessageInfo = classMapBuilder.buildOrThrow(); + } +} 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 95352d83b..8bca7bf31 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,7 @@ package dev.cel.common.internal; +import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.DoNotCall; import com.google.errorprone.annotations.Immutable; import dev.cel.common.annotations.Internal; @@ -55,6 +56,11 @@ public int size() { return 0; } + @Override + public ImmutableList lineOffsets() { + return ImmutableList.of(1); + } + @Override public String toString() { return ""; diff --git a/common/src/main/java/dev/cel/common/internal/EnvVisitor.java b/common/src/main/java/dev/cel/common/internal/EnvVisitor.java index df3a08b9c..edb46a1dd 100644 --- a/common/src/main/java/dev/cel/common/internal/EnvVisitor.java +++ b/common/src/main/java/dev/cel/common/internal/EnvVisitor.java @@ -16,6 +16,7 @@ import dev.cel.expr.Decl; import dev.cel.common.annotations.Internal; +import dev.cel.parser.CelMacro; import java.util.List; /** @@ -23,7 +24,6 @@ * *

CEL Library Internals. Do Not Use. */ -@FunctionalInterface @Internal public interface EnvVisitor { @@ -32,4 +32,7 @@ public interface EnvVisitor { * with that name. */ void visitDecl(String name, List decls); + + /** Visit the CEL macro. */ + void visitMacro(CelMacro macro); } diff --git a/common/src/main/java/dev/cel/common/internal/Errors.java b/common/src/main/java/dev/cel/common/internal/Errors.java index 3ee3ad19c..3796b642a 100644 --- a/common/src/main/java/dev/cel/common/internal/Errors.java +++ b/common/src/main/java/dev/cel/common/internal/Errors.java @@ -28,7 +28,7 @@ import java.util.Deque; import java.util.List; import java.util.Optional; -import org.jspecify.nullness.Nullable; +import org.jspecify.annotations.Nullable; /** * An object which manages error reporting. Enriches error messages by source context pointing to @@ -137,8 +137,10 @@ public static class Error { private final Context context; private final int position; private final String message; + private final long exprId; - private Error(Context context, int position, String message) { + private Error(long exprId, Context context, int position, String message) { + this.exprId = exprId; this.context = context; this.position = position; this.message = message; @@ -154,6 +156,14 @@ public String rawMessage() { return message; } + /** + * Returns the expression ID associated with this error. May return 0 if the error is not caused + * by an expression (ex: environment misconfiguration). + */ + public long exprId() { + return exprId; + } + /** Formats the error into a string which indicates where it occurs within the expression. */ public String toDisplayString(@Nullable ErrorFormatter formatter) { String marker = formatter != null ? formatter.formatError("ERROR") : "ERROR"; @@ -278,13 +288,23 @@ public String getAllErrorsAsString() { return Joiner.on(NEWLINE).join(errors); } + /** + * Note: Used by codegen + * + * @deprecated Use {@link #reportError(long, int, String, Object...) instead} + */ + @Deprecated + public void reportError(int position, String message, Object... args) { + reportError(0L, position, message, args); + } + /** Reports an error. */ // TODO: Consider adding @FormatMethod here and updating all upstream callers. - public void reportError(int position, String message, Object... args) { + public void reportError(long exprId, int position, String message, Object... args) { if (args.length > 0) { message = String.format(message, args); } - errors.add(new Error(context.peekFirst(), position, message)); + errors.add(new Error(exprId, context.peekFirst(), position, message)); } /** diff --git a/common/src/main/java/dev/cel/common/internal/FileDescriptorSetConverter.java b/common/src/main/java/dev/cel/common/internal/FileDescriptorSetConverter.java index c6fd4a0da..5bde34097 100644 --- a/common/src/main/java/dev/cel/common/internal/FileDescriptorSetConverter.java +++ b/common/src/main/java/dev/cel/common/internal/FileDescriptorSetConverter.java @@ -15,6 +15,7 @@ package dev.cel.common.internal; import com.google.common.base.VerifyException; +import com.google.common.collect.ImmutableCollection; import com.google.common.collect.ImmutableSet; import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.CheckReturnValue; @@ -76,7 +77,15 @@ private static FileDescriptor readDescriptor( // Read dependencies first, they are needed to create the logical descriptor from the proto. List deps = new ArrayList<>(); for (String dep : fileProto.getDependencyList()) { - deps.add(readDescriptor(dep, descriptorProtos, descriptors)); + ImmutableCollection wktProtos = WellKnownProto.getByPathName(dep); + if (wktProtos.isEmpty()) { + deps.add(readDescriptor(dep, descriptorProtos, descriptors)); + } else { + // Ensure the generated message's descriptor is used as a dependency for WKTs to avoid + // issues with descriptor instance mismatch. + WellKnownProto wellKnownProto = wktProtos.iterator().next(); + deps.add(DefaultDescriptorPool.getWellKnownProtoDescriptor(wellKnownProto).getFile()); + } } // Create the file descriptor, cache, and return. try { 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 854df7fa7..42cc0445c 100644 --- a/common/src/main/java/dev/cel/common/internal/Latin1CodePointArray.java +++ b/common/src/main/java/dev/cel/common/internal/Latin1CodePointArray.java @@ -19,7 +19,8 @@ import static com.google.common.base.Preconditions.checkPositionIndexes; import static java.nio.charset.StandardCharsets.ISO_8859_1; -import com.google.common.annotations.VisibleForTesting; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.Immutable; import dev.cel.common.annotations.Internal; @@ -29,45 +30,41 @@ *

CEL Library Internals. Do Not Use. */ @Immutable -@VisibleForTesting @Internal -public final class Latin1CodePointArray extends CelCodePointArray { +@AutoValue +@AutoValue.CopyAnnotations +@SuppressWarnings("Immutable") // byte[] is not exposed externally, thus cannot be mutated. +public abstract class Latin1CodePointArray extends CelCodePointArray { - @SuppressWarnings("Immutable") - private final byte[] codePoints; + @SuppressWarnings("mutable") + abstract byte[] codePoints(); - private final int offset; - private final int size; + abstract int offset(); - Latin1CodePointArray(byte[] codePoints, int size) { - this(codePoints, 0, size); + static Latin1CodePointArray create( + byte[] codePoints, int size, ImmutableList lineOffsets) { + return create(codePoints, 0, lineOffsets, size); } - Latin1CodePointArray(byte[] codePoints, int offset, int size) { - this.codePoints = checkNotNull(codePoints); - this.offset = offset; - this.size = size; + static Latin1CodePointArray create( + byte[] codePoints, int offset, ImmutableList lineOffsets, int size) { + return new AutoValue_Latin1CodePointArray(size, checkNotNull(lineOffsets), codePoints, offset); } @Override public Latin1CodePointArray slice(int i, int j) { checkPositionIndexes(i, j, size()); - return new Latin1CodePointArray(codePoints, offset + i, j - i); + return create(codePoints(), offset() + i, lineOffsets(), j - i); } @Override public int get(int index) { checkElementIndex(index, size()); - return Byte.toUnsignedInt(codePoints[offset + index]); + return Byte.toUnsignedInt(codePoints()[offset() + index]); } @Override - public int size() { - return size; - } - - @Override - public String toString() { - return new String(codePoints, offset, size, ISO_8859_1); + public final String toString() { + return new String(codePoints(), offset(), size(), ISO_8859_1); } } 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 6e291c626..7e3910433 100644 --- a/common/src/main/java/dev/cel/common/internal/ProtoAdapter.java +++ b/common/src/main/java/dev/cel/common/internal/ProtoAdapter.java @@ -15,51 +15,32 @@ package dev.cel.common.internal; import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.collect.ImmutableMap.toImmutableMap; -import dev.cel.expr.ExprValue; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; import com.google.common.primitives.Ints; import com.google.common.primitives.UnsignedInts; import com.google.common.primitives.UnsignedLong; import com.google.errorprone.annotations.CheckReturnValue; import com.google.errorprone.annotations.Immutable; import com.google.protobuf.Any; -import com.google.protobuf.BoolValue; import com.google.protobuf.ByteString; -import com.google.protobuf.BytesValue; import com.google.protobuf.Descriptors.Descriptor; import com.google.protobuf.Descriptors.EnumValueDescriptor; import com.google.protobuf.Descriptors.FieldDescriptor; -import com.google.protobuf.DoubleValue; -import com.google.protobuf.Duration; import com.google.protobuf.DynamicMessage; -import com.google.protobuf.FloatValue; -import com.google.protobuf.Int32Value; -import com.google.protobuf.Int64Value; import com.google.protobuf.InvalidProtocolBufferException; -import com.google.protobuf.ListValue; import com.google.protobuf.MapEntry; import com.google.protobuf.Message; import com.google.protobuf.MessageOrBuilder; -import com.google.protobuf.NullValue; -import com.google.protobuf.StringValue; -import com.google.protobuf.Struct; -import com.google.protobuf.Timestamp; -import com.google.protobuf.UInt32Value; -import com.google.protobuf.UInt64Value; -import com.google.protobuf.Value; -import dev.cel.common.CelErrorCode; -import dev.cel.common.CelRuntimeException; +import dev.cel.common.CelOptions; import dev.cel.common.annotations.Internal; +import dev.cel.common.exceptions.CelNumericOverflowException; +import dev.cel.common.values.CelByteString; +import dev.cel.common.values.NullValue; import java.util.ArrayList; -import java.util.Base64; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; -import org.jspecify.nullness.Nullable; /** * The {@code ProtoAdapter} utilities handle conversion between native Java objects which represent @@ -137,12 +118,14 @@ public final class ProtoAdapter { public static final BidiConverter DOUBLE_CONVERTER = BidiConverter.of(Number::doubleValue, Number::floatValue); + private final ProtoLiteAdapter protoLiteAdapter; + private final CelOptions celOptions; private final DynamicProto dynamicProto; - private final boolean enableUnsignedLongs; - public ProtoAdapter(DynamicProto dynamicProto, boolean enableUnsignedLongs) { + public ProtoAdapter(DynamicProto dynamicProto, CelOptions celOptions) { this.dynamicProto = checkNotNull(dynamicProto); - this.enableUnsignedLongs = enableUnsignedLongs; + this.protoLiteAdapter = new ProtoLiteAdapter(celOptions); + this.celOptions = celOptions; } /** @@ -158,7 +141,7 @@ public Object adaptProtoToValue(MessageOrBuilder proto) { // If the proto is not a well-known type, then the input Message is what's expected as the // output return value. WellKnownProto wellKnownProto = - WellKnownProto.getByDescriptorName(typeName(proto.getDescriptorForType())); + WellKnownProto.getByTypeName(typeName(proto.getDescriptorForType())).orElse(null); if (wellKnownProto == null) { return proto; } @@ -167,57 +150,41 @@ public Object adaptProtoToValue(MessageOrBuilder proto) { switch (wellKnownProto) { case ANY_VALUE: return unpackAnyProto((Any) proto); - case JSON_VALUE: - return adaptJsonToValue((Value) proto); - case JSON_STRUCT_VALUE: - return adaptJsonStructToValue((Struct) proto); - case JSON_LIST_VALUE: - return adaptJsonListToValue((ListValue) proto); - case BOOL_VALUE: - return ((BoolValue) proto).getValue(); - case BYTES_VALUE: - return ((BytesValue) proto).getValue(); - case DOUBLE_VALUE: - return ((DoubleValue) proto).getValue(); - case FLOAT_VALUE: - return (double) ((FloatValue) proto).getValue(); - case INT32_VALUE: - return (long) ((Int32Value) proto).getValue(); - case INT64_VALUE: - return ((Int64Value) proto).getValue(); - case STRING_VALUE: - return ((StringValue) proto).getValue(); - case UINT32_VALUE: - if (enableUnsignedLongs) { - return UnsignedLong.fromLongBits( - Integer.toUnsignedLong(((UInt32Value) proto).getValue())); - } - return (long) ((UInt32Value) proto).getValue(); - case UINT64_VALUE: - if (enableUnsignedLongs) { - return UnsignedLong.fromLongBits(((UInt64Value) proto).getValue()); - } - return ((UInt64Value) proto).getValue(); default: - return proto; + return protoLiteAdapter.adaptWellKnownProtoToValue(proto, wellKnownProto); } } @SuppressWarnings({"unchecked", "rawtypes"}) public Optional adaptFieldToValue(FieldDescriptor fieldDescriptor, Object fieldValue) { - if (isUnknown(fieldValue)) { - return Optional.of(fieldValue); - } if (fieldDescriptor.isMapField()) { Descriptor entryDescriptor = fieldDescriptor.getMessageType(); - BidiConverter keyConverter = fieldToValueConverter(entryDescriptor.findFieldByNumber(1)); - BidiConverter valueConverter = fieldToValueConverter(entryDescriptor.findFieldByNumber(2)); + FieldDescriptor keyFieldDescriptor = entryDescriptor.findFieldByNumber(1); + FieldDescriptor valueFieldDescriptor = entryDescriptor.findFieldByNumber(2); + BidiConverter keyConverter = fieldToValueConverter(keyFieldDescriptor); + BidiConverter valueConverter = fieldToValueConverter(valueFieldDescriptor); + Map map = new HashMap<>(); - for (MapEntry entry : ((List) fieldValue)) { + Object mapKey; + Object mapValue; + for (Object entry : ((List) fieldValue)) { + if (entry instanceof MapEntry) { + MapEntry mapEntry = (MapEntry) entry; + mapKey = mapEntry.getKey(); + mapValue = mapEntry.getValue(); + } else if (entry instanceof DynamicMessage) { + DynamicMessage dynamicMessage = (DynamicMessage) entry; + mapKey = dynamicMessage.getField(keyFieldDescriptor); + mapValue = dynamicMessage.getField(valueFieldDescriptor); + } else { + throw new IllegalStateException("Unexpected map field type: " + entry); + } + map.put( - keyConverter.forwardConverter().convert(entry.getKey()), - valueConverter.forwardConverter().convert(entry.getValue())); + keyConverter.forwardConverter().convert(mapKey), + valueConverter.forwardConverter().convert(mapValue)); } + return Optional.of(map); } if (fieldDescriptor.isRepeated()) { @@ -225,8 +192,11 @@ public Optional adaptFieldToValue(FieldDescriptor fieldDescriptor, Objec if (bidiConverter == BidiConverter.IDENTITY) { return Optional.of(fieldValue); } - return Optional.of(AdaptingTypes.adaptingList((List) fieldValue, bidiConverter)); + ArrayList convertedList = + new ArrayList<>(AdaptingTypes.adaptingList((List) fieldValue, bidiConverter)); + return Optional.of(convertedList); } + return Optional.of( fieldToValueConverter(fieldDescriptor).forwardConverter().convert(fieldValue)); } @@ -234,11 +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 (isUnknown(fieldValue)) { - return Optional.of(fieldValue); + 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(); @@ -254,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())) @@ -264,35 +254,102 @@ 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()) { case SFIXED32: case SINT32: case INT32: - return INT_CONVERTER; + return unwrapAndConvert(INT_CONVERTER); case FIXED32: case UINT32: - if (enableUnsignedLongs) { - return UNSIGNED_UINT32_CONVERTER; + if (celOptions.enableUnsignedLongs()) { + return unwrapAndConvert(UNSIGNED_UINT32_CONVERTER); } - return SIGNED_UINT32_CONVERTER; + return unwrapAndConvert(SIGNED_UINT32_CONVERTER); case FIXED64: case UINT64: - if (enableUnsignedLongs) { - return UNSIGNED_UINT64_CONVERTER; + if (celOptions.enableUnsignedLongs()) { + return unwrapAndConvert(UNSIGNED_UINT64_CONVERTER); } - return BidiConverter.IDENTITY; + return BidiConverter.of( + BidiConverter.IDENTITY.forwardConverter(), + value -> BidiConverter.IDENTITY.backwardConverter().convert(maybeUnwrap(value))); case FLOAT: - return DOUBLE_CONVERTER; + 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( + ProtoAdapter::adaptProtoByteStringToValue, + value -> adaptCelByteStringToProto(maybeUnwrap(value))); + } + + return BidiConverter.of( + BidiConverter.IDENTITY.forwardConverter(), + value -> BidiConverter.IDENTITY.backwardConverter().convert(maybeUnwrap(value))); + case STRING: + return BidiConverter.of( + BidiConverter.IDENTITY.forwardConverter(), + value -> BidiConverter.IDENTITY.backwardConverter().convert(maybeUnwrap(value))); + case BOOL: + return BidiConverter.of( + BidiConverter.IDENTITY.forwardConverter(), + value -> BidiConverter.IDENTITY.backwardConverter().convert(maybeUnwrap(value))); case ENUM: return BidiConverter.of( value -> (long) ((EnumValueDescriptor) value).getNumber(), @@ -303,17 +360,28 @@ private BidiConverter fieldToValueConverter(FieldDescriptor fieldDescriptor) { case MESSAGE: return BidiConverter.of( this::adaptProtoToValue, - value -> - adaptValueToProto(value, fieldDescriptor.getMessageType().getFullName()) - .orElseThrow( - () -> - new IllegalStateException( - String.format("value not convertible to proto: %s", value)))); + value -> adaptValueToProto(value, fieldDescriptor.getMessageType().getFullName())); default: return BidiConverter.IDENTITY; } } + private static CelByteString adaptProtoByteStringToValue(Object proto) { + if (proto instanceof CelByteString) { + return (CelByteString) proto; + } + + return CelByteString.of(((ByteString) proto).toByteArray()); + } + + private static ByteString adaptCelByteStringToProto(Object value) { + if (value instanceof ByteString) { + return (ByteString) value; + } + + return ByteString.copyFrom(((CelByteString) value).toByteArray()); + } + /** * Adapt the Java object {@code value} to the given protobuf {@code protoTypeName} if possible. * @@ -322,284 +390,25 @@ private BidiConverter fieldToValueConverter(FieldDescriptor fieldDescriptor) { * protoTypeName} will indicate an alternative packaging of the value which needs to be * considered, such as a packing an {@code google.protobuf.StringValue} into a {@code Any} value. */ - public Optional adaptValueToProto(Object value, String protoTypeName) { - WellKnownProto wellKnownProto = WellKnownProto.getByDescriptorName(protoTypeName); + public Message adaptValueToProto(Object value, String protoTypeName) { + WellKnownProto wellKnownProto = WellKnownProto.getByTypeName(protoTypeName).orElse(null); if (wellKnownProto == null) { if (value instanceof Message) { - return Optional.of((Message) value); + return (Message) value; } - return Optional.empty(); + + throw new IllegalStateException(String.format("value not convertible to proto: %s", value)); } + switch (wellKnownProto) { case ANY_VALUE: - return Optional.ofNullable(adaptValueToAny(value)); - case JSON_VALUE: - return Optional.ofNullable(adaptValueToJsonValue(value)); - case JSON_LIST_VALUE: - return Optional.ofNullable(adaptValueToJsonListValue(value)); - case JSON_STRUCT_VALUE: - return Optional.ofNullable(adaptValueToJsonStructValue(value)); - case BOOL_VALUE: - if (value instanceof Boolean) { - return Optional.of(BoolValue.of((Boolean) value)); + if (value instanceof Message) { + protoTypeName = ((Message) value).getDescriptorForType().getFullName(); } - break; - case BYTES_VALUE: - if (value instanceof ByteString) { - return Optional.of(BytesValue.of((ByteString) value)); - } - break; - case DOUBLE_VALUE: - return Optional.ofNullable(adaptValueToDouble(value)); - case DURATION_VALUE: - return Optional.of((Duration) value); - case FLOAT_VALUE: - return Optional.ofNullable(adaptValueToFloat(value)); - case INT32_VALUE: - return Optional.ofNullable(adaptValueToInt32(value)); - case INT64_VALUE: - return Optional.ofNullable(adaptValueToInt64(value)); - case STRING_VALUE: - if (value instanceof String) { - return Optional.of(StringValue.of((String) value)); - } - break; - case TIMESTAMP_VALUE: - return Optional.of((Timestamp) value); - case UINT32_VALUE: - return Optional.ofNullable(adaptValueToUint32(value)); - case UINT64_VALUE: - return Optional.ofNullable(adaptValueToUint64(value)); - } - return Optional.empty(); - } - - // Helper functions which return a {@code null} value if the conversion is not successful. - // This technique was chosen over {@code Optional} for brevity as any call site which might - // care about an Optional return is handled higher up the call stack. - - private @Nullable Message adaptValueToAny(Object value) { - if (value == null || value instanceof NullValue) { - return Any.pack(Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build()); - } - if (value instanceof Boolean) { - return maybePackAny(value, WellKnownProto.BOOL_VALUE); - } - if (value instanceof ByteString) { - return maybePackAny(value, WellKnownProto.BYTES_VALUE); - } - if (value instanceof Double) { - return maybePackAny(value, WellKnownProto.DOUBLE_VALUE); - } - if (value instanceof Float) { - return maybePackAny(value, WellKnownProto.FLOAT_VALUE); - } - if (value instanceof Integer) { - return maybePackAny(value, WellKnownProto.INT32_VALUE); - } - if (value instanceof Long) { - return maybePackAny(value, WellKnownProto.INT64_VALUE); - } - if (value instanceof Message) { - return Any.pack((Message) value); - } - if (value instanceof Iterable) { - return maybePackAny(value, WellKnownProto.JSON_LIST_VALUE); - } - if (value instanceof Map) { - return maybePackAny(value, WellKnownProto.JSON_STRUCT_VALUE); - } - if (value instanceof String) { - return maybePackAny(value, WellKnownProto.STRING_VALUE); - } - if (value instanceof UnsignedLong) { - return maybePackAny(value, WellKnownProto.UINT64_VALUE); - } - return null; - } - - private @Nullable Any maybePackAny(Object value, WellKnownProto wellKnownProto) { - Optional protoValue = adaptValueToProto(value, wellKnownProto.typeName()); - if (protoValue.isPresent()) { - return Any.pack(protoValue.get()); - } - return null; - } - - private static final long JSON_MAX_INT_VALUE = (1L << 53) - 1; - private static final long JSON_MIN_INT_VALUE = -JSON_MAX_INT_VALUE; - private static final UnsignedLong JSON_MAX_UINT_VALUE = - UnsignedLong.fromLongBits(JSON_MAX_INT_VALUE); - - private @Nullable Value adaptValueToJsonValue(Object value) { - Value.Builder json = Value.newBuilder(); - if (value == null || value instanceof NullValue) { - return json.setNullValue(NullValue.NULL_VALUE).build(); - } - if (value instanceof Boolean) { - return json.setBoolValue((Boolean) value).build(); - } - if (value instanceof Integer || value instanceof Long) { - long longValue = ((Number) value).longValue(); - if (longValue < JSON_MIN_INT_VALUE || longValue > JSON_MAX_INT_VALUE) { - return json.setStringValue(Long.toString(longValue)).build(); - } - return json.setNumberValue((double) longValue).build(); - } - if (value instanceof UnsignedLong) { - if (((UnsignedLong) value).compareTo(JSON_MAX_UINT_VALUE) > 0) { - return json.setStringValue(((UnsignedLong) value).toString()).build(); - } - return json.setNumberValue((double) ((UnsignedLong) value).longValue()).build(); - } - if (value instanceof Float || value instanceof Double) { - return json.setNumberValue(((Number) value).doubleValue()).build(); - } - if (value instanceof ByteString) { - return json.setStringValue( - Base64.getEncoder().encodeToString(((ByteString) value).toByteArray())) - .build(); - } - if (value instanceof String) { - return json.setStringValue((String) value).build(); - } - if (value instanceof Map) { - Struct struct = adaptValueToJsonStructValue(value); - if (struct != null) { - return json.setStructValue(struct).build(); - } - } - if (value instanceof Iterable) { - ListValue listValue = adaptValueToJsonListValue(value); - if (listValue != null) { - return json.setListValue(listValue).build(); - } - } - return null; - } - - private @Nullable ListValue adaptValueToJsonListValue(Object value) { - if (!(value instanceof Iterable)) { - return null; - } - Iterable list = (Iterable) value; - ListValue.Builder jsonList = ListValue.newBuilder(); - for (Object elem : list) { - jsonList.addValues(adaptValueToJsonValue(elem)); - } - return jsonList.build(); - } - - private @Nullable Struct adaptValueToJsonStructValue(Object value) { - if (!(value instanceof Map)) { - return null; - } - Map map = (Map) value; - Struct.Builder struct = Struct.newBuilder(); - for (Map.Entry entry : map.entrySet()) { - Object key = entry.getKey(); - Object keyValue = entry.getValue(); - if (!(key instanceof String)) { - // Not a valid map key type for JSON. - return null; - } - struct.putFields((String) key, adaptValueToJsonValue(keyValue)); - } - return struct.build(); - } - - private @Nullable Message adaptValueToDouble(Object value) { - if (value instanceof Double) { - return DoubleValue.of((Double) value); - } - if (value instanceof Float) { - return DoubleValue.of(((Float) value).doubleValue()); - } - return null; - } - - private @Nullable Message adaptValueToFloat(Object value) { - if (value instanceof Double) { - return FloatValue.of(((Double) value).floatValue()); - } - if (value instanceof Float) { - return FloatValue.of((Float) value); - } - return null; - } - - private @Nullable Message adaptValueToInt32(Object value) { - if (value instanceof Integer) { - return Int32Value.of((Integer) value); - } - if (value instanceof Long) { - return Int32Value.of(intCheckedCast((Long) value)); - } - return null; - } - - private @Nullable Message adaptValueToInt64(Object value) { - if (value instanceof Integer) { - return Int64Value.of(((Integer) value).longValue()); - } - if (value instanceof Long) { - return Int64Value.of((Long) value); - } - return null; - } - - private @Nullable Message adaptValueToUint32(Object value) { - if (value instanceof Integer) { - return UInt32Value.of((Integer) value); - } - if (value instanceof Long) { - try { - return UInt32Value.of(unsignedIntCheckedCast((Long) value)); - } catch (IllegalArgumentException e) { - throw new CelRuntimeException(e, CelErrorCode.NUMERIC_OVERFLOW); - } - } - if (value instanceof UnsignedLong) { - try { - return UInt32Value.of(unsignedIntCheckedCast(((UnsignedLong) value).longValue())); - } catch (IllegalArgumentException e) { - throw new CelRuntimeException(e, CelErrorCode.NUMERIC_OVERFLOW); - } - } - return null; - } - - private @Nullable Message adaptValueToUint64(Object value) { - if (value instanceof Integer) { - return UInt64Value.of(UnsignedInts.toLong((Integer) value)); - } - if (value instanceof Long) { - return UInt64Value.of((Long) value); - } - if (value instanceof UnsignedLong) { - return UInt64Value.of(((UnsignedLong) value).longValue()); - } - return null; - } - - private @Nullable Object adaptJsonToValue(Value value) { - switch (value.getKindCase()) { - case BOOL_VALUE: - return value.getBoolValue(); - case NULL_VALUE: - return value.getNullValue(); - case NUMBER_VALUE: - return value.getNumberValue(); - case STRING_VALUE: - return value.getStringValue(); - case LIST_VALUE: - return adaptJsonListToValue(value.getListValue()); - case STRUCT_VALUE: - return adaptJsonStructToValue(value.getStructValue()); - case KIND_NOT_SET: - return NullValue.NULL_VALUE; + return protoLiteAdapter.adaptValueToAny(value, protoTypeName); + default: + return (Message) protoLiteAdapter.adaptValueToWellKnownProto(value, wellKnownProto); } - return null; } private Object unpackAnyProto(Any anyProto) { @@ -610,17 +419,6 @@ private Object unpackAnyProto(Any anyProto) { } } - private ImmutableList adaptJsonListToValue(ListValue listValue) { - return listValue.getValuesList().stream() - .map(this::adaptJsonToValue) - .collect(ImmutableList.toImmutableList()); - } - - private ImmutableMap adaptJsonStructToValue(Struct struct) { - return struct.getFieldsMap().entrySet().stream() - .collect(toImmutableMap(e -> e.getKey(), e -> adaptJsonToValue(e.getValue()))); - } - /** Returns the default value for a field that can be a proto message */ private static Object getDefaultValueForMaybeMessage(FieldDescriptor descriptor) { if (descriptor.getJavaType() == FieldDescriptor.JavaType.MESSAGE) { @@ -634,25 +432,11 @@ 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(); - WellKnownProto wellKnownProto = WellKnownProto.getByDescriptorName(fieldTypeName); - return wellKnownProto != null && wellKnownProto.isWrapperType(); - } - - private static boolean isUnknown(Object object) { - return object instanceof ExprValue - && ((ExprValue) object).getKindCase() == ExprValue.KindCase.UNKNOWN; - } - private static int intCheckedCast(long value) { try { return Ints.checkedCast(value); } catch (IllegalArgumentException e) { - throw new CelRuntimeException(e, CelErrorCode.NUMERIC_OVERFLOW); + throw new CelNumericOverflowException(e); } } @@ -660,7 +444,21 @@ private static int unsignedIntCheckedCast(long value) { try { return UnsignedInts.checkedCast(value); } catch (IllegalArgumentException e) { - throw new CelRuntimeException(e, CelErrorCode.NUMERIC_OVERFLOW); + throw new CelNumericOverflowException(e); + } + } + + private Object maybeUnwrap(Object value) { + if (value instanceof Message) { + return adaptProtoToValue((MessageOrBuilder) value); } + return value; + } + + private BidiConverter unwrapAndConvert( + final BidiConverter original) { + return BidiConverter.of( + original.forwardConverter()::convert, + value -> original.backwardConverter().convert((Number) maybeUnwrap(value))); } } diff --git a/common/src/main/java/dev/cel/common/internal/ProtoLiteAdapter.java b/common/src/main/java/dev/cel/common/internal/ProtoLiteAdapter.java new file mode 100644 index 000000000..3b13ecea1 --- /dev/null +++ b/common/src/main/java/dev/cel/common/internal/ProtoLiteAdapter.java @@ -0,0 +1,360 @@ +// 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 static com.google.common.collect.ImmutableMap.toImmutableMap; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.primitives.Ints; +import com.google.common.primitives.UnsignedInts; +import com.google.common.primitives.UnsignedLong; +import com.google.errorprone.annotations.Immutable; +import com.google.protobuf.Any; +import com.google.protobuf.BoolValue; +import com.google.protobuf.ByteString; +import com.google.protobuf.BytesValue; +import com.google.protobuf.DoubleValue; +import com.google.protobuf.Duration; +import com.google.protobuf.FloatValue; +import com.google.protobuf.Int32Value; +import com.google.protobuf.Int64Value; +import com.google.protobuf.ListValue; +import com.google.protobuf.Message; +import com.google.protobuf.MessageLite; +import com.google.protobuf.MessageLiteOrBuilder; +import com.google.protobuf.NullValue; +import com.google.protobuf.StringValue; +import com.google.protobuf.Struct; +import com.google.protobuf.Timestamp; +import com.google.protobuf.UInt32Value; +import com.google.protobuf.UInt64Value; +import com.google.protobuf.Value; +import dev.cel.common.CelOptions; +import dev.cel.common.CelProtoJsonAdapter; +import dev.cel.common.annotations.Internal; +import dev.cel.common.exceptions.CelNumericOverflowException; +import dev.cel.common.values.CelByteString; +import java.time.Instant; +import java.util.Map; +import java.util.Map.Entry; + +/** + * {@code ProtoLiteAdapter} utilities handle conversion between native Java objects which represent + * CEL values and well-known protobuf counterparts. + * + *

This adapter does not leverage descriptors, thus is compatible with lite-variants of protobuf + * messages. + * + *

CEL Library Internals. Do Not Use. + */ +@Internal +@Immutable +public final class ProtoLiteAdapter { + + private final CelOptions celOptions; + + @SuppressWarnings("unchecked") + public MessageLite adaptValueToWellKnownProto(Object value, WellKnownProto wellKnownProto) { + switch (wellKnownProto) { + case JSON_VALUE: + return CelProtoJsonAdapter.adaptValueToJsonValue(value); + case JSON_STRUCT_VALUE: + return CelProtoJsonAdapter.adaptToJsonStructValue((Map) value); + case JSON_LIST_VALUE: + return CelProtoJsonAdapter.adaptToJsonListValue((Iterable) value); + case BOOL_VALUE: + return BoolValue.of((Boolean) value); + case BYTES_VALUE: + CelByteString byteString = (CelByteString) value; + return BytesValue.of(ByteString.copyFrom(byteString.toByteArray())); + case DOUBLE_VALUE: + return adaptValueToDouble(value); + case FLOAT_VALUE: + return adaptValueToFloat(value); + case INT32_VALUE: + return adaptValueToInt32(value); + case INT64_VALUE: + return adaptValueToInt64(value); + case STRING_VALUE: + return StringValue.of((String) value); + case UINT32_VALUE: + return adaptValueToUint32(value); + case UINT64_VALUE: + return adaptValueToUint64(value); + case DURATION: + return adaptValueToProtoDuration(value); + case TIMESTAMP: + return adaptValueToProtoTimestamp(value); + case EMPTY: + case FIELD_MASK: + // These two WKTs are typically used in context of JSON conversions, in which they are + // automatically unwrapped into equivalent primitive types. + // In other cases, just return the original message itself. + return (MessageLite) value; + default: + throw new IllegalArgumentException( + "Unexpected wellKnownProto kind: " + wellKnownProto + " for value: " + value); + } + } + + public Any adaptValueToAny(Object value, String typeName) { + if (value instanceof MessageLite) { + return packAnyMessage((MessageLite) value, typeName); + } + + if (value instanceof dev.cel.common.values.NullValue) { + return packAnyMessage( + Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build(), WellKnownProto.JSON_VALUE); + } + + WellKnownProto wellKnownProto; + + if (value instanceof Boolean) { + wellKnownProto = WellKnownProto.BOOL_VALUE; + } else if (value instanceof CelByteString) { + wellKnownProto = WellKnownProto.BYTES_VALUE; + } else if (value instanceof String) { + wellKnownProto = WellKnownProto.STRING_VALUE; + } else if (value instanceof Float) { + wellKnownProto = WellKnownProto.FLOAT_VALUE; + } else if (value instanceof Double) { + wellKnownProto = WellKnownProto.DOUBLE_VALUE; + } else if (value instanceof Long) { + wellKnownProto = WellKnownProto.INT64_VALUE; + } else if (value instanceof UnsignedLong) { + wellKnownProto = WellKnownProto.UINT64_VALUE; + } else if (value instanceof Iterable) { + wellKnownProto = WellKnownProto.JSON_LIST_VALUE; + } else if (value instanceof Map) { + wellKnownProto = WellKnownProto.JSON_STRUCT_VALUE; + } else if (value instanceof Instant) { + wellKnownProto = WellKnownProto.TIMESTAMP; + } else if (value instanceof java.time.Duration) { + wellKnownProto = WellKnownProto.DURATION; + } else { + throw new IllegalArgumentException("Unsupported value conversion to any: " + value); + } + + MessageLite wellKnownProtoMsg = adaptValueToWellKnownProto(value, wellKnownProto); + return packAnyMessage(wellKnownProtoMsg, wellKnownProto); + } + + public Object adaptWellKnownProtoToValue( + MessageLiteOrBuilder proto, WellKnownProto wellKnownProto) { + // Exhaustive switch over the conversion and adaptation of well-known protobuf types to Java + // values. + switch (wellKnownProto) { + case JSON_VALUE: + return adaptJsonToValue((Value) proto); + case JSON_STRUCT_VALUE: + return adaptJsonStructToValue((Struct) proto); + case JSON_LIST_VALUE: + return adaptJsonListToValue((ListValue) proto); + case BOOL_VALUE: + return ((BoolValue) proto).getValue(); + case BYTES_VALUE: + ByteString byteString = ((BytesValue) proto).getValue(); + return CelByteString.of(byteString.toByteArray()); + case DOUBLE_VALUE: + return ((DoubleValue) proto).getValue(); + case FLOAT_VALUE: + return (double) ((FloatValue) proto).getValue(); + case INT32_VALUE: + return (long) ((Int32Value) proto).getValue(); + case INT64_VALUE: + return ((Int64Value) proto).getValue(); + case STRING_VALUE: + return ((StringValue) proto).getValue(); + case UINT32_VALUE: + if (celOptions.enableUnsignedLongs()) { + return UnsignedLong.fromLongBits( + Integer.toUnsignedLong(((UInt32Value) proto).getValue())); + } + return (long) ((UInt32Value) proto).getValue(); + case UINT64_VALUE: + if (celOptions.enableUnsignedLongs()) { + return UnsignedLong.fromLongBits(((UInt64Value) proto).getValue()); + } + return ((UInt64Value) proto).getValue(); + case TIMESTAMP: + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return ProtoTimeUtils.toJavaInstant((Timestamp) proto); + } + return proto; + case DURATION: + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return ProtoTimeUtils.toJavaDuration((Duration) proto); + } + return proto; + default: + return proto; + } + } + + private Object adaptJsonToValue(Value value) { + switch (value.getKindCase()) { + case BOOL_VALUE: + return value.getBoolValue(); + case NULL_VALUE: + case KIND_NOT_SET: + return dev.cel.common.values.NullValue.NULL_VALUE; + case NUMBER_VALUE: + return value.getNumberValue(); + case STRING_VALUE: + return value.getStringValue(); + case LIST_VALUE: + return adaptJsonListToValue(value.getListValue()); + case STRUCT_VALUE: + return adaptJsonStructToValue(value.getStructValue()); + } + throw new IllegalArgumentException("unexpected value kind: " + value.getKindCase()); + } + + private ImmutableList adaptJsonListToValue(ListValue listValue) { + return listValue.getValuesList().stream() + .map(this::adaptJsonToValue) + .collect(ImmutableList.toImmutableList()); + } + + private ImmutableMap adaptJsonStructToValue(Struct struct) { + return struct.getFieldsMap().entrySet().stream() + .collect(toImmutableMap(Entry::getKey, e -> adaptJsonToValue(e.getValue()))); + } + + private Message adaptValueToDouble(Object value) { + if (value instanceof Double) { + return DoubleValue.of((Double) value); + } + if (value instanceof Float) { + return DoubleValue.of(((Float) value).doubleValue()); + } + throw new IllegalArgumentException("Unexpected value type: " + value); + } + + private Message adaptValueToFloat(Object value) { + if (value instanceof Double) { + return FloatValue.of(((Double) value).floatValue()); + } + if (value instanceof Float) { + return FloatValue.of((Float) value); + } + throw new IllegalArgumentException("Unexpected value type: " + value); + } + + private Message adaptValueToInt32(Object value) { + if (value instanceof Integer) { + return Int32Value.of((Integer) value); + } + if (value instanceof Long) { + return Int32Value.of(intCheckedCast((Long) value)); + } + throw new IllegalArgumentException("Unexpected value type: " + value); + } + + private Message adaptValueToInt64(Object value) { + if (value instanceof Integer) { + return Int64Value.of(((Integer) value).longValue()); + } + if (value instanceof Long) { + return Int64Value.of((Long) value); + } + + throw new IllegalArgumentException("Unexpected value type: " + value); + } + + private Message adaptValueToUint32(Object value) { + if (value instanceof Integer) { + return UInt32Value.of((Integer) value); + } + if (value instanceof Long) { + try { + return UInt32Value.of(unsignedIntCheckedCast((Long) value)); + } catch (IllegalArgumentException e) { + throw new CelNumericOverflowException(e); + } + } + if (value instanceof UnsignedLong) { + try { + return UInt32Value.of(unsignedIntCheckedCast(((UnsignedLong) value).longValue())); + } catch (IllegalArgumentException e) { + throw new CelNumericOverflowException(e); + } + } + + throw new IllegalArgumentException("Unexpected value type: " + value); + } + + private Message adaptValueToUint64(Object value) { + if (value instanceof Integer) { + return UInt64Value.of(UnsignedInts.toLong((Integer) value)); + } + if (value instanceof Long) { + return UInt64Value.of((Long) value); + } + if (value instanceof UnsignedLong) { + return UInt64Value.of(((UnsignedLong) value).longValue()); + } + + throw new IllegalArgumentException("Unexpected value type: " + value); + } + + private static int intCheckedCast(long value) { + try { + return Ints.checkedCast(value); + } catch (IllegalArgumentException e) { + throw new CelNumericOverflowException(e); + } + } + + private static int unsignedIntCheckedCast(long value) { + try { + return UnsignedInts.checkedCast(value); + } catch (IllegalArgumentException e) { + throw new CelNumericOverflowException(e); + } + } + + private static Any packAnyMessage(MessageLite msg, WellKnownProto wellKnownProto) { + return packAnyMessage(msg, wellKnownProto.typeName()); + } + + private static Any packAnyMessage(MessageLite msg, String typeUrl) { + return Any.newBuilder() + .setValue(msg.toByteString()) + .setTypeUrl("type.googleapis.com/" + typeUrl) + .build(); + } + + private Timestamp adaptValueToProtoTimestamp(Object value) { + if (!celOptions.evaluateCanonicalTypesToNativeValues()) { + return (Timestamp) value; + } + + return ProtoTimeUtils.toProtoTimestamp((Instant) value); + } + + private Duration adaptValueToProtoDuration(Object value) { + if (!celOptions.evaluateCanonicalTypesToNativeValues()) { + return (Duration) value; + } + + return ProtoTimeUtils.toProtoDuration((java.time.Duration) value); + } + + public ProtoLiteAdapter(CelOptions celOptions) { + this.celOptions = celOptions; + } +} diff --git a/common/src/main/java/dev/cel/common/internal/ProtoTimeUtils.java b/common/src/main/java/dev/cel/common/internal/ProtoTimeUtils.java new file mode 100644 index 000000000..36671842d --- /dev/null +++ b/common/src/main/java/dev/cel/common/internal/ProtoTimeUtils.java @@ -0,0 +1,572 @@ +// 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 static com.google.common.math.LongMath.checkedAdd; +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; +import com.google.protobuf.Timestamp; +import dev.cel.common.annotations.Internal; +import java.io.Serializable; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.time.Instant; +import java.util.Comparator; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.Locale; +import java.util.TimeZone; + +/** + * Utility methods for handling {@code protobuf/duration.proto} and {@code + * protobuf/timestamp.proto}. + * + *

Forked from com.google.protobuf.util package. These exist because there's not an equivalent + * util JAR published in maven central that's compatible with protolite. See relevant github issue. + * + *

CEL Library Internals. Do Not Use. + */ +@Internal +// Forked from protobuf-java-utils. Retaining units/date API for parity. +@SuppressWarnings({"GoodTime-ApiWithNumericTimeUnit", "JavaUtilDate"}) +public final class ProtoTimeUtils { + + // Timestamp for "0001-01-01T00:00:00Z" + @VisibleForTesting + 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; + + private static final int MILLIS_PER_SECOND = 1000; + + private static final int NANOS_PER_SECOND = 1000000000; + private static final int NANOS_PER_MILLISECOND = 1000000; + private static final int NANOS_PER_MICROSECOND = 1000; + + private static final long SECONDS_PER_MINUTE = 60L; + private static final long SECONDS_PER_HOUR = SECONDS_PER_MINUTE * 60; + + private static final ThreadLocal TIMESTAMP_FORMAT = + new ThreadLocal() { + @Override + protected SimpleDateFormat initialValue() { + return createTimestampFormat(); + } + }; + + private enum TimestampComparator implements Comparator, Serializable { + INSTANCE; + + @Override + public int compare(Timestamp t1, Timestamp t2) { + checkValid(t1); + checkValid(t2); + int secDiff = Long.compare(t1.getSeconds(), t2.getSeconds()); + return (secDiff != 0) ? secDiff : Integer.compare(t1.getNanos(), t2.getNanos()); + } + } + + private enum DurationComparator implements Comparator, Serializable { + INSTANCE; + + @Override + public int compare(Duration d1, Duration d2) { + checkValid(d1); + checkValid(d2); + int secDiff = Long.compare(d1.getSeconds(), d2.getSeconds()); + return (secDiff != 0) ? secDiff : Integer.compare(d1.getNanos(), d2.getNanos()); + } + } + + /** + * A constant holding the {@link Timestamp} of epoch time, {@code 1970-01-01T00:00:00.000000000Z}. + */ + public static final Timestamp TIMESTAMP_EPOCH = + Timestamp.newBuilder().setSeconds(0).setNanos(0).build(); + + /** A constant holding the duration of zero. */ + public static final Duration DURATION_ZERO = + Duration.newBuilder().setSeconds(0L).setNanos(0).build(); + + private static SimpleDateFormat createTimestampFormat() { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH); + GregorianCalendar calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC")); + // We use Proleptic Gregorian Calendar (i.e., Gregorian calendar extends + // backwards to year one) for timestamp formatting. + calendar.setGregorianChange(new Date(Long.MIN_VALUE)); + sdf.setCalendar(calendar); + return sdf; + } + + /** Convert a {@link Instant} object to proto-based {@link Timestamp}. */ + public static Timestamp toProtoTimestamp(Instant instant) { + return normalizedTimestamp(instant.getEpochSecond(), instant.getNano()); + } + + /** Convert a {@link java.time.Duration} object to proto-based {@link Duration}. */ + public static Duration toProtoDuration(java.time.Duration duration) { + return normalizedDuration(duration.getSeconds(), duration.getNano()); + } + + /** Convert a {@link Timestamp} object to java-based {@link Instant}. */ + public static Instant toJavaInstant(Timestamp timestamp) { + timestamp = normalizedTimestamp(timestamp.getSeconds(), timestamp.getNanos()); + return Instant.ofEpochSecond(timestamp.getSeconds(), timestamp.getNanos()); + } + + /** Convert a {@link Duration} object to java-based {@link java.time.Duration}. */ + public static java.time.Duration toJavaDuration(Duration duration) { + duration = normalizedDuration(duration.getSeconds(), duration.getNanos()); + return java.time.Duration.ofSeconds(duration.getSeconds(), duration.getNanos()); + } + + /** Convert a Timestamp to the number of seconds elapsed from the epoch. */ + public static long toSeconds(Timestamp timestamp) { + return checkValid(timestamp).getSeconds(); + } + + /** + * Convert a Duration to the number of seconds. The result will be rounded towards 0 to the + * nearest second. E.g., if the duration represents -1 nanosecond, it will be rounded to 0. + */ + public static long toSeconds(Duration duration) { + return checkValid(duration).getSeconds(); + } + + /** + * Convert a Duration to the number of hours. The result will be rounded towards 0 to the nearest + * hour. + */ + public static long toHours(Duration duration) { + return checkValid(duration).getSeconds() / SECONDS_PER_HOUR; + } + + /** + * Convert a Duration to the number of minutes. The result will be rounded towards 0 to the + * nearest minute. + */ + public static long toMinutes(Duration duration) { + return checkValid(duration).getSeconds() / SECONDS_PER_MINUTE; + } + + /** + * Convert a Duration to the number of milliseconds. The result will be rounded towards 0 to the + * nearest millisecond. E.g., if the duration represents -1 nanosecond, it will be rounded to 0. + */ + public static long toMillis(Duration duration) { + checkValid(duration); + return checkedAdd( + checkedMultiply(duration.getSeconds(), MILLIS_PER_SECOND), + duration.getNanos() / NANOS_PER_MILLISECOND); + } + + /** Create a Timestamp from the number of seconds elapsed from the epoch. */ + public static Timestamp fromSecondsToTimestamp(long seconds) { + return normalizedTimestamp(seconds, 0); + } + + /** Create a Timestamp from the number of seconds elapsed from the epoch. */ + public static Duration fromSecondsToDuration(long seconds) { + return normalizedDuration(seconds, 0); + } + + /** Create a Timestamp from the number of seconds elapsed from the epoch. */ + public static Duration fromMillisToDuration(long milliseconds) { + return normalizedDuration( + milliseconds / MILLIS_PER_SECOND, + (int) (milliseconds % MILLIS_PER_SECOND * NANOS_PER_MILLISECOND)); + } + + /** Throws an {@link IllegalArgumentException} if the given {@link Duration} is not valid. */ + @CanIgnoreReturnValue + private static Duration checkValid(Duration duration) { + long seconds = duration.getSeconds(); + int nanos = duration.getNanos(); + if (!isDurationValid(seconds, nanos)) { + throw new IllegalArgumentException( + Strings.lenientFormat( + "Duration is not valid. See proto definition for valid values. " + + "Seconds (%s) must be in range [-315,576,000,000, +315,576,000,000]. " + + "Nanos (%s) must be in range [-999,999,999, +999,999,999]. " + + "Nanos must have the same sign as seconds", + seconds, nanos)); + } + return duration; + } + + /** Throws an {@link IllegalArgumentException} if the given {@link Timestamp} is not valid. */ + @CanIgnoreReturnValue + private static Timestamp checkValid(Timestamp timestamp) { + long seconds = timestamp.getSeconds(); + int nanos = timestamp.getNanos(); + if (!isTimestampValid(seconds, nanos)) { + throw new IllegalArgumentException( + Strings.lenientFormat( + "Timestamp is not valid. See proto definition for valid values. " + + "Seconds (%s) must be in range [-62,135,596,800, +253,402,300,799]. " + + "Nanos (%s) must be in range [0, +999,999,999].", + seconds, nanos)); + } + return timestamp; + } + + /** + * Convert Timestamp to RFC 3339 date string format. The output will always be Z-normalized and + * uses 0, 3, 6 or 9 fractional digits as required to represent the exact value. Note that + * Timestamp can only represent time from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. + * See https://www.ietf.org/rfc/rfc3339.txt + * + *

Example of generated format: "1972-01-01T10:00:20.021Z" + * + * @return The string representation of the given timestamp. + * @throws IllegalArgumentException if the given timestamp is not in the valid range. + */ + public static String toString(Timestamp timestamp) { + checkValid(timestamp); + + long seconds = timestamp.getSeconds(); + int nanos = timestamp.getNanos(); + + StringBuilder result = new StringBuilder(); + // Format the seconds part. + Date date = new Date(seconds * MILLIS_PER_SECOND); + result.append(TIMESTAMP_FORMAT.get().format(date)); + // Format the nanos part. + if (nanos != 0) { + result.append("."); + result.append(formatNanos(nanos)); + } + result.append("Z"); + return result.toString(); + } + + /** + * Convert Duration to string format. The string format will contains 3, 6, or 9 fractional digits + * depending on the precision required to represent the exact Duration value. For example: "1s", + * "1.010s", "1.000000100s", "-3.100s" The range that can be represented by Duration is from + * -315,576,000,000 to +315,576,000,000 inclusive (in seconds). + * + * @return The string representation of the given duration. + * @throws IllegalArgumentException if the given duration is not in the valid range. + */ + public static String toString(Duration duration) { + checkValid(duration); + + long seconds = duration.getSeconds(); + int nanos = duration.getNanos(); + + StringBuilder result = new StringBuilder(); + if (seconds < 0 || nanos < 0) { + result.append("-"); + seconds = -seconds; + nanos = -nanos; + } + result.append(seconds); + if (nanos != 0) { + result.append("."); + result.append(formatNanos(nanos)); + } + result.append("s"); + return result.toString(); + } + + /** + * Parse from RFC 3339 date string to Timestamp. This method accepts all outputs of {@link + * #toString(Timestamp)} and it also accepts any fractional digits (or none) and any offset as + * long as they fit into nano-seconds precision. + * + *

Example of accepted format: "1972-01-01T10:00:20.021-05:00" + * + * @return a Timestamp parsed from the string + * @throws ParseException if parsing fails + */ + public static Timestamp parse(String value) throws ParseException { + int dayOffset = value.indexOf('T'); + if (dayOffset == -1) { + throw new ParseException("Failed to parse timestamp: invalid timestamp \"" + value + "\"", 0); + } + int timezoneOffsetPosition = value.indexOf('Z', dayOffset); + if (timezoneOffsetPosition == -1) { + timezoneOffsetPosition = value.indexOf('+', dayOffset); + } + if (timezoneOffsetPosition == -1) { + timezoneOffsetPosition = value.indexOf('-', dayOffset); + } + if (timezoneOffsetPosition == -1) { + throw new ParseException("Failed to parse timestamp: missing valid timezone offset.", 0); + } + // Parse seconds and nanos. + String timeValue = value.substring(0, timezoneOffsetPosition); + String secondValue = timeValue; + String nanoValue = ""; + int pointPosition = timeValue.indexOf('.'); + if (pointPosition != -1) { + secondValue = timeValue.substring(0, pointPosition); + nanoValue = timeValue.substring(pointPosition + 1); + } + Date date = TIMESTAMP_FORMAT.get().parse(secondValue); + long seconds = date.getTime() / MILLIS_PER_SECOND; + int nanos = nanoValue.isEmpty() ? 0 : parseNanos(nanoValue); + // Parse timezone offsets. + if (value.charAt(timezoneOffsetPosition) == 'Z') { + if (value.length() != timezoneOffsetPosition + 1) { + throw new ParseException( + "Failed to parse timestamp: invalid trailing data \"" + + value.substring(timezoneOffsetPosition) + + "\"", + 0); + } + } else { + String offsetValue = value.substring(timezoneOffsetPosition + 1); + long offset = parseTimezoneOffset(offsetValue); + if (value.charAt(timezoneOffsetPosition) == '+') { + seconds -= offset; + } else { + seconds += offset; + } + } + try { + Timestamp timestamp = normalizedTimestamp(seconds, nanos); + return checkValid(timestamp); + } catch (IllegalArgumentException e) { + ParseException ex = + new ParseException( + "Failed to parse timestamp " + value + " Timestamp is out of range.", 0); + ex.initCause(e); + throw ex; + } + } + + /** Adds two durations */ + public static Duration add(Duration d1, Duration d2) { + java.time.Duration javaDuration1 = ProtoTimeUtils.toJavaDuration(checkValid(d1)); + java.time.Duration javaDuration2 = ProtoTimeUtils.toJavaDuration(checkValid(d2)); + + java.time.Duration sum = javaDuration1.plus(javaDuration2); + + return ProtoTimeUtils.toProtoDuration(sum); + } + + /** Adds two timestamps. */ + public static Timestamp add(Timestamp ts, Duration dur) { + Instant javaInstant = ProtoTimeUtils.toJavaInstant(checkValid(ts)); + java.time.Duration javaDuration = ProtoTimeUtils.toJavaDuration(checkValid(dur)); + + Instant newInstant = javaInstant.plus(javaDuration); + + return ProtoTimeUtils.toProtoTimestamp(newInstant); + } + + /** Subtract a duration from another. */ + public static Duration subtract(Duration d1, Duration d2) { + java.time.Duration javaDuration1 = ProtoTimeUtils.toJavaDuration(checkValid(d1)); + java.time.Duration javaDuration2 = ProtoTimeUtils.toJavaDuration(checkValid(d2)); + + java.time.Duration sum = javaDuration1.minus(javaDuration2); + + return ProtoTimeUtils.toProtoDuration(sum); + } + + /** Subtracts two timestamps */ + public static Timestamp subtract(Timestamp ts, Duration dur) { + Instant javaInstant = ProtoTimeUtils.toJavaInstant(checkValid(ts)); + java.time.Duration javaDuration = ProtoTimeUtils.toJavaDuration(checkValid(dur)); + + Instant newInstant = javaInstant.minus(javaDuration); + + return ProtoTimeUtils.toProtoTimestamp(newInstant); + } + + /** Calculate the difference between two timestamps. */ + public static Duration between(Timestamp from, Timestamp to) { + Instant javaFrom = ProtoTimeUtils.toJavaInstant(checkValid(from)); + Instant javaTo = ProtoTimeUtils.toJavaInstant(checkValid(to)); + + java.time.Duration between = java.time.Duration.between(javaFrom, javaTo); + + return ProtoTimeUtils.toProtoDuration(between); + } + + /** + * Compares two durations. The value returned is identical to what would be returned by: {@code + * Durations.comparator().compare(x, y)}. + * + * @return the value {@code 0} if {@code x == y}; a value less than {@code 0} if {@code x < y}; + * and a value greater than {@code 0} if {@code x > y} + */ + public static int compare(Duration x, Duration y) { + return DurationComparator.INSTANCE.compare(x, y); + } + + /** + * Compares two timestamps. The value returned is identical to what would be returned by: {@code + * Timestamps.comparator().compare(x, y)}. + * + * @return the value {@code 0} if {@code x == y}; a value less than {@code 0} if {@code x < y}; + * and a value greater than {@code 0} if {@code x > y} + */ + public static int compare(Timestamp x, Timestamp y) { + return TimestampComparator.INSTANCE.compare(x, y); + } + + /** + * Create a {@link Timestamp} using the best-available (in terms of precision) system clock. + * + *

Note: that while this API is convenient, it may harm the testability of your code, as + * you're unable to mock the current time. Instead, you may want to consider injecting a clock + * instance to read the current time. + */ + public static Timestamp now() { + Instant nowInstant = Instant.now(); + + return Timestamp.newBuilder() + .setSeconds(nowInstant.getEpochSecond()) + .setNanos(nowInstant.getNano()) + .build(); + } + + private static long parseTimezoneOffset(String value) throws ParseException { + int pos = value.indexOf(':'); + if (pos == -1) { + throw new ParseException("Invalid offset value: " + value, 0); + } + String hours = value.substring(0, pos); + String minutes = value.substring(pos + 1); + try { + return (Long.parseLong(hours) * 60 + Long.parseLong(minutes)) * 60; + } catch (NumberFormatException e) { + ParseException ex = new ParseException("Invalid offset value: " + value, 0); + ex.initCause(e); + throw ex; + } + } + + private static int parseNanos(String value) throws ParseException { + int result = 0; + for (int i = 0; i < 9; ++i) { + result = result * 10; + if (i < value.length()) { + if (value.charAt(i) < '0' || value.charAt(i) > '9') { + throw new ParseException("Invalid nanoseconds.", 0); + } + result += value.charAt(i) - '0'; + } + } + return result; + } + + /** + * Returns true if the given number of seconds and nanos is a valid {@link Duration}. The {@code + * seconds} value must be in the range [-315,576,000,000, +315,576,000,000]. The {@code nanos} + * value must be in the range [-999,999,999, +999,999,999]. + * + *

Note: Durations less than one second are represented with a 0 {@code seconds} field + * and a positive or negative {@code nanos} field. For durations of one second or more, a non-zero + * value for the {@code nanos} field must be of the same sign as the {@code seconds} field. + */ + private static boolean isDurationValid(long seconds, int nanos) { + if (seconds < DURATION_SECONDS_MIN || seconds > DURATION_SECONDS_MAX) { + return false; + } + if (nanos < -999999999L || nanos >= NANOS_PER_SECOND) { + return false; + } + if (seconds < 0 || nanos < 0) { + if (seconds > 0 || nanos > 0) { + return false; + } + } + return true; + } + + /** + * Returns true if the given number of seconds and nanos is a valid {@link Timestamp}. The {@code + * seconds} value must be in the range [-62,135,596,800, +253,402,300,799] (i.e., between + * 0001-01-01T00:00:00Z and 9999-12-31T23:59:59Z). The {@code nanos} value must be in the range + * [0, +999,999,999]. + * + *

Note: Negative second values with fractional seconds must still have non-negative + * nanos values that count forward in time. + */ + private static boolean isTimestampValid(long seconds, int nanos) { + if (!isTimestampSecondsValid(seconds)) { + return false; + } + + return nanos >= 0 && nanos < NANOS_PER_SECOND; + } + + /** + * Returns true if the given number of seconds is valid, if combined with a valid number of nanos. + * The {@code seconds} value must be in the range [-62,135,596,800, +253,402,300,799] (i.e., + * between 0001-01-01T00:00:00Z and 9999-12-31T23:59:59Z). + */ + @SuppressWarnings("GoodTime") // this is a legacy conversion API + private static boolean isTimestampSecondsValid(long seconds) { + return seconds >= TIMESTAMP_SECONDS_MIN && seconds <= TIMESTAMP_SECONDS_MAX; + } + + private static Timestamp normalizedTimestamp(long seconds, int nanos) { + if (nanos <= -NANOS_PER_SECOND || nanos >= NANOS_PER_SECOND) { + seconds = checkedAdd(seconds, nanos / NANOS_PER_SECOND); + nanos = nanos % NANOS_PER_SECOND; + } + if (nanos < 0) { + nanos = nanos + NANOS_PER_SECOND; // no overflow since nanos is negative (and we're adding) + seconds = checkedSubtract(seconds, 1); + } + return Timestamp.newBuilder().setSeconds(seconds).setNanos(nanos).build(); + } + + private static Duration normalizedDuration(long seconds, int nanos) { + if (nanos <= -NANOS_PER_SECOND || nanos >= NANOS_PER_SECOND) { + seconds = checkedAdd(seconds, nanos / NANOS_PER_SECOND); + nanos %= NANOS_PER_SECOND; + } + if (seconds > 0 && nanos < 0) { + nanos += NANOS_PER_SECOND; // no overflow since nanos is negative (and we're adding) + seconds--; // no overflow since seconds is positive (and we're decrementing) + } + if (seconds < 0 && nanos > 0) { + nanos -= NANOS_PER_SECOND; // no overflow since nanos is positive (and we're subtracting) + seconds++; // no overflow since seconds is negative (and we're incrementing) + } + return Duration.newBuilder().setSeconds(seconds).setNanos(nanos).build(); + } + + private static String formatNanos(int nanos) { + // Determine whether to use 3, 6, or 9 digits for the nano part. + if (nanos % NANOS_PER_MILLISECOND == 0) { + return String.format(Locale.ENGLISH, "%1$03d", nanos / NANOS_PER_MILLISECOND); + } else if (nanos % NANOS_PER_MICROSECOND == 0) { + return String.format(Locale.ENGLISH, "%1$06d", nanos / NANOS_PER_MICROSECOND); + } else { + return String.format(Locale.ENGLISH, "%1$09d", nanos); + } + } + + private ProtoTimeUtils() {} +} diff --git a/common/src/main/java/dev/cel/common/internal/ReflectionUtil.java b/common/src/main/java/dev/cel/common/internal/ReflectionUtil.java new file mode 100644 index 000000000..97bed650f --- /dev/null +++ b/common/src/main/java/dev/cel/common/internal/ReflectionUtil.java @@ -0,0 +1,67 @@ +// 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.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. + * + *

CEL Library Internals. Do Not Use. + */ +@Internal +public final class ReflectionUtil { + + public static Method getMethod(Class clazz, String methodName, Class... params) { + try { + return clazz.getMethod(methodName, params); + } catch (NoSuchMethodException e) { + throw new LinkageError( + String.format("method [%s] does not exist in class: [%s].", methodName, clazz.getName()), + e); + } + } + + public static Object invoke(Method method, Object object, Object... params) { + try { + return method.invoke(object, params); + } catch (IllegalArgumentException | InvocationTargetException | IllegalAccessException e) { + throw new LinkageError( + String.format( + "method [%s] invocation failed on class [%s].", + method.getName(), method.getDeclaringClass()), + e); + } + } + + /** 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 8090a591d..0c9214410 100644 --- a/common/src/main/java/dev/cel/common/internal/SupplementalCodePointArray.java +++ b/common/src/main/java/dev/cel/common/internal/SupplementalCodePointArray.java @@ -18,7 +18,8 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkPositionIndexes; -import com.google.common.annotations.VisibleForTesting; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.Immutable; import dev.cel.common.annotations.Internal; @@ -29,45 +30,42 @@ *

CEL Library Internals. Do Not Use. */ @Immutable -@VisibleForTesting @Internal -public final class SupplementalCodePointArray extends CelCodePointArray { +@AutoValue +@AutoValue.CopyAnnotations +@SuppressWarnings("Immutable") // int[] is not exposed externally, thus cannot be mutated. +public abstract class SupplementalCodePointArray extends CelCodePointArray { - @SuppressWarnings("Immutable") - private final int[] codePoints; + @SuppressWarnings("mutable") + abstract int[] codePoints(); - private final int offset; - private final int size; + abstract int offset(); - SupplementalCodePointArray(int[] codePoints, int size) { - this(codePoints, 0, size); + static SupplementalCodePointArray create( + int[] codePoints, int size, ImmutableList lineOffsets) { + return create(codePoints, 0, lineOffsets, size); } - SupplementalCodePointArray(int[] codePoints, int offset, int size) { - this.codePoints = checkNotNull(codePoints); - this.offset = offset; - this.size = size; + static SupplementalCodePointArray create( + int[] codePoints, int offset, ImmutableList lineOffsets, int size) { + return new AutoValue_SupplementalCodePointArray( + size, checkNotNull(lineOffsets), codePoints, offset); } @Override public SupplementalCodePointArray slice(int i, int j) { checkPositionIndexes(i, j, size()); - return new SupplementalCodePointArray(codePoints, offset + i, j - i); + return create(codePoints(), offset() + i, lineOffsets(), j - i); } @Override public int get(int index) { checkElementIndex(index, size()); - return codePoints[offset + index]; + return codePoints()[offset() + index]; } @Override - public int size() { - return size; - } - - @Override - public String toString() { - return new String(codePoints, offset, size); + public final String toString() { + return new String(codePoints(), offset(), size()); } } diff --git a/common/src/main/java/dev/cel/common/internal/WellKnownProto.java b/common/src/main/java/dev/cel/common/internal/WellKnownProto.java index 14da4396d..21ee8053e 100644 --- a/common/src/main/java/dev/cel/common/internal/WellKnownProto.java +++ b/common/src/main/java/dev/cel/common/internal/WellKnownProto.java @@ -17,13 +17,16 @@ import static com.google.common.collect.ImmutableMap.toImmutableMap; import static java.util.Arrays.stream; +import com.google.common.collect.ImmutableCollection; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableMultimap; import com.google.protobuf.Any; import com.google.protobuf.BoolValue; import com.google.protobuf.BytesValue; -import com.google.protobuf.Descriptors.Descriptor; import com.google.protobuf.DoubleValue; import com.google.protobuf.Duration; +import com.google.protobuf.Empty; +import com.google.protobuf.FieldMask; import com.google.protobuf.FloatValue; import com.google.protobuf.Int32Value; import com.google.protobuf.Int64Value; @@ -35,6 +38,7 @@ import com.google.protobuf.UInt64Value; import com.google.protobuf.Value; import dev.cel.common.annotations.Internal; +import java.util.Optional; import java.util.function.Function; /** @@ -44,55 +48,149 @@ */ @Internal public enum WellKnownProto { - JSON_VALUE(Value.getDescriptor()), - JSON_STRUCT_VALUE(Struct.getDescriptor()), - JSON_LIST_VALUE(ListValue.getDescriptor()), - ANY_VALUE(Any.getDescriptor()), - BOOL_VALUE(BoolValue.getDescriptor(), true), - BYTES_VALUE(BytesValue.getDescriptor(), true), - DOUBLE_VALUE(DoubleValue.getDescriptor(), true), - FLOAT_VALUE(FloatValue.getDescriptor(), true), - INT32_VALUE(Int32Value.getDescriptor(), true), - INT64_VALUE(Int64Value.getDescriptor(), true), - STRING_VALUE(StringValue.getDescriptor(), true), - UINT32_VALUE(UInt32Value.getDescriptor(), true), - UINT64_VALUE(UInt64Value.getDescriptor(), true), - DURATION_VALUE(Duration.getDescriptor()), - TIMESTAMP_VALUE(Timestamp.getDescriptor()); - - private final Descriptor descriptor; + ANY_VALUE("google.protobuf.Any", "google/protobuf/any.proto", Any.class), + DURATION("google.protobuf.Duration", "google/protobuf/duration.proto", Duration.class), + JSON_LIST_VALUE("google.protobuf.ListValue", "google/protobuf/struct.proto", ListValue.class), + JSON_STRUCT_VALUE("google.protobuf.Struct", "google/protobuf/struct.proto", Struct.class), + JSON_VALUE("google.protobuf.Value", "google/protobuf/struct.proto", Value.class), + TIMESTAMP("google.protobuf.Timestamp", "google/protobuf/timestamp.proto", Timestamp.class), + // Wrapper types + FLOAT_VALUE( + "google.protobuf.FloatValue", + "google/protobuf/wrappers.proto", + FloatValue.class, + /* isWrapperType= */ true), + INT32_VALUE( + "google.protobuf.Int32Value", + "google/protobuf/wrappers.proto", + Int32Value.class, + /* isWrapperType= */ true), + INT64_VALUE( + "google.protobuf.Int64Value", + "google/protobuf/wrappers.proto", + Int64Value.class, + /* isWrapperType= */ true), + STRING_VALUE( + "google.protobuf.StringValue", + "google/protobuf/wrappers.proto", + StringValue.class, + /* isWrapperType= */ true), + BOOL_VALUE( + "google.protobuf.BoolValue", + "google/protobuf/wrappers.proto", + BoolValue.class, + /* isWrapperType= */ true), + BYTES_VALUE( + "google.protobuf.BytesValue", + "google/protobuf/wrappers.proto", + BytesValue.class, + /* isWrapperType= */ true), + DOUBLE_VALUE( + "google.protobuf.DoubleValue", + "google/protobuf/wrappers.proto", + DoubleValue.class, + /* isWrapperType= */ true), + UINT32_VALUE( + "google.protobuf.UInt32Value", + "google/protobuf/wrappers.proto", + UInt32Value.class, + /* isWrapperType= */ true), + UINT64_VALUE( + "google.protobuf.UInt64Value", + "google/protobuf/wrappers.proto", + UInt64Value.class, + /* isWrapperType= */ true), + // These aren't explicitly called out as wrapper types in the spec, but behave like one, because + // they are still converted into an equivalent primitive type. + + EMPTY( + "google.protobuf.Empty", + "google/protobuf/empty.proto", + Empty.class, + /* isWrapperType= */ true), + FIELD_MASK( + "google.protobuf.FieldMask", + "google/protobuf/field_mask.proto", + FieldMask.class, + /* isWrapperType= */ true); + + private static final ImmutableMap TYPE_NAME_TO_WELL_KNOWN_PROTO_MAP = + stream(WellKnownProto.values()) + .collect(toImmutableMap(WellKnownProto::typeName, Function.identity())); + + private static final ImmutableMap, WellKnownProto> + CLASS_TO_NAME_TO_WELL_KNOWN_PROTO_MAP = + stream(WellKnownProto.values()) + .collect(toImmutableMap(WellKnownProto::messageClass, Function.identity())); + + private static final ImmutableMultimap PATH_NAME_TO_WELL_KNOWN_PROTO_MAP = + initPathNameMap(); + + private static ImmutableMultimap initPathNameMap() { + ImmutableMultimap.Builder builder = ImmutableMultimap.builder(); + for (WellKnownProto proto : values()) { + builder.put(proto.pathName(), proto); + } + return builder.build(); + } + + private final String wellKnownProtoTypeName; + private final String pathName; + private final Class clazz; private final boolean isWrapperType; - private static final ImmutableMap WELL_KNOWN_PROTO_MAP; + /** Gets the full proto path name (ex: google/protobuf/any.proto) */ + public String pathName() { + return pathName; + } + + /** Gets the fully qualified prototype name (ex: google.protobuf.FloatValue) */ + public String typeName() { + return wellKnownProtoTypeName; + } - static { - WELL_KNOWN_PROTO_MAP = - stream(WellKnownProto.values()) - .collect(toImmutableMap(WellKnownProto::typeName, Function.identity())); + /** Gets the underlying java class for this WellKnownProto. */ + public Class messageClass() { + return clazz; } - WellKnownProto(Descriptor descriptor) { - this(descriptor, /* isWrapperType= */ false); + /** + * Returns the well known proto given the full proto path (example: + * google/protobuf/timestamp.proto) + */ + public static ImmutableCollection getByPathName(String typeName) { + return PATH_NAME_TO_WELL_KNOWN_PROTO_MAP.get(typeName); } - WellKnownProto(Descriptor descriptor, boolean isWrapperType) { - this.descriptor = descriptor; - this.isWrapperType = isWrapperType; + public static Optional getByTypeName(String typeName) { + return Optional.ofNullable(TYPE_NAME_TO_WELL_KNOWN_PROTO_MAP.get(typeName)); } - public Descriptor descriptor() { - return descriptor; + public static Optional getByClass(Class clazz) { + return Optional.ofNullable(CLASS_TO_NAME_TO_WELL_KNOWN_PROTO_MAP.get(clazz)); } - public String typeName() { - return descriptor.getFullName(); + /** + * Returns true if the provided {@code typeName} is a well known type, and it's a wrapper. False + * otherwise. + */ + public static boolean isWrapperType(String typeName) { + return getByTypeName(typeName).map(WellKnownProto::isWrapperType).orElse(false); } public boolean isWrapperType() { return isWrapperType; } - public static WellKnownProto getByDescriptorName(String name) { - return WELL_KNOWN_PROTO_MAP.get(name); + WellKnownProto(String wellKnownProtoTypeName, String pathName, Class clazz) { + this(wellKnownProtoTypeName, pathName, clazz, /* isWrapperType= */ false); + } + + WellKnownProto( + String wellKnownProtoFullName, String pathName, Class clazz, boolean isWrapperType) { + this.wellKnownProtoTypeName = wellKnownProtoFullName; + this.pathName = pathName; + this.clazz = clazz; + this.isWrapperType = isWrapperType; } } 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 5a3281e75..b43f3c289 100644 --- a/common/src/main/java/dev/cel/common/navigation/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/navigation/BUILD.bazel @@ -1,3 +1,5 @@ +load("@rules_java//java:defs.bzl", "java_library") + package( default_applicable_licenses = [ "//:license", @@ -7,21 +9,60 @@ package( ], ) +java_library( + name = "common", + srcs = [ + "BaseNavigableExpr.java", + "CelNavigableExprVisitor.java", + "ExprPropertyCalculator.java", + "TraversalOrder.java", + ], + tags = [ + ], + deps = [ + "//:auto_value", + "//common/ast", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven//:org_jspecify_jspecify", + ], +) + java_library( name = "navigation", srcs = [ "CelNavigableAst.java", "CelNavigableExpr.java", - "CelNavigableExprVisitor.java", - "ExprHeightCalculator.java", ], tags = [ ], deps = [ + ":common", "//:auto_value", - "//common", + "//common:cel_ast", "//common/ast", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", + "@maven//:org_jspecify_jspecify", + ], +) + +java_library( + name = "mutable_navigation", + srcs = [ + "CelNavigableMutableAst.java", + "CelNavigableMutableExpr.java", + ], + tags = [ + ], + deps = [ + ":common", + "//:auto_value", + "//common:mutable_ast", + "//common/ast:mutable_expr", + "//common/types:type_providers", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven//:org_jspecify_jspecify", ], ) diff --git a/common/src/main/java/dev/cel/common/navigation/BaseNavigableExpr.java b/common/src/main/java/dev/cel/common/navigation/BaseNavigableExpr.java new file mode 100644 index 000000000..1699b4a96 --- /dev/null +++ b/common/src/main/java/dev/cel/common/navigation/BaseNavigableExpr.java @@ -0,0 +1,140 @@ +// 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. + +package dev.cel.common.navigation; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import com.google.errorprone.annotations.CheckReturnValue; +import dev.cel.common.ast.CelExpr; +import dev.cel.common.ast.CelExpr.ExprKind; +import dev.cel.common.ast.Expression; +import java.util.Optional; +import java.util.stream.Stream; + +/** + * BaseNavigableExpr represents the base navigable expression value with methods to inspect the + * parent and child expressions. + */ +@SuppressWarnings("unchecked") // Generic types are properly bound to Expression +abstract class BaseNavigableExpr { + + public abstract E expr(); + + public long id() { + return expr().id(); + } + + public abstract > Optional parent(); + + /** Represents the count of transitive parents. Depth of an AST's root is 0. */ + public abstract int depth(); + + /** + * Represents the maximum ID of the tree. Note that if the underlying expression tree held by this + * navigable expression is mutated, its max ID becomes stale and must be recomputed. + */ + public abstract long maxId(); + + /** + * Represents the maximum count of children from any of its branches. Height of a leaf node is 0. + * For example, the height of the call node 'func' in expression `(1 + 2 + 3).func(4 + 5)` is 3. + */ + public abstract int height(); + + /** + * Returns a stream of {@link BaseNavigableExpr} collected from the current node down to the last + * leaf-level member using post-order traversal. + */ + public > Stream allNodes() { + return allNodes(TraversalOrder.POST_ORDER); + } + + /** + * Returns a stream of {@link BaseNavigableExpr} collected from the current node down to the last + * leaf-level member using the specified traversal order. + */ + public > Stream allNodes(TraversalOrder traversalOrder) { + return CelNavigableExprVisitor.collect((T) this, traversalOrder); + } + + /** + * Returns a stream of {@link BaseNavigableExpr} collected down to the last leaf-level member + * using post-order traversal. + */ + public > Stream descendants() { + return descendants(TraversalOrder.POST_ORDER); + } + + /** + * Returns a stream of {@link BaseNavigableExpr} collected down to the last leaf-level member + * using the specified traversal order. + */ + public > Stream descendants(TraversalOrder traversalOrder) { + return CelNavigableExprVisitor.collect((T) this, traversalOrder) + .filter(node -> node.depth() > this.depth()); + } + + /** + * Returns a stream of {@link BaseNavigableExpr} collected from its immediate children using + * post-order traversal. + */ + public > Stream children() { + return children(TraversalOrder.POST_ORDER); + } + + /** + * Returns a stream of {@link BaseNavigableExpr} collected from its immediate children using the + * specified traversal order. + */ + public > Stream children(TraversalOrder traversalOrder) { + return CelNavigableExprVisitor.collect((T) this, this.depth() + 1, traversalOrder) + .filter(node -> node.depth() > this.depth()); + } + + /** Returns the underlying kind of the {@link CelExpr}. */ + public ExprKind.Kind getKind() { + return expr().getKind(); + } + + public abstract > Builder builderFromInstance(); + + interface Builder> { + + E expr(); + + int depth(); + + default ExprKind.Kind getKind() { + return expr().getKind(); + } + + @CanIgnoreReturnValue + Builder setExpr(E value); + + @CanIgnoreReturnValue + Builder setParent(T value); + + @CanIgnoreReturnValue + Builder setDepth(int value); + + @CanIgnoreReturnValue + Builder setHeight(int value); + + @CanIgnoreReturnValue + Builder setMaxId(long value); + + @CheckReturnValue + T build(); + } +} diff --git a/common/src/main/java/dev/cel/common/navigation/CelNavigableExpr.java b/common/src/main/java/dev/cel/common/navigation/CelNavigableExpr.java index d0cd2ba74..69453b900 100644 --- a/common/src/main/java/dev/cel/common/navigation/CelNavigableExpr.java +++ b/common/src/main/java/dev/cel/common/navigation/CelNavigableExpr.java @@ -16,146 +16,94 @@ import com.google.auto.value.AutoValue; import dev.cel.common.ast.CelExpr; -import dev.cel.common.ast.CelExpr.CelComprehension; -import dev.cel.common.ast.CelExpr.ExprKind; +import dev.cel.common.navigation.ExprPropertyCalculator.ExprProperty; import java.util.Optional; import java.util.stream.Stream; /** - * CelNavigableExpr represents the base navigable expression value with methods to inspect the - * parent and child expressions. + * CelNavigableExpr decorates {@link CelExpr} with capabilities to inspect the parent and its + * descendants with ease. */ @AutoValue -public abstract class CelNavigableExpr { - - /** - * Specifies the traversal order of AST navigation. - * - *

For call expressions, the target is visited before its arguments. - * - *

For comprehensions, the visiting order is as follows: - * - *

    - *
  1. {@link CelComprehension#iterRange} - *
  2. {@link CelComprehension#accuInit} - *
  3. {@link CelComprehension#loopCondition} - *
  4. {@link CelComprehension#loopStep} - *
  5. {@link CelComprehension#result} - *
- */ - public enum TraversalOrder { - PRE_ORDER, - POST_ORDER - } - - public abstract CelExpr expr(); - - public long id() { - return expr().id(); - } - - public abstract Optional parent(); - - /** Represents the count of transitive parents. Depth of an AST's root is 0. */ - public abstract int depth(); - - /** - * Represents the maximum count of children from any of its branches. Height of a leaf node is 0. - * For example, the height of the call node 'func' in expression `(1 + 2 + 3).func(4 + 5)` is 3. - */ - public abstract int height(); +// unchecked: Generic types are properly bound to BaseNavigableExpr +// redundant override: Overriding is required to specify the return type to a concrete type. +@SuppressWarnings({"unchecked", "RedundantOverride"}) +public abstract class CelNavigableExpr extends BaseNavigableExpr { /** Constructs a new instance of {@link CelNavigableExpr} from {@link CelExpr}. */ public static CelNavigableExpr fromExpr(CelExpr expr) { - ExprHeightCalculator exprHeightCalculator = new ExprHeightCalculator(expr); + ExprPropertyCalculator exprHeightCalculator = new ExprPropertyCalculator<>(expr); + ExprProperty exprProperty = exprHeightCalculator.getProperty(expr.id()); - return CelNavigableExpr.builder() + return builder() .setExpr(expr) - .setHeight(exprHeightCalculator.getHeight(expr.id())) + .setHeight(exprProperty.height()) + .setMaxId(exprProperty.maxId()) .build(); } - /** - * Returns a stream of {@link CelNavigableExpr} collected from the current node down to the last - * leaf-level member using post-order traversal. - */ + @Override public Stream allNodes() { - return allNodes(TraversalOrder.POST_ORDER); + return super.allNodes(); } - /** - * Returns a stream of {@link CelNavigableExpr} collected from the current node down to the last - * leaf-level member using the specified traversal order. - */ + @Override public Stream allNodes(TraversalOrder traversalOrder) { - return CelNavigableExprVisitor.collect(this, traversalOrder); + return super.allNodes(traversalOrder); } - /** - * Returns a stream of {@link CelNavigableExpr} collected down to the last leaf-level member using - * post-order traversal. - */ + @Override + public abstract Optional parent(); + + @Override public Stream descendants() { - return descendants(TraversalOrder.POST_ORDER); + return super.descendants(); } - /** - * Returns a stream of {@link CelNavigableExpr} collected down to the last leaf-level member using - * the specified traversal order. - */ + @Override public Stream descendants(TraversalOrder traversalOrder) { - return CelNavigableExprVisitor.collect(this, traversalOrder) - .filter(node -> node.depth() > this.depth()); + return super.descendants(traversalOrder); } - /** - * Returns a stream of {@link CelNavigableExpr} collected from its immediate children using - * post-order traversal. - */ + @Override public Stream children() { - return children(TraversalOrder.POST_ORDER); + return super.children(); } - /** - * Returns a stream of {@link CelNavigableExpr} collected from its immediate children using the - * specified traversal order. - */ + @Override public Stream children(TraversalOrder traversalOrder) { - return CelNavigableExprVisitor.collect(this, this.depth() + 1, traversalOrder) - .filter(node -> node.depth() > this.depth()); + return super.children(traversalOrder); } - /** Returns the underlying kind of the {@link CelExpr}. */ - public ExprKind.Kind getKind() { - return expr().exprKind().getKind(); + @Override + public Builder builderFromInstance() { + return builder(); } /** Create a new builder to construct a {@link CelNavigableExpr} instance. */ public static Builder builder() { - return new AutoValue_CelNavigableExpr.Builder().setDepth(0).setHeight(0); + return new AutoValue_CelNavigableExpr.Builder().setDepth(0).setHeight(0).setMaxId(0); } /** Builder to configure {@link CelNavigableExpr}. */ @AutoValue.Builder - public abstract static class Builder { - - public abstract CelExpr expr(); - - public abstract int depth(); + public abstract static class Builder + implements BaseNavigableExpr.Builder { - public ExprKind.Kind getKind() { - return expr().exprKind().getKind(); - } + @Override + public abstract Builder setParent(CelNavigableExpr value); + @Override public abstract Builder setExpr(CelExpr value); - abstract Builder setParent(CelNavigableExpr value); - + @Override public abstract Builder setDepth(int value); - public abstract Builder setHeight(int value); + @Override + public abstract Builder setMaxId(long value); - public abstract CelNavigableExpr build(); + @Override + public abstract Builder setHeight(int value); } public abstract Builder toBuilder(); diff --git a/common/src/main/java/dev/cel/common/navigation/CelNavigableExprVisitor.java b/common/src/main/java/dev/cel/common/navigation/CelNavigableExprVisitor.java index afafd89c2..c8eb57d53 100644 --- a/common/src/main/java/dev/cel/common/navigation/CelNavigableExprVisitor.java +++ b/common/src/main/java/dev/cel/common/navigation/CelNavigableExprVisitor.java @@ -14,30 +14,26 @@ package dev.cel.common.navigation; -import com.google.common.collect.ImmutableList; -import dev.cel.common.ast.CelExpr; -import dev.cel.common.ast.CelExpr.CelCall; -import dev.cel.common.ast.CelExpr.CelComprehension; -import dev.cel.common.ast.CelExpr.CelCreateList; -import dev.cel.common.ast.CelExpr.CelCreateMap; -import dev.cel.common.ast.CelExpr.CelCreateStruct; -import dev.cel.common.ast.CelExpr.CelSelect; -import dev.cel.common.navigation.CelNavigableExpr.TraversalOrder; +import dev.cel.common.ast.Expression; +import dev.cel.common.ast.Expression.List; +import dev.cel.common.navigation.ExprPropertyCalculator.ExprProperty; import java.util.stream.Stream; /** Visitor implementation to navigate an AST. */ -final class CelNavigableExprVisitor { +final class CelNavigableExprVisitor> { private static final int MAX_DESCENDANTS_RECURSION_DEPTH = 500; - private final Stream.Builder streamBuilder; - private final ExprHeightCalculator exprHeightCalculator; + private final Stream.Builder streamBuilder; + private final ExprPropertyCalculator exprPropertyCalculator; private final TraversalOrder traversalOrder; private final int maxDepth; private CelNavigableExprVisitor( - int maxDepth, ExprHeightCalculator exprHeightCalculator, TraversalOrder traversalOrder) { + int maxDepth, + ExprPropertyCalculator exprPropertyCalculator, + TraversalOrder traversalOrder) { this.maxDepth = maxDepth; - this.exprHeightCalculator = exprHeightCalculator; + this.exprPropertyCalculator = exprPropertyCalculator; this.traversalOrder = traversalOrder; this.streamBuilder = Stream.builder(); } @@ -60,8 +56,8 @@ private CelNavigableExprVisitor( * maxDepth of 2: a, b, d, e, c * */ - static Stream collect( - CelNavigableExpr navigableExpr, TraversalOrder traversalOrder) { + static > Stream collect( + T navigableExpr, TraversalOrder traversalOrder) { return collect(navigableExpr, MAX_DESCENDANTS_RECURSION_DEPTH, traversalOrder); } @@ -83,18 +79,19 @@ static Stream collect( * maxDepth of 2: a, b, d, e, c * */ - static Stream collect( - CelNavigableExpr navigableExpr, int maxDepth, TraversalOrder traversalOrder) { - ExprHeightCalculator exprHeightCalculator = new ExprHeightCalculator(navigableExpr.expr()); - CelNavigableExprVisitor visitor = - new CelNavigableExprVisitor(maxDepth, exprHeightCalculator, traversalOrder); + static > Stream collect( + T navigableExpr, int maxDepth, TraversalOrder traversalOrder) { + ExprPropertyCalculator exprHeightCalculator = + new ExprPropertyCalculator<>(navigableExpr.expr()); + CelNavigableExprVisitor visitor = + new CelNavigableExprVisitor<>(maxDepth, exprHeightCalculator, traversalOrder); visitor.visit(navigableExpr); return visitor.streamBuilder.build(); } - private void visit(CelNavigableExpr navigableExpr) { + private void visit(T navigableExpr) { if (navigableExpr.depth() > MAX_DESCENDANTS_RECURSION_DEPTH - 1) { throw new IllegalStateException("Max recursion depth reached."); } @@ -108,17 +105,17 @@ private void visit(CelNavigableExpr navigableExpr) { case CALL: visit(navigableExpr, navigableExpr.expr().call()); break; - case CREATE_LIST: - visit(navigableExpr, navigableExpr.expr().createList()); + case LIST: + visit(navigableExpr, navigableExpr.expr().list()); break; case SELECT: visit(navigableExpr, navigableExpr.expr().select()); break; - case CREATE_STRUCT: - visitStruct(navigableExpr, navigableExpr.expr().createStruct()); + case STRUCT: + visitStruct(navigableExpr, navigableExpr.expr().struct()); break; - case CREATE_MAP: - visitMap(navigableExpr, navigableExpr.expr().createMap()); + case MAP: + visitMap(navigableExpr, navigableExpr.expr().map()); break; case COMPREHENSION: visit(navigableExpr, navigableExpr.expr().comprehension()); @@ -132,7 +129,7 @@ private void visit(CelNavigableExpr navigableExpr) { } } - private void visit(CelNavigableExpr navigableExpr, CelCall call) { + private void visit(T navigableExpr, Expression.Call call) { if (call.target().isPresent()) { visit(newNavigableChild(navigableExpr, call.target().get())); } @@ -140,16 +137,16 @@ private void visit(CelNavigableExpr navigableExpr, CelCall call) { visitExprList(call.args(), navigableExpr); } - private void visit(CelNavigableExpr navigableExpr, CelCreateList createList) { - visitExprList(createList.elements(), navigableExpr); + private void visit(T navigableExpr, List list) { + visitExprList(list.elements(), navigableExpr); } - private void visit(CelNavigableExpr navigableExpr, CelSelect selectExpr) { - CelNavigableExpr operand = newNavigableChild(navigableExpr, selectExpr.operand()); + private void visit(T navigableExpr, Expression.Select selectExpr) { + T operand = newNavigableChild(navigableExpr, selectExpr.operand()); visit(operand); } - private void visit(CelNavigableExpr navigableExpr, CelComprehension comprehension) { + private void visit(T navigableExpr, Expression.Comprehension comprehension) { visit(newNavigableChild(navigableExpr, comprehension.iterRange())); visit(newNavigableChild(navigableExpr, comprehension.accuInit())); visit(newNavigableChild(navigableExpr, comprehension.loopCondition())); @@ -157,36 +154,38 @@ private void visit(CelNavigableExpr navigableExpr, CelComprehension comprehensio visit(newNavigableChild(navigableExpr, comprehension.result())); } - private void visitStruct(CelNavigableExpr navigableExpr, CelCreateStruct struct) { - for (CelCreateStruct.Entry entry : struct.entries()) { + private void visitStruct(T navigableExpr, Expression.Struct> struct) { + for (Expression.Struct.Entry entry : struct.entries()) { visit(newNavigableChild(navigableExpr, entry.value())); } } - private void visitMap(CelNavigableExpr navigableExpr, CelCreateMap map) { - for (CelCreateMap.Entry entry : map.entries()) { - CelNavigableExpr key = newNavigableChild(navigableExpr, entry.key()); + private void visitMap(T navigableExpr, Expression.Map> map) { + for (Expression.Map.Entry entry : map.entries()) { + T key = newNavigableChild(navigableExpr, entry.key()); visit(key); - CelNavigableExpr value = newNavigableChild(navigableExpr, entry.value()); + T value = newNavigableChild(navigableExpr, entry.value()); visit(value); } } - private void visitExprList(ImmutableList createListExpr, CelNavigableExpr parent) { - for (CelExpr expr : createListExpr) { + private void visitExprList(java.util.List list, T parent) { + for (E expr : list) { visit(newNavigableChild(parent, expr)); } } - private CelNavigableExpr newNavigableChild(CelNavigableExpr parent, CelExpr expr) { - CelNavigableExpr.Builder navigableExpr = - CelNavigableExpr.builder() - .setExpr(expr) - .setDepth(parent.depth() + 1) - .setHeight(exprHeightCalculator.getHeight(expr.id())) - .setParent(parent); - - return navigableExpr.build(); + private T newNavigableChild(T parent, E expr) { + ExprProperty exprProperty = exprPropertyCalculator.getProperty(expr.id()); + + return parent + .builderFromInstance() + .setExpr(expr) + .setDepth(parent.depth() + 1) + .setHeight(exprProperty.height()) + .setMaxId(exprProperty.maxId()) + .setParent(parent) + .build(); } } diff --git a/common/src/main/java/dev/cel/common/navigation/CelNavigableMutableAst.java b/common/src/main/java/dev/cel/common/navigation/CelNavigableMutableAst.java new file mode 100644 index 000000000..70d33ab41 --- /dev/null +++ b/common/src/main/java/dev/cel/common/navigation/CelNavigableMutableAst.java @@ -0,0 +1,60 @@ +// 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. + +package dev.cel.common.navigation; + +import dev.cel.common.CelMutableAst; +import dev.cel.common.types.CelType; +import java.util.Optional; + +/** + * Decorates a {@link CelMutableAst} with navigational properties. This allows us to visit a node's + * children, descendants or its parent with ease. + */ +public final class CelNavigableMutableAst { + + private final CelMutableAst ast; + private final CelNavigableMutableExpr root; + + private CelNavigableMutableAst(CelMutableAst mutableAst) { + this.ast = mutableAst; + this.root = CelNavigableMutableExpr.fromExpr(mutableAst.expr()); + } + + /** Constructs a new instance of {@link CelNavigableMutableAst} from {@link CelMutableAst}. */ + public static CelNavigableMutableAst fromAst(CelMutableAst ast) { + return new CelNavigableMutableAst(ast); + } + + /** Returns the root of the AST. */ + public CelNavigableMutableExpr getRoot() { + return root; + } + + /** Returns the underlying {@link CelMutableAst}. */ + public CelMutableAst getAst() { + return ast; + } + + /** + * Returns the type of the expression node for a type-checked AST. This simply proxies down the + * call to {@link CelMutableAst#getType(long)}. + * + * @return Optional of {@link CelType} or {@link Optional#empty} if the type does not exist at the + * ID. + */ + public Optional getType(long exprId) { + return ast.getType(exprId); + } +} diff --git a/common/src/main/java/dev/cel/common/navigation/CelNavigableMutableExpr.java b/common/src/main/java/dev/cel/common/navigation/CelNavigableMutableExpr.java new file mode 100644 index 000000000..b9af67675 --- /dev/null +++ b/common/src/main/java/dev/cel/common/navigation/CelNavigableMutableExpr.java @@ -0,0 +1,109 @@ +// 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. + +package dev.cel.common.navigation; + +import com.google.auto.value.AutoValue; +import dev.cel.common.ast.CelMutableExpr; +import dev.cel.common.navigation.ExprPropertyCalculator.ExprProperty; +import java.util.Optional; +import java.util.stream.Stream; + +/** + * CelNavigableMutableExpr decorates {@link CelMutableExpr} with capabilities to inspect the parent + * and its descendants with ease. + */ +@AutoValue +// unchecked: Generic types are properly bound to BaseNavigableExpr +// redundant override: Overriding is required to specify the return type to a concrete type. +@SuppressWarnings({"unchecked", "RedundantOverride"}) +public abstract class CelNavigableMutableExpr extends BaseNavigableExpr { + + /** Constructs a new instance of {@link CelNavigableMutableExpr} from {@link CelMutableExpr}. */ + public static CelNavigableMutableExpr fromExpr(CelMutableExpr expr) { + ExprPropertyCalculator exprHeightCalculator = + new ExprPropertyCalculator<>(expr); + ExprProperty exprProperty = exprHeightCalculator.getProperty(expr.id()); + + return builder() + .setExpr(expr) + .setHeight(exprProperty.height()) + .setMaxId(exprProperty.maxId()) + .build(); + } + + @Override + public Stream allNodes() { + return super.allNodes(); + } + + @Override + public Stream allNodes(TraversalOrder traversalOrder) { + return super.allNodes(traversalOrder); + } + + @Override + public abstract Optional parent(); + + @Override + public Stream descendants() { + return super.descendants(); + } + + @Override + public Stream descendants(TraversalOrder traversalOrder) { + return super.descendants(traversalOrder); + } + + @Override + public Stream children() { + return super.children(); + } + + @Override + public Stream children(TraversalOrder traversalOrder) { + return super.children(traversalOrder); + } + + @Override + public Builder builderFromInstance() { + return builder(); + } + + /** Create a new builder to construct a {@link CelNavigableExpr} instance. */ + public static CelNavigableMutableExpr.Builder builder() { + return new AutoValue_CelNavigableMutableExpr.Builder().setDepth(0).setHeight(0).setMaxId(0); + } + + /** Builder to configure {@link CelNavigableExpr}. */ + @AutoValue.Builder + public abstract static class Builder + implements BaseNavigableExpr.Builder { + + @Override + public abstract Builder setParent(CelNavigableMutableExpr value); + + @Override + public abstract Builder setExpr(CelMutableExpr value); + + @Override + public abstract Builder setDepth(int value); + + @Override + public abstract Builder setMaxId(long value); + + @Override + public abstract Builder setHeight(int value); + } +} diff --git a/common/src/main/java/dev/cel/common/navigation/ExprHeightCalculator.java b/common/src/main/java/dev/cel/common/navigation/ExprHeightCalculator.java deleted file mode 100644 index f6555a09b..000000000 --- a/common/src/main/java/dev/cel/common/navigation/ExprHeightCalculator.java +++ /dev/null @@ -1,132 +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. - -package dev.cel.common.navigation; - -import static java.lang.Math.max; - -import com.google.common.collect.ImmutableList; -import dev.cel.common.ast.CelExpr; -import dev.cel.common.ast.CelExpr.CelCall; -import dev.cel.common.ast.CelExpr.CelComprehension; -import dev.cel.common.ast.CelExpr.CelCreateList; -import dev.cel.common.ast.CelExpr.CelCreateMap; -import dev.cel.common.ast.CelExpr.CelCreateStruct; -import dev.cel.common.ast.CelExpr.CelSelect; -import java.util.HashMap; - -/** Package-private class to assist computing the height of expression nodes. */ -final class ExprHeightCalculator { - // Store hashmap instead of immutable map for performance, such that this helper class can be - // instantiated faster. - private final HashMap idToHeight; - - ExprHeightCalculator(CelExpr celExpr) { - this.idToHeight = new HashMap<>(); - visit(celExpr); - } - - int getHeight(Long exprId) { - if (!idToHeight.containsKey(exprId)) { - throw new IllegalStateException("Height not found for expression id: " + exprId); - } - - return idToHeight.get(exprId); - } - - private int visit(CelExpr celExpr) { - int height = 1; - switch (celExpr.exprKind().getKind()) { - case CALL: - height += visit(celExpr.call()); - break; - case CREATE_LIST: - height += visit(celExpr.createList()); - break; - case SELECT: - height += visit(celExpr.select()); - break; - case CREATE_STRUCT: - height += visitStruct(celExpr.createStruct()); - break; - case CREATE_MAP: - height += visitMap(celExpr.createMap()); - break; - case COMPREHENSION: - height += visit(celExpr.comprehension()); - break; - default: - // This is a leaf node - height = 0; - break; - } - - idToHeight.put(celExpr.id(), height); - return height; - } - - private int visit(CelCall call) { - int targetHeight = 0; - if (call.target().isPresent()) { - targetHeight = visit(call.target().get()); - } - - int argumentHeight = visitExprList(call.args()); - return max(targetHeight, argumentHeight); - } - - private int visit(CelCreateList createList) { - return visitExprList(createList.elements()); - } - - private int visit(CelSelect selectExpr) { - return visit(selectExpr.operand()); - } - - private int visit(CelComprehension comprehension) { - int maxHeight = 0; - maxHeight = max(visit(comprehension.iterRange()), maxHeight); - maxHeight = max(visit(comprehension.accuInit()), maxHeight); - maxHeight = max(visit(comprehension.loopCondition()), maxHeight); - maxHeight = max(visit(comprehension.loopStep()), maxHeight); - maxHeight = max(visit(comprehension.result()), maxHeight); - - return maxHeight; - } - - private int visitStruct(CelCreateStruct struct) { - int maxHeight = 0; - for (CelCreateStruct.Entry entry : struct.entries()) { - maxHeight = max(visit(entry.value()), maxHeight); - } - return maxHeight; - } - - private int visitMap(CelCreateMap map) { - int maxHeight = 0; - for (CelCreateMap.Entry entry : map.entries()) { - maxHeight = max(visit(entry.key()), maxHeight); - maxHeight = max(visit(entry.value()), maxHeight); - } - return maxHeight; - } - - private int visitExprList(ImmutableList createListExpr) { - int maxHeight = 0; - for (CelExpr expr : createListExpr) { - maxHeight = max(visit(expr), maxHeight); - } - return maxHeight; - } -} diff --git a/common/src/main/java/dev/cel/common/navigation/ExprPropertyCalculator.java b/common/src/main/java/dev/cel/common/navigation/ExprPropertyCalculator.java new file mode 100644 index 000000000..be2c07221 --- /dev/null +++ b/common/src/main/java/dev/cel/common/navigation/ExprPropertyCalculator.java @@ -0,0 +1,156 @@ +// 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. + +package dev.cel.common.navigation; + +import static java.lang.Math.max; + +import com.google.auto.value.AutoValue; +import dev.cel.common.ast.Expression; +import dev.cel.common.ast.Expression.List; +import dev.cel.common.ast.Expression.Map; +import dev.cel.common.ast.Expression.Struct; +import java.util.HashMap; + +/** Package-private class to assist computing the height and the max ID of expression nodes. */ +final class ExprPropertyCalculator { + // Store hashmap instead of immutable map for performance, such that this helper class can be + // instantiated faster. + private final HashMap idToProperty; + + ExprPropertyCalculator(E celExpr) { + this.idToProperty = new HashMap<>(); + visit(celExpr); + } + + /** + * Retrieves the property containing the expression's maximum ID and the height of the subtree. + * + * @throws IllegalArgumentException If the provided expression ID does not exist. + */ + ExprProperty getProperty(Long exprId) { + if (!idToProperty.containsKey(exprId)) { + throw new IllegalArgumentException("Property not found for expression id: " + exprId); + } + + return idToProperty.get(exprId); + } + + private ExprProperty visit(E expr) { + int baseHeight = 1; + ExprProperty visitedProperty; + switch (expr.getKind()) { + case CALL: + visitedProperty = visit(expr.call()); + break; + case LIST: + visitedProperty = visit(expr.list()); + break; + case SELECT: + visitedProperty = visit(expr.select()); + break; + case STRUCT: + visitedProperty = visitStruct(expr.struct()); + break; + case MAP: + visitedProperty = visitMap(expr.map()); + break; + case COMPREHENSION: + visitedProperty = visit(expr.comprehension()); + break; + default: + // This is a leaf node + baseHeight = 0; + visitedProperty = ExprProperty.create(baseHeight, expr.id()); + break; + } + + ExprProperty exprProperty = + ExprProperty.create( + baseHeight + visitedProperty.height(), max(visitedProperty.maxId(), expr.id())); + idToProperty.put(expr.id(), exprProperty); + + return exprProperty; + } + + private ExprProperty visit(Expression.Call call) { + ExprProperty visitedTarget = ExprProperty.create(0, 0); + if (call.target().isPresent()) { + visitedTarget = visit(call.target().get()); + } + + ExprProperty visitedArgument = visitExprList(call.args()); + return ExprProperty.merge(visitedArgument, visitedTarget); + } + + private ExprProperty visit(List list) { + return visitExprList(list.elements()); + } + + private ExprProperty visit(Expression.Select selectExpr) { + return visit(selectExpr.operand()); + } + + private ExprProperty visit(Expression.Comprehension comprehension) { + ExprProperty visitedProperty = visit(comprehension.iterRange()); + visitedProperty = ExprProperty.merge(visitedProperty, visit(comprehension.accuInit())); + visitedProperty = ExprProperty.merge(visitedProperty, visit(comprehension.loopCondition())); + visitedProperty = ExprProperty.merge(visitedProperty, visit(comprehension.loopStep())); + visitedProperty = ExprProperty.merge(visitedProperty, visit(comprehension.result())); + + return visitedProperty; + } + + private ExprProperty visitStruct(Expression.Struct> struct) { + ExprProperty visitedProperty = ExprProperty.create(0, 0); + for (Struct.Entry entry : struct.entries()) { + visitedProperty = ExprProperty.merge(visitedProperty, visit(entry.value())); + } + return visitedProperty; + } + + private ExprProperty visitMap(Expression.Map> map) { + ExprProperty visitedProperty = ExprProperty.create(0, 0); + for (Map.Entry entry : map.entries()) { + visitedProperty = ExprProperty.merge(visitedProperty, visit(entry.key())); + visitedProperty = ExprProperty.merge(visitedProperty, visit(entry.value())); + } + return visitedProperty; + } + + private ExprProperty visitExprList(java.util.List list) { + ExprProperty visitedProperty = ExprProperty.create(0, 0); + for (E expr : list) { + visitedProperty = ExprProperty.merge(visitedProperty, visit(expr)); + } + return visitedProperty; + } + + /** Value class to store the height and the max ID at a specific expression ID. */ + @AutoValue + abstract static class ExprProperty { + abstract int height(); + + abstract long maxId(); + + /** Merges the two {@link ExprProperty}, taking their maximum values from the properties. */ + private static ExprProperty merge(ExprProperty e1, ExprProperty e2) { + return create(max(e1.height(), e2.height()), max(e1.maxId(), e2.maxId())); + } + + private static ExprProperty create(int height, long maxId) { + return new AutoValue_ExprPropertyCalculator_ExprProperty(height, maxId); + } + } +} diff --git a/common/src/main/java/dev/cel/common/navigation/TraversalOrder.java b/common/src/main/java/dev/cel/common/navigation/TraversalOrder.java new file mode 100644 index 000000000..80aafbf74 --- /dev/null +++ b/common/src/main/java/dev/cel/common/navigation/TraversalOrder.java @@ -0,0 +1,37 @@ +// 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. + +package dev.cel.common.navigation; + +import dev.cel.common.ast.CelExpr.CelComprehension; + +/** + * Specifies the traversal order of AST navigation. + * + *

For call expressions, the target is visited before its arguments. + * + *

For comprehensions, the visiting order is as follows: + * + *

    + *
  1. {@link CelComprehension#iterRange} + *
  2. {@link CelComprehension#accuInit} + *
  3. {@link CelComprehension#loopCondition} + *
  4. {@link CelComprehension#loopStep} + *
  5. {@link CelComprehension#result} + *
+ */ +public enum TraversalOrder { + PRE_ORDER, + POST_ORDER +} diff --git a/common/src/main/java/dev/cel/common/testing/BUILD.bazel b/common/src/main/java/dev/cel/common/testing/BUILD.bazel index a5b967b16..574638e35 100644 --- a/common/src/main/java/dev/cel/common/testing/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/testing/BUILD.bazel @@ -1,3 +1,5 @@ +load("@rules_java//java:defs.bzl", "java_library") + package( default_applicable_licenses = [ "//:license", diff --git a/common/src/main/java/dev/cel/common/types/BUILD.bazel b/common/src/main/java/dev/cel/common/types/BUILD.bazel index 1a3f3ca0d..de65d0b1f 100644 --- a/common/src/main/java/dev/cel/common/types/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/types/BUILD.bazel @@ -1,9 +1,13 @@ +load("@rules_java//java:defs.bzl", "java_library") +load("//:cel_android_rules.bzl", "cel_android_library") + package( default_applicable_licenses = [ "//:license", ], default_visibility = [ "//common/types:__pkg__", + "//publish:__pkg__", ], ) @@ -69,12 +73,53 @@ java_library( tags = [ ], deps = [ - ":cel_internal_types", ":type_providers", ":types", "//common/annotations", - "@cel_spec//proto/cel/expr:expr_java_proto", "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "cel_proto_types", + srcs = ["CelProtoTypes.java"], + tags = [ + ], + deps = [ + ":cel_internal_types", + ":cel_types", + ":type_providers", + ":types", + "@cel_spec//proto/cel/expr:checked_java_proto", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + ], +) + +cel_android_library( + name = "cel_proto_types_android", + srcs = ["CelProtoTypes.java"], + tags = [ + ], + deps = [ + ":cel_internal_types_android", + ":cel_types_android", + ":type_providers_android", + ":types_android", + "@cel_spec//proto/cel/expr:checked_java_proto_lite", + "@maven_android//:com_google_guava_guava", + "@maven_android//:com_google_protobuf_protobuf_javalite", + ], +) + +java_library( + name = "cel_proto_message_types", + srcs = ["CelProtoMessageTypes.java"], + tags = [ + ], + deps = [ + ":cel_proto_types", + "@cel_spec//proto/cel/expr:checked_java_proto", "@maven//:com_google_protobuf_protobuf_java", ], ) @@ -130,10 +175,90 @@ java_library( ":type_providers", ":types", "//:auto_value", - "//common", + "//common:cel_descriptor_util", + "//common:cel_descriptors", "//common/internal:file_descriptor_converter", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", ], ) + +java_library( + name = "default_type_provider", + srcs = [ + "DefaultTypeProvider.java", + ], + tags = [ + ], + deps = [ + ":type_providers", + ":types", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "default_type_provider_android", + srcs = [ + "DefaultTypeProvider.java", + ], + tags = [ + ], + deps = [ + ":type_providers_android", + ":types_android", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "cel_types_android", + srcs = ["CelTypes.java"], + tags = [ + ], + deps = [ + "//common/annotations", + "//common/types:type_providers_android", + "//common/types:types_android", + "@maven_android//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "type_providers_android", + srcs = CEL_TYPE_PROVIDER_SOURCES, + tags = [ + ], + deps = [ + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "types_android", + srcs = CEL_TYPE_SOURCES, + tags = [ + ], + deps = [ + ":type_providers_android", + "//:auto_value", + "//common/annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "cel_internal_types_android", + srcs = CEL_INTERNAL_TYPE_SOURCES, + deps = [ + "//:auto_value", + "//common/annotations", + "//common/types:type_providers_android", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) diff --git a/common/src/main/java/dev/cel/common/types/CelKind.java b/common/src/main/java/dev/cel/common/types/CelKind.java index a97fe5b55..7d55ddaf1 100644 --- a/common/src/main/java/dev/cel/common/types/CelKind.java +++ b/common/src/main/java/dev/cel/common/types/CelKind.java @@ -32,7 +32,6 @@ public enum CelKind { BYTES, DOUBLE, DURATION, - FUNCTION, INT, LIST, MAP, diff --git a/testing/src/main/java/dev/cel/testing/TestDecl.java b/common/src/main/java/dev/cel/common/types/CelProtoMessageTypes.java similarity index 54% rename from testing/src/main/java/dev/cel/testing/TestDecl.java rename to common/src/main/java/dev/cel/common/types/CelProtoMessageTypes.java index f17b7d235..108305e13 100644 --- a/testing/src/main/java/dev/cel/testing/TestDecl.java +++ b/common/src/main/java/dev/cel/common/types/CelProtoMessageTypes.java @@ -1,4 +1,4 @@ -// Copyright 2022 Google LLC +// 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. @@ -12,21 +12,21 @@ // See the License for the specific language governing permissions and // limitations under the License. -package dev.cel.testing; +package dev.cel.common.types; -import dev.cel.expr.Decl; import dev.cel.expr.Type; -import dev.cel.common.types.CelType; -import dev.cel.compiler.CelCompilerBuilder; +import com.google.protobuf.Descriptors.Descriptor; /** - * Abstract declaration type that can work with Proto based types {@link Type} and CEL native types - * {@link CelType}. This abstraction is defined so that expression evaluations can be tested using - * both types. + * Utility class for working with {@link Type} that require a full protobuf dependency (i.e: + * descriptors). */ -abstract class TestDecl { +public final class CelProtoMessageTypes { - abstract void loadDeclsToCompiler(CelCompilerBuilder builder); + /** Create a message {@code Type} for {@code Descriptor}. */ + public static Type createMessage(Descriptor descriptor) { + return CelProtoTypes.createMessage(descriptor.getFullName()); + } - abstract Decl getDecl(); + private CelProtoMessageTypes() {} } diff --git a/common/src/main/java/dev/cel/common/types/CelProtoTypes.java b/common/src/main/java/dev/cel/common/types/CelProtoTypes.java new file mode 100644 index 000000000..feddb02db --- /dev/null +++ b/common/src/main/java/dev/cel/common/types/CelProtoTypes.java @@ -0,0 +1,275 @@ +// 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. + +package dev.cel.common.types; + +import static com.google.common.collect.ImmutableList.toImmutableList; + +import dev.cel.expr.Type; +import dev.cel.expr.Type.AbstractType; +import dev.cel.expr.Type.PrimitiveType; +import dev.cel.expr.Type.WellKnownType; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.protobuf.Empty; +import com.google.protobuf.NullValue; + +/** + * Utility class for working with {@link Type}. + * + *

This is equivalent to {@link CelTypes}, except this works specifically with canonical CEL expr + * protos. + */ +public final class CelProtoTypes { + + public static final Type ERROR = Type.newBuilder().setError(Empty.getDefaultInstance()).build(); + public static final Type DYN = Type.newBuilder().setDyn(Empty.getDefaultInstance()).build(); + public static final Type NULL_TYPE = Type.newBuilder().setNull(NullValue.NULL_VALUE).build(); + public static final Type BOOL = create(PrimitiveType.BOOL); + public static final Type BYTES = create(PrimitiveType.BYTES); + public static final Type STRING = create(PrimitiveType.STRING); + public static final Type DOUBLE = create(PrimitiveType.DOUBLE); + public static final Type UINT64 = create(PrimitiveType.UINT64); + public static final Type INT64 = create(PrimitiveType.INT64); + public static final Type ANY = create(WellKnownType.ANY); + public static final Type TIMESTAMP = create(WellKnownType.TIMESTAMP); + public static final Type DURATION = create(WellKnownType.DURATION); + + private static final ImmutableMap SIMPLE_CEL_KIND_TO_TYPE = + ImmutableMap.builder() + .put(CelKind.ERROR, ERROR) + .put(CelKind.DYN, DYN) + .put(CelKind.ANY, ANY) + .put(CelKind.BOOL, BOOL) + .put(CelKind.BYTES, BYTES) + .put(CelKind.DOUBLE, DOUBLE) + .put(CelKind.DURATION, DURATION) + .put(CelKind.INT, INT64) + .put(CelKind.NULL_TYPE, NULL_TYPE) + .put(CelKind.STRING, STRING) + .put(CelKind.TIMESTAMP, TIMESTAMP) + .put(CelKind.UINT, UINT64) + .buildOrThrow(); + + private static final ImmutableMap PROTOBUF_TYPE_TO_CEL_TYPE_MAP = + ImmutableMap.builder() + .put(BOOL, SimpleType.BOOL) + .put(BYTES, SimpleType.BYTES) + .put(DOUBLE, SimpleType.DOUBLE) + .put(INT64, SimpleType.INT) + .put(STRING, SimpleType.STRING) + .put(UINT64, SimpleType.UINT) + .put(ANY, SimpleType.ANY) + .put(DURATION, SimpleType.DURATION) + .put(TIMESTAMP, SimpleType.TIMESTAMP) + .put(DYN, SimpleType.DYN) + .put(NULL_TYPE, SimpleType.NULL_TYPE) + .put(ERROR, SimpleType.ERROR) + .buildOrThrow(); + + /** Create a primitive {@code Type}. */ + public static Type create(PrimitiveType type) { + return Type.newBuilder().setPrimitive(type).build(); + } + + /** Create a well-known {@code Type}. */ + public static Type create(WellKnownType type) { + return Type.newBuilder().setWellKnown(type).build(); + } + + /** Create a type {@code Type}. */ + public static Type create(Type target) { + return Type.newBuilder().setType(target).build(); + } + + /** Create a list with {@code elemType}. */ + public static Type createList(Type elemType) { + return Type.newBuilder().setListType(Type.ListType.newBuilder().setElemType(elemType)).build(); + } + + /** Create a map with {@code keyType} and {@code valueType}. */ + public static Type createMap(Type keyType, Type valueType) { + return Type.newBuilder() + .setMapType(Type.MapType.newBuilder().setKeyType(keyType).setValueType(valueType)) + .build(); + } + + /** Create a message {@code Type} for {@code messageName}. */ + public static Type createMessage(String messageName) { + return Type.newBuilder().setMessageType(messageName).build(); + } + + /** Create a type param {@code Type}. */ + public static Type createTypeParam(String name) { + return Type.newBuilder().setTypeParam(name).build(); + } + + /** Create a wrapper type for the {@code primitive}. */ + public static Type createWrapper(PrimitiveType primitive) { + return Type.newBuilder().setWrapper(primitive).build(); + } + + /** Create a wrapper type where the input is a {@code Type} of primitive types. */ + public static Type createWrapper(Type type) { + Preconditions.checkArgument(type.getTypeKindCase() == Type.TypeKindCase.PRIMITIVE); + return createWrapper(type.getPrimitive()); + } + + /** + * Create an abstract type indicating that the parameterized type may be contained within the + * object. + */ + public static Type createOptionalType(Type paramType) { + return Type.newBuilder() + .setAbstractType( + AbstractType.newBuilder() + .setName(OptionalType.NAME) + .addParameterTypes(paramType) + .build()) + .build(); + } + + /** Checks if the provided parameter is an optional type */ + public static boolean isOptionalType(Type type) { + return type.hasAbstractType() && type.getAbstractType().getName().equals(OptionalType.NAME); + } + + /** + * Method to adapt a simple {@code Type} into a {@code String} representation. + * + *

This method can also format global functions. See the {@link CelTypes#formatFunction} + * methods for richer control over function formatting. + */ + public static String format(Type type) { + return CelTypes.format(typeToCelType(type), /* typeParamToDyn= */ false); + } + + /** Converts a Protobuf type into CEL native type. */ + public static Type celTypeToType(CelType celType) { + Type type = SIMPLE_CEL_KIND_TO_TYPE.get(celType.kind()); + if (type != null) { + if (celType instanceof NullableType) { + return createWrapper(type); + } + return type; + } + + switch (celType.kind()) { + case UNSPECIFIED: + return Type.getDefaultInstance(); + case LIST: + ListType listType = (ListType) celType; + if (listType.hasElemType()) { + return createList(celTypeToType(listType.elemType())); + } else { + // TODO: Exists for compatibility reason only. Remove after callers have been + // migrated. + return Type.newBuilder().setListType(Type.ListType.getDefaultInstance()).build(); + } + case MAP: + MapType mapType = (MapType) celType; + return createMap(celTypeToType(mapType.keyType()), celTypeToType(mapType.valueType())); + case OPAQUE: + if (celType.name().equals("function")) { + Type.FunctionType.Builder functionBuilder = Type.FunctionType.newBuilder(); + if (!celType.parameters().isEmpty()) { + functionBuilder + .setResultType(celTypeToType(celType.parameters().get(0))) + .addAllArgTypes( + celType.parameters().stream() + .skip(1) + .map(CelProtoTypes::celTypeToType) + .collect(toImmutableList())); + } + return Type.newBuilder().setFunction(functionBuilder).build(); + } else { + return Type.newBuilder() + .setAbstractType( + Type.AbstractType.newBuilder() + .setName(celType.name()) + .addAllParameterTypes( + celType.parameters().stream() + .map(CelProtoTypes::celTypeToType) + .collect(toImmutableList()))) + .build(); + } + case STRUCT: + return createMessage(celType.name()); + case TYPE: + TypeType typeType = (TypeType) celType; + return create(celTypeToType(typeType.type())); + case TYPE_PARAM: + return createTypeParam(celType.name()); + default: + throw new IllegalArgumentException(String.format("Unsupported type: %s", celType)); + } + } + + /** Converts a Protobuf type to CEL native type. */ + public static CelType typeToCelType(Type type) { + CelType celType = PROTOBUF_TYPE_TO_CEL_TYPE_MAP.get(type); + if (celType != null) { + return celType; + } + + switch (type.getTypeKindCase()) { + case TYPEKIND_NOT_SET: + return UnspecifiedType.create(); + case WRAPPER: + return NullableType.create(typeToCelType(create(type.getWrapper()))); + case MESSAGE_TYPE: + return StructTypeReference.create(type.getMessageType()); + case LIST_TYPE: + Type.ListType listType = type.getListType(); + if (listType.hasElemType()) { + return ListType.create(typeToCelType(listType.getElemType())); + } else { + // TODO: Exists for compatibility reason only. Remove after callers have been + // migrated. + return ListType.create(); + } + case MAP_TYPE: + Type.MapType mapType = type.getMapType(); + return MapType.create( + typeToCelType(mapType.getKeyType()), typeToCelType(mapType.getValueType())); + case TYPE_PARAM: + return TypeParamType.create(type.getTypeParam()); + case ABSTRACT_TYPE: + Type.AbstractType abstractType = type.getAbstractType(); + ImmutableList params = + abstractType.getParameterTypesList().stream() + .map(CelProtoTypes::typeToCelType) + .collect(toImmutableList()); + if (abstractType.getName().equals(OptionalType.NAME)) { + return OptionalType.create(params.get(0)); + } + return OpaqueType.create(abstractType.getName(), params); + case TYPE: + return TypeType.create(typeToCelType(type.getType())); + case FUNCTION: + Type.FunctionType functionType = type.getFunction(); + return CelTypes.createFunctionType( + typeToCelType(functionType.getResultType()), + functionType.getArgTypesList().stream() + .map(CelProtoTypes::typeToCelType) + .collect(toImmutableList())); + default: + // Add more cases as needed. + throw new IllegalArgumentException(String.format("Unsupported type: %s", type)); + } + } + + private CelProtoTypes() {} +} diff --git a/common/src/main/java/dev/cel/common/types/CelTypes.java b/common/src/main/java/dev/cel/common/types/CelTypes.java index 1d1782145..34e0fa5ef 100644 --- a/common/src/main/java/dev/cel/common/types/CelTypes.java +++ b/common/src/main/java/dev/cel/common/types/CelTypes.java @@ -14,25 +14,14 @@ package dev.cel.common.types; -import static com.google.common.collect.ImmutableList.toImmutableList; - -import dev.cel.expr.Type; -import dev.cel.expr.Type.AbstractType; -import dev.cel.expr.Type.PrimitiveType; -import dev.cel.expr.Type.WellKnownType; -import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Joiner; -import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; -import com.google.protobuf.Descriptors.Descriptor; -import com.google.protobuf.Empty; -import com.google.protobuf.NullValue; import dev.cel.common.annotations.Internal; import java.util.Optional; -/** Utility class for working with {@link Type}. */ +/** Utility class for working with {@code CelType}. */ public final class CelTypes { // Message type names with well-known type equivalents or special handling. @@ -54,40 +43,6 @@ public final class CelTypes { public static final String UINT32_WRAPPER_MESSAGE = "google.protobuf.UInt32Value"; public static final String UINT64_WRAPPER_MESSAGE = "google.protobuf.UInt64Value"; - // Static types. - public static final Type ERROR = Type.newBuilder().setError(Empty.getDefaultInstance()).build(); - public static final Type DYN = Type.newBuilder().setDyn(Empty.getDefaultInstance()).build(); - public static final Type NULL_TYPE = Type.newBuilder().setNull(NullValue.NULL_VALUE).build(); - public static final Type BOOL = create(PrimitiveType.BOOL); - public static final Type BYTES = create(PrimitiveType.BYTES); - public static final Type STRING = create(PrimitiveType.STRING); - public static final Type DOUBLE = create(PrimitiveType.DOUBLE); - public static final Type UINT64 = create(PrimitiveType.UINT64); - public static final Type INT64 = create(PrimitiveType.INT64); - public static final Type ANY = create(WellKnownType.ANY); - public static final Type TIMESTAMP = create(WellKnownType.TIMESTAMP); - public static final Type DURATION = create(WellKnownType.DURATION); - - /** Map of well-known proto messages and their CEL {@code Type} equivalents. */ - static final ImmutableMap WELL_KNOWN_TYPE_MAP = - ImmutableMap.builder() - .put(DOUBLE_WRAPPER_MESSAGE, CelTypes.createWrapper(CelTypes.DOUBLE)) - .put(FLOAT_WRAPPER_MESSAGE, CelTypes.createWrapper(CelTypes.DOUBLE)) - .put(INT64_WRAPPER_MESSAGE, CelTypes.createWrapper(CelTypes.INT64)) - .put(INT32_WRAPPER_MESSAGE, CelTypes.createWrapper(CelTypes.INT64)) - .put(UINT64_WRAPPER_MESSAGE, CelTypes.createWrapper(CelTypes.UINT64)) - .put(UINT32_WRAPPER_MESSAGE, CelTypes.createWrapper(CelTypes.UINT64)) - .put(BOOL_WRAPPER_MESSAGE, CelTypes.createWrapper(CelTypes.BOOL)) - .put(STRING_WRAPPER_MESSAGE, CelTypes.createWrapper(CelTypes.STRING)) - .put(BYTES_WRAPPER_MESSAGE, CelTypes.createWrapper(CelTypes.BYTES)) - .put(TIMESTAMP_MESSAGE, CelTypes.TIMESTAMP) - .put(DURATION_MESSAGE, CelTypes.DURATION) - .put(STRUCT_MESSAGE, CelTypes.createMap(CelTypes.STRING, CelTypes.DYN)) - .put(VALUE_MESSAGE, CelTypes.DYN) - .put(LIST_VALUE_MESSAGE, CelTypes.createList(CelTypes.DYN)) - .put(ANY_MESSAGE, CelTypes.ANY) - .buildOrThrow(); - private static final ImmutableMap WELL_KNOWN_CEL_TYPE_MAP = ImmutableMap.builder() .put(BOOL_WRAPPER_MESSAGE, NullableType.create(SimpleType.BOOL)) @@ -107,91 +62,6 @@ public final class CelTypes { .put(VALUE_MESSAGE, SimpleType.DYN) .buildOrThrow(); - static final ImmutableMap SIMPLE_CEL_KIND_TO_TYPE = - ImmutableMap.builder() - .put(CelKind.ERROR, CelTypes.ERROR) - .put(CelKind.DYN, CelTypes.DYN) - .put(CelKind.ANY, CelTypes.ANY) - .put(CelKind.BOOL, CelTypes.BOOL) - .put(CelKind.BYTES, CelTypes.BYTES) - .put(CelKind.DOUBLE, CelTypes.DOUBLE) - .put(CelKind.DURATION, CelTypes.DURATION) - .put(CelKind.INT, CelTypes.INT64) - .put(CelKind.NULL_TYPE, CelTypes.NULL_TYPE) - .put(CelKind.STRING, CelTypes.STRING) - .put(CelKind.TIMESTAMP, CelTypes.TIMESTAMP) - .put(CelKind.UINT, CelTypes.UINT64) - .buildOrThrow(); - - private static final ImmutableMap PROTOBUF_TYPE_TO_CEL_TYPE_MAP = - ImmutableMap.builder() - .put(CelTypes.BOOL, SimpleType.BOOL) - .put(CelTypes.BYTES, SimpleType.BYTES) - .put(CelTypes.DOUBLE, SimpleType.DOUBLE) - .put(CelTypes.INT64, SimpleType.INT) - .put(CelTypes.STRING, SimpleType.STRING) - .put(CelTypes.UINT64, SimpleType.UINT) - .put(CelTypes.ANY, SimpleType.ANY) - .put(CelTypes.DURATION, SimpleType.DURATION) - .put(CelTypes.TIMESTAMP, SimpleType.TIMESTAMP) - .put(CelTypes.DYN, SimpleType.DYN) - .put(CelTypes.NULL_TYPE, SimpleType.NULL_TYPE) - .put(CelTypes.ERROR, SimpleType.ERROR) - .buildOrThrow(); - - /** Create a primitive {@code Type}. */ - public static Type create(PrimitiveType type) { - return Type.newBuilder().setPrimitive(type).build(); - } - - /** Create a well-known {@code Type}. */ - public static Type create(WellKnownType type) { - return Type.newBuilder().setWellKnown(type).build(); - } - - /** Create a type {@code Type}. */ - public static Type create(Type target) { - return Type.newBuilder().setType(target).build(); - } - - /** Create a list with {@code elemType}. */ - public static Type createList(Type elemType) { - return Type.newBuilder().setListType(Type.ListType.newBuilder().setElemType(elemType)).build(); - } - - /** Create a map with {@code keyType} and {@code valueType}. */ - public static Type createMap(Type keyType, Type valueType) { - return Type.newBuilder() - .setMapType(Type.MapType.newBuilder().setKeyType(keyType).setValueType(valueType)) - .build(); - } - - /** Create a message {@code Type} for {@code messageName}. */ - public static Type createMessage(String messageName) { - return Type.newBuilder().setMessageType(messageName).build(); - } - - /** Create a message {@code Type} for {@code Descriptor}. */ - public static Type createMessage(Descriptor descriptor) { - return createMessage(descriptor.getFullName()); - } - - /** Create a type param {@code Type}. */ - public static Type createTypeParam(String name) { - return Type.newBuilder().setTypeParam(name).build(); - } - - /** Create a wrapper type for the {@code primitive}. */ - public static Type createWrapper(PrimitiveType primitive) { - return Type.newBuilder().setWrapper(primitive).build(); - } - - /** Create a wrapper type where the input is a {@code Type} of primitive types. */ - public static Type createWrapper(Type type) { - Preconditions.checkArgument(type.getTypeKindCase() == Type.TypeKindCase.PRIMITIVE); - return createWrapper(type.getPrimitive()); - } - /** Checks if the fully-qualified protobuf type name is a wrapper type. */ public static boolean isWrapperType(String typeName) { switch (typeName) { @@ -210,26 +80,6 @@ public static boolean isWrapperType(String typeName) { } } - /** - * Create an abstract type indicating that the parameterized type may be contained within the - * object. - */ - @VisibleForTesting - public static Type createOptionalType(Type paramType) { - return Type.newBuilder() - .setAbstractType( - AbstractType.newBuilder() - .setName(OptionalType.NAME) - .addParameterTypes(paramType) - .build()) - .build(); - } - - /** Checks if the provided parameter is an optional type */ - public static boolean isOptionalType(Type type) { - return type.hasAbstractType() && type.getAbstractType().getName().equals(OptionalType.NAME); - } - /** * Create an abstract type with an expected result type (first argument in the parameter) and the * argument types. @@ -244,19 +94,6 @@ public static OpaqueType createFunctionType(CelType resultType, IterableThis method can also format global functions. See the {@link #formatFunction} methods for - * richer control over function formatting. - * - * @deprecated Use {@link #format(CelType)} instead. - */ - @Deprecated - public static String format(Type type) { - return format(typeToCelType(type), /* typeParamToDyn= */ false); - } - /** * Method to adapt a simple {@code Type} into a {@code String} representation. * @@ -267,7 +104,7 @@ public static String format(CelType type) { return format(type, /* typeParamToDyn= */ false); } - private static String format(CelType type, boolean typeParamToDyn) { + static String format(CelType type, boolean typeParamToDyn) { if (type instanceof NullableType) { return String.format( "wrapper(%s)", format(((NullableType) type).targetType(), typeParamToDyn)); @@ -363,130 +200,13 @@ public static String formatFunction( } public static boolean isWellKnownType(String typeName) { - return WELL_KNOWN_TYPE_MAP.containsKey(typeName); + return WELL_KNOWN_CEL_TYPE_MAP.containsKey(typeName); } public static Optional getWellKnownCelType(String typeName) { return Optional.ofNullable(WELL_KNOWN_CEL_TYPE_MAP.getOrDefault(typeName, null)); } - /** Converts a Protobuf type into CEL native type. */ - @Internal - public static Type celTypeToType(CelType celType) { - Type type = SIMPLE_CEL_KIND_TO_TYPE.get(celType.kind()); - if (type != null) { - if (celType instanceof NullableType) { - return CelTypes.createWrapper(type); - } - return type; - } - - switch (celType.kind()) { - case UNSPECIFIED: - return Type.getDefaultInstance(); - case LIST: - ListType listType = (ListType) celType; - if (listType.hasElemType()) { - return CelTypes.createList(celTypeToType(listType.elemType())); - } else { - // TODO: Exists for compatibility reason only. Remove after callers have been - // migrated. - return Type.newBuilder().setListType(Type.ListType.getDefaultInstance()).build(); - } - case MAP: - MapType mapType = (MapType) celType; - return CelTypes.createMap( - celTypeToType(mapType.keyType()), celTypeToType(mapType.valueType())); - case OPAQUE: - if (celType.name().equals("function")) { - Type.FunctionType.Builder functionBuilder = Type.FunctionType.newBuilder(); - if (!celType.parameters().isEmpty()) { - functionBuilder.setResultType(CelTypes.celTypeToType(celType.parameters().get(0))); - functionBuilder.addAllArgTypes( - celType.parameters().stream() - .skip(1) - .map(CelTypes::celTypeToType) - .collect(toImmutableList())); - } - return Type.newBuilder().setFunction(functionBuilder).build(); - } else { - return Type.newBuilder() - .setAbstractType( - Type.AbstractType.newBuilder() - .setName(celType.name()) - .addAllParameterTypes( - celType.parameters().stream() - .map(CelTypes::celTypeToType) - .collect(toImmutableList()))) - .build(); - } - case STRUCT: - return CelTypes.createMessage(celType.name()); - case TYPE: - TypeType typeType = (TypeType) celType; - return CelTypes.create(celTypeToType(typeType.type())); - case TYPE_PARAM: - return CelTypes.createTypeParam(celType.name()); - default: - throw new IllegalArgumentException(String.format("Unsupported type: %s", celType)); - } - } - - /** Converts a Protobuf type to CEL native type. */ - @Internal - public static CelType typeToCelType(Type type) { - CelType celType = PROTOBUF_TYPE_TO_CEL_TYPE_MAP.get(type); - if (celType != null) { - return celType; - } - - switch (type.getTypeKindCase()) { - case TYPEKIND_NOT_SET: - return UnspecifiedType.create(); - case WRAPPER: - return NullableType.create(typeToCelType(CelTypes.create(type.getWrapper()))); - case MESSAGE_TYPE: - return StructTypeReference.create(type.getMessageType()); - case LIST_TYPE: - Type.ListType listType = type.getListType(); - if (listType.hasElemType()) { - return ListType.create(typeToCelType(listType.getElemType())); - } else { - // TODO: Exists for compatibility reason only. Remove after callers have been - // migrated. - return ListType.create(); - } - case MAP_TYPE: - Type.MapType mapType = type.getMapType(); - return MapType.create( - typeToCelType(mapType.getKeyType()), typeToCelType(mapType.getValueType())); - case TYPE_PARAM: - return TypeParamType.create(type.getTypeParam()); - case ABSTRACT_TYPE: - Type.AbstractType abstractType = type.getAbstractType(); - ImmutableList params = - abstractType.getParameterTypesList().stream() - .map(CelTypes::typeToCelType) - .collect(toImmutableList()); - if (abstractType.getName().equals(OptionalType.NAME)) { - return OptionalType.create(params.get(0)); - } - return OpaqueType.create(abstractType.getName(), params); - case TYPE: - return TypeType.create(typeToCelType(type.getType())); - case FUNCTION: - Type.FunctionType functionType = type.getFunction(); - return CelTypes.createFunctionType( - CelTypes.typeToCelType(functionType.getResultType()), - functionType.getArgTypesList().stream() - .map(CelTypes::typeToCelType) - .collect(toImmutableList())); - default: - // Add more cases as needed. - throw new IllegalArgumentException(String.format("Unsupported type: %s", type)); - } - } - private static String formatTypeArgs(Iterable types, final boolean typeParamToDyn) { return String.format( "(%s)", diff --git a/common/src/main/java/dev/cel/common/types/DefaultTypeProvider.java b/common/src/main/java/dev/cel/common/types/DefaultTypeProvider.java new file mode 100644 index 000000000..372a50e73 --- /dev/null +++ b/common/src/main/java/dev/cel/common/types/DefaultTypeProvider.java @@ -0,0 +1,55 @@ +// 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.types; + +import com.google.common.collect.ImmutableCollection; +import com.google.common.collect.ImmutableMap; +import com.google.errorprone.annotations.Immutable; +import java.util.Optional; + +/** {@code DefaultTypeProvider} is a registry of common CEL types. */ +@Immutable +public class DefaultTypeProvider implements CelTypeProvider { + + private static final DefaultTypeProvider INSTANCE = new DefaultTypeProvider(); + private final ImmutableMap commonTypes; + + @Override + public ImmutableCollection types() { + return commonTypes.values(); + } + + @Override + public Optional findType(String typeName) { + return Optional.ofNullable(commonTypes.get(typeName)); + } + + public static DefaultTypeProvider getInstance() { + return INSTANCE; + } + + private DefaultTypeProvider() { + ImmutableMap.Builder typeMapBuilder = ImmutableMap.builder(); + typeMapBuilder.putAll(SimpleType.TYPE_MAP); + typeMapBuilder.put("list", ListType.create(SimpleType.DYN)); + typeMapBuilder.put("map", MapType.create(SimpleType.DYN, SimpleType.DYN)); + typeMapBuilder.put("type", TypeType.create(SimpleType.DYN)); + typeMapBuilder.put( + "optional_type", + // TODO: Move to CelOptionalLibrary and register it on demand + OptionalType.create(SimpleType.DYN)); + this.commonTypes = typeMapBuilder.buildOrThrow(); + } +} diff --git a/common/src/main/java/dev/cel/common/types/ProtoMessageType.java b/common/src/main/java/dev/cel/common/types/ProtoMessageType.java index 7ac3f4fd3..11e48bbe6 100644 --- a/common/src/main/java/dev/cel/common/types/ProtoMessageType.java +++ b/common/src/main/java/dev/cel/common/types/ProtoMessageType.java @@ -29,14 +29,17 @@ public final class ProtoMessageType extends StructType { private final StructType.FieldResolver extensionResolver; + private final JsonNameResolver jsonNameResolver; ProtoMessageType( String name, ImmutableSet fieldNames, StructType.FieldResolver fieldResolver, - StructType.FieldResolver extensionResolver) { + StructType.FieldResolver extensionResolver, + JsonNameResolver jsonNameResolver) { super(name, fieldNames, fieldResolver); this.extensionResolver = extensionResolver; + this.jsonNameResolver = jsonNameResolver; } /** Find an {@code Extension} by its fully-qualified {@code extensionName}. */ @@ -46,20 +49,35 @@ public Optional findExtension(String extensionName) { .map(type -> Extension.of(extensionName, type, this)); } + /** Returns true if the field name is a json name. */ + public boolean isJsonName(String fieldName) { + return jsonNameResolver.isJsonName(fieldName); + } + /** * Create a new instance of the {@code ProtoMessageType} using the {@code visibleFields} set as a * mask of the fields from the backing proto. */ public ProtoMessageType withVisibleFields(ImmutableSet visibleFields) { - return new ProtoMessageType(name, visibleFields, fieldResolver, extensionResolver); + return new ProtoMessageType( + name, visibleFields, fieldResolver, extensionResolver, jsonNameResolver); } public static ProtoMessageType create( String name, ImmutableSet fieldNames, FieldResolver fieldResolver, - FieldResolver extensionResolver) { - return new ProtoMessageType(name, fieldNames, fieldResolver, extensionResolver); + FieldResolver extensionResolver, + JsonNameResolver jsonNameResolver) { + return new ProtoMessageType( + name, fieldNames, fieldResolver, extensionResolver, jsonNameResolver); + } + + /** Functional interface for resolving whether a field name is a json name. */ + @FunctionalInterface + @Immutable + public interface JsonNameResolver { + boolean isJsonName(String fieldName); } /** {@code Extension} contains the name, type, and target message type of the extension. */ diff --git a/common/src/main/java/dev/cel/common/types/ProtoMessageTypeProvider.java b/common/src/main/java/dev/cel/common/types/ProtoMessageTypeProvider.java index f366a79bb..022f5cd8e 100644 --- a/common/src/main/java/dev/cel/common/types/ProtoMessageTypeProvider.java +++ b/common/src/main/java/dev/cel/common/types/ProtoMessageTypeProvider.java @@ -14,15 +14,14 @@ package dev.cel.common.types; -import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableMap.toImmutableMap; import com.google.common.collect.ImmutableCollection; -import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMultimap; 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 com.google.protobuf.DescriptorProtos.FileDescriptorSet; @@ -34,6 +33,7 @@ import dev.cel.common.CelDescriptorUtil; import dev.cel.common.CelDescriptors; import dev.cel.common.internal.FileDescriptorSetConverter; +import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.Map; @@ -68,28 +68,53 @@ public final class ProtoMessageTypeProvider implements CelTypeProvider { .buildOrThrow(); private final ImmutableMap allTypes; + private final boolean allowJsonFieldNames; + /** Returns a new builder for {@link ProtoMessageTypeProvider}. */ + public static Builder newBuilder() { + return new Builder(); + } + + /** + * @deprecated Use {@link #newBuilder()} instead. + */ + @Deprecated public ProtoMessageTypeProvider() { - this(CelDescriptors.builder().build()); + this(CelDescriptors.builder().build(), false); } + /** + * @deprecated Use {@link #newBuilder()} instead. + */ + @Deprecated public ProtoMessageTypeProvider(FileDescriptorSet descriptorSet) { this( CelDescriptorUtil.getAllDescriptorsFromFileDescriptor( - FileDescriptorSetConverter.convert(descriptorSet))); + FileDescriptorSetConverter.convert(descriptorSet)), + false); } + /** + * @deprecated Use {@link #newBuilder()} instead. + */ + @Deprecated public ProtoMessageTypeProvider(Iterable descriptors) { this( CelDescriptorUtil.getAllDescriptorsFromFileDescriptor( - ImmutableSet.copyOf(Iterables.transform(descriptors, Descriptor::getFile)))); + ImmutableSet.copyOf(Iterables.transform(descriptors, Descriptor::getFile))), + false); } + /** + * @deprecated Use {@link #newBuilder()} instead. + */ + @Deprecated public ProtoMessageTypeProvider(ImmutableSet fileDescriptors) { - this(CelDescriptorUtil.getAllDescriptorsFromFileDescriptor(fileDescriptors)); + this(CelDescriptorUtil.getAllDescriptorsFromFileDescriptor(fileDescriptors), false); } - public ProtoMessageTypeProvider(CelDescriptors celDescriptors) { + private ProtoMessageTypeProvider(CelDescriptors celDescriptors, boolean allowJsonFieldNames) { + this.allowJsonFieldNames = allowJsonFieldNames; this.allTypes = ImmutableMap.builder() .putAll(createEnumTypes(celDescriptors.enumDescriptors())) @@ -120,8 +145,18 @@ private ImmutableMap createProtoMessageTypes( if (protoMessageTypes.containsKey(descriptor.getFullName())) { continue; } - ImmutableList fieldNames = - descriptor.getFields().stream().map(FieldDescriptor::getName).collect(toImmutableList()); + + ImmutableSet.Builder fieldNamesBuilder = ImmutableSet.builder(); + ImmutableSet.Builder jsonNamesBuilder = ImmutableSet.builder(); + for (FieldDescriptor fd : descriptor.getFields()) { + if (allowJsonFieldNames) { + fieldNamesBuilder.add(fd.getJsonName()); + jsonNamesBuilder.add(fd.getJsonName()); + } else { + fieldNamesBuilder.add(fd.getName()); + } + } + ImmutableSet jsonNames = jsonNamesBuilder.build(); Map extensionFields = new HashMap<>(); for (FieldDescriptor extension : extensionMap.get(descriptor.getFullName())) { @@ -133,17 +168,21 @@ private ImmutableMap createProtoMessageTypes( descriptor.getFullName(), ProtoMessageType.create( descriptor.getFullName(), - ImmutableSet.copyOf(fieldNames), + fieldNamesBuilder.build(), new FieldResolver(this, descriptor)::findField, - new FieldResolver(this, extensions)::findField)); + new FieldResolver(this, extensions)::findField, + jsonNames::contains)); } return ImmutableMap.copyOf(protoMessageTypes); } private ImmutableMap createEnumTypes( Collection enumDescriptors) { - ImmutableMap.Builder enumTypes = ImmutableMap.builder(); + HashMap enumTypes = new HashMap<>(); for (EnumDescriptor enumDescriptor : enumDescriptors) { + if (enumTypes.containsKey(enumDescriptor.getFullName())) { + continue; + } ImmutableMap values = enumDescriptor.getValues().stream() .collect( @@ -151,23 +190,46 @@ private ImmutableMap createEnumTypes( enumTypes.put( enumDescriptor.getFullName(), EnumType.create(enumDescriptor.getFullName(), values)); } - return enumTypes.buildOrThrow(); + return ImmutableMap.copyOf(enumTypes); } private static class FieldResolver { - private final CelTypeProvider celTypeProvider; + private final ProtoMessageTypeProvider protoMessageTypeProvider; private final ImmutableMap fields; - private FieldResolver(CelTypeProvider celTypeProvider, Descriptor descriptor) { + private static ImmutableMap collectJsonFieldDescriptorMap( + Descriptor descriptor) { + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (FieldDescriptor fd : descriptor.getFields()) { + if (!fd.getJsonName().isEmpty()) { + builder.put(fd.getJsonName(), fd); + } else { + builder.put(fd.getName(), fd); + } + } + + return builder.buildOrThrow(); + } + + private static ImmutableMap collectFieldDescriptorMap( + Descriptor descriptor) { + return descriptor.getFields().stream() + .collect(toImmutableMap(FieldDescriptor::getName, Function.identity())); + } + + private FieldResolver( + ProtoMessageTypeProvider protoMessageTypeProvider, Descriptor descriptor) { this( - celTypeProvider, - descriptor.getFields().stream() - .collect(toImmutableMap(FieldDescriptor::getName, Function.identity()))); + protoMessageTypeProvider, + protoMessageTypeProvider.allowJsonFieldNames + ? collectJsonFieldDescriptorMap(descriptor) + : collectFieldDescriptorMap(descriptor)); } private FieldResolver( - CelTypeProvider celTypeProvider, ImmutableMap fields) { - this.celTypeProvider = celTypeProvider; + ProtoMessageTypeProvider protoMessageTypeProvider, + ImmutableMap fields) { + this.protoMessageTypeProvider = protoMessageTypeProvider; this.fields = fields; } @@ -200,11 +262,11 @@ private Optional findFieldInternal(FieldDescriptor fieldDescriptor) { String messageName = descriptor.getFullName(); fieldType = CelTypes.getWellKnownCelType(messageName) - .orElse(celTypeProvider.findType(descriptor.getFullName()).orElse(null)); + .orElse(protoMessageTypeProvider.findType(descriptor.getFullName()).orElse(null)); break; case ENUM: EnumDescriptor enumDescriptor = fieldDescriptor.getEnumType(); - fieldType = celTypeProvider.findType(enumDescriptor.getFullName()).orElse(null); + fieldType = protoMessageTypeProvider.findType(enumDescriptor.getFullName()).orElse(null); break; default: fieldType = PROTO_TYPE_TO_CEL_TYPE.get(fieldDescriptor.getType()); @@ -219,4 +281,83 @@ private Optional findFieldInternal(FieldDescriptor fieldDescriptor) { return Optional.of(fieldType); } } + + /** Builder for {@link ProtoMessageTypeProvider}. */ + public static final class Builder { + private final ImmutableSet.Builder fileDescriptors = ImmutableSet.builder(); + private boolean allowJsonFieldNames; + private boolean resolveTypeDependencies; + private CelDescriptors celDescriptors; + + /** Adds a {@link FileDescriptor} to the provider. */ + @CanIgnoreReturnValue + public Builder addFileDescriptors(FileDescriptor... fileDescriptors) { + return addFileDescriptors(Arrays.asList(fileDescriptors)); + } + + /** Adds a collection of {@link FileDescriptor}s to the provider. */ + @CanIgnoreReturnValue + public Builder addFileDescriptors(Iterable fileDescriptors) { + this.fileDescriptors.addAll(fileDescriptors); + return this; + } + + /** Adds a collection of {@link Descriptor}s. The parent file of each descriptor is added. */ + @CanIgnoreReturnValue + public Builder addDescriptors(Iterable descriptors) { + this.fileDescriptors.addAll(Iterables.transform(descriptors, Descriptor::getFile)); + return this; + } + + /** + * Use the `json_name` field option on a protobuf message as the name of the field. + * + *

If enabled, the type checker will only accept the `json_name` and will no longer recognize + * the original protobuf field name. This is to avoid ambiguity between the two names. + */ + @CanIgnoreReturnValue + public Builder setAllowJsonFieldNames(boolean allowJsonFieldNames) { + this.allowJsonFieldNames = allowJsonFieldNames; + return this; + } + + /** + * If true, all transitive dependencies of the added {@link FileDescriptor}s will be resolved + * and their types will be made available to the type provider. By default, this is disabled. + */ + @CanIgnoreReturnValue + public Builder setResolveTypeDependencies(boolean resolveTypeDependencies) { + this.resolveTypeDependencies = resolveTypeDependencies; + return this; + } + + /** + * Sets the CEL descriptors. Note this cannot be used in conjunction with other descriptor + * adders such as {@link #addDescriptors}. + */ + @CanIgnoreReturnValue + public Builder setCelDescriptors(CelDescriptors celDescriptors) { + this.celDescriptors = celDescriptors; + return this; + } + + /** Builds the {@link ProtoMessageTypeProvider}. */ + public ProtoMessageTypeProvider build() { + ImmutableSet fds = fileDescriptors.build(); + if (celDescriptors != null && !fds.isEmpty()) { + throw new IllegalArgumentException( + "Both CelDescriptors and FileDescriptors cannot be set at the same time."); + } + + if (celDescriptors == null) { + celDescriptors = + CelDescriptorUtil.getAllDescriptorsFromFileDescriptor( + fileDescriptors.build(), resolveTypeDependencies); + } + + return new ProtoMessageTypeProvider(celDescriptors, allowJsonFieldNames); + } + + private Builder() {} + } } 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 378c669f1..93bd5326d 100644 --- a/common/src/main/java/dev/cel/common/types/SimpleType.java +++ b/common/src/main/java/dev/cel/common/types/SimpleType.java @@ -15,8 +15,10 @@ package dev.cel.common.types; import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableMap; import com.google.errorprone.annotations.CheckReturnValue; import com.google.errorprone.annotations.Immutable; +import java.util.Optional; /** Simple types represent scalar, dynamic, and error values. */ @AutoValue @@ -42,6 +44,18 @@ public abstract class SimpleType extends CelType { public static final CelType TIMESTAMP = create(CelKind.TIMESTAMP, "google.protobuf.Timestamp"); public static final CelType UINT = create(CelKind.UINT, "uint"); + public static final ImmutableMap TYPE_MAP = + ImmutableMap.of( + BOOL.name(), BOOL, + BYTES.name(), BYTES, + DOUBLE.name(), DOUBLE, + DURATION.name(), DURATION, + INT.name(), INT, + NULL_TYPE.name(), NULL_TYPE, + STRING.name(), STRING, + TIMESTAMP.name(), TIMESTAMP, + UINT.name(), UINT); + @Override public abstract CelKind kind(); @@ -56,6 +70,11 @@ public boolean isAssignableFrom(CelType other) { || (other instanceof NullableType && other.isAssignableFrom(this)); } + /** Returns a matching SimpleType by its name if one exists. */ + public static Optional findByName(String typeName) { + return Optional.ofNullable(TYPE_MAP.get(typeName)); + } + private static CelType create(CelKind kind, String name) { return new AutoValue_SimpleType(kind, name); } diff --git a/common/src/main/java/dev/cel/common/types/StructType.java b/common/src/main/java/dev/cel/common/types/StructType.java index 60ec2e905..b91b6fef0 100644 --- a/common/src/main/java/dev/cel/common/types/StructType.java +++ b/common/src/main/java/dev/cel/common/types/StructType.java @@ -99,7 +99,7 @@ public static StructType create( */ @Immutable @FunctionalInterface - public static interface FieldResolver { + public interface FieldResolver { /** Find the {@code CelType} for the given {@code fieldName} if the field is defined. */ Optional findField(String fieldName); } diff --git a/common/src/main/java/dev/cel/common/types/UnspecifiedType.java b/common/src/main/java/dev/cel/common/types/UnspecifiedType.java index 0daf90865..2c1df422e 100644 --- a/common/src/main/java/dev/cel/common/types/UnspecifiedType.java +++ b/common/src/main/java/dev/cel/common/types/UnspecifiedType.java @@ -32,7 +32,7 @@ @CheckReturnValue @Immutable @Internal -public class UnspecifiedType extends CelType { +public abstract class UnspecifiedType extends CelType { @Override public CelKind kind() { 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 766e22309..5ccc498fd 100644 --- a/common/src/main/java/dev/cel/common/values/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/values/BUILD.bazel @@ -1,35 +1,25 @@ +load("@rules_java//java:defs.bzl", "java_library") +load("//:cel_android_rules.bzl", "cel_android_library") + package( default_applicable_licenses = [ "//:license", ], default_visibility = [ "//common/values:__pkg__", + "//publish:__pkg__", ], ) # keep sorted CEL_VALUES_SOURCES = [ - "BoolValue.java", - "BytesValue.java", "CelValueConverter.java", - "DoubleValue.java", - "DurationValue.java", - "EnumValue.java", "ErrorValue.java", - "ImmutableListValue.java", - "ImmutableMapValue.java", - "IntValue.java", - "ListValue.java", - "MapValue.java", "NullValue.java", "OpaqueValue.java", "OptionalValue.java", "SelectableValue.java", - "StringValue.java", "StructValue.java", - "TimestampValue.java", - "TypeValue.java", - "UintValue.java", ] # keep sorted @@ -50,20 +40,122 @@ java_library( ], ) +cel_android_library( + name = "cel_value_android", + srcs = ["CelValue.java"], + tags = [ + ], + deps = [ + "//common/annotations", + "//common/types:type_providers_android", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + java_library( name = "cel_value_provider", + srcs = ["CelValueProvider.java"], + tags = [ + ], + deps = [ + "//common/values", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +cel_android_library( + name = "cel_value_provider_android", + srcs = ["CelValueProvider.java"], + tags = [ + ], + deps = [ + "//common/values:values_android", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +java_library( + name = "combined_cel_value_provider", srcs = [ - "CelValueProvider.java", + "CombinedCelValueProvider.java", ], tags = [ ], deps = [ - ":cel_value", + ":combined_cel_value_converter", + ":values", + "//common/values:cel_value_provider", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "combined_cel_value_provider_android", + 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, @@ -72,10 +164,8 @@ java_library( deps = [ ":cel_byte_string", ":cel_value", + ":preadapted_list", "//:auto_value", - "//common:error_codes", - "//common:options", - "//common:runtime_exception", "//common/annotations", "//common/types", "//common/types:type_providers", @@ -85,14 +175,99 @@ 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, + tags = [ + ], + deps = [ + ":cel_byte_string", + ":cel_value_android", + ":preadapted_list_android", + "//:auto_value", + "//common/annotations", + "//common/types:type_providers_android", + "//common/types:types_android", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:org_jspecify_jspecify", + "@maven_android//:com_google_guava_guava", + ], +) + java_library( name = "cel_byte_string", srcs = ["CelByteString.java"], + # used_by_android tags = [ ], deps = [ "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +java_library( + name = "base_proto_cel_value_converter", + srcs = ["BaseProtoCelValueConverter.java"], + tags = [ + ], + deps = [ + ":cel_byte_string", + "//common/annotations", + "//common/internal:proto_time_utils", + "//common/internal:well_known_proto", + "//common/values", + "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + ], +) + +cel_android_library( + name = "base_proto_cel_value_converter_android", + srcs = ["BaseProtoCelValueConverter.java"], + tags = [ + ], + deps = [ + ":cel_byte_string", + ":values_android", + "//common/annotations", + "//common/internal:proto_time_utils_android", + "//common/internal:well_known_proto_android", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", + "@maven_android//:com_google_protobuf_protobuf_javalite", ], ) @@ -102,7 +277,8 @@ java_library( tags = [ ], deps = [ - ":cel_value", + ":base_proto_cel_value_converter", + ":preadapted_list", ":values", "//:auto_value", "//common:options", @@ -111,14 +287,10 @@ java_library( "//common/internal:dynamic_proto", "//common/internal:well_known_proto", "//common/types", - "//common/types:cel_types", "//common/types:type_providers", - "//common/values:cel_byte_string", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", - "@maven//:com_google_protobuf_protobuf_java_util", - "@maven//:org_jspecify_jspecify", ], ) @@ -128,16 +300,129 @@ java_library( tags = [ ], deps = [ - ":cel_value", - ":cel_value_provider", ":proto_message_value", - "//common:error_codes", "//common:options", - "//common:runtime_exception", "//common/annotations", "//common/internal:dynamic_proto", "//common/internal:proto_message_factory", + "//common/values", + "//common/values:cel_value_provider", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_protobuf_protobuf_java", ], ) + +java_library( + name = "proto_message_lite_value", + srcs = [ + "ProtoLiteCelValueConverter.java", + "ProtoMessageLiteValue.java", + ], + tags = [ + ], + deps = [ + ":base_proto_cel_value_converter", + ":values", + "//:auto_value", + "//common/annotations", + "//common/internal:cel_lite_descriptor_pool", + "//common/internal:well_known_proto", + "//common/types", + "//common/types:type_providers", + "//common/values:cel_byte_string", + "//protobuf:cel_lite_descriptor", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven_android//:com_google_protobuf_protobuf_javalite", + ], +) + +cel_android_library( + name = "proto_message_lite_value_android", + srcs = [ + "ProtoLiteCelValueConverter.java", + "ProtoMessageLiteValue.java", + ], + tags = [ + ], + deps = [ + ":base_proto_cel_value_converter_android", + ":values_android", + "//:auto_value", + "//common/annotations", + "//common/internal:cel_lite_descriptor_pool_android", + "//common/internal:well_known_proto_android", + "//common/types:type_providers_android", + "//common/types:types_android", + "//common/values:cel_byte_string", + "//protobuf:cel_lite_descriptor", + "@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", + ], +) + +java_library( + name = "proto_message_lite_value_provider", + srcs = ["ProtoMessageLiteValueProvider.java"], + tags = [ + ], + deps = [ + ":base_proto_message_value_provider", + ":proto_message_lite_value", + "//common/annotations", + "//common/internal:cel_lite_descriptor_pool", + "//common/internal:default_lite_descriptor_pool", + "//common/values:base_proto_cel_value_converter", + "//protobuf:cel_lite_descriptor", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven_android//:com_google_protobuf_protobuf_javalite", + ], +) + +cel_android_library( + name = "proto_message_lite_value_provider_android", + srcs = ["ProtoMessageLiteValueProvider.java"], + tags = [ + ], + deps = [ + ":base_proto_message_value_provider_android", + ":proto_message_lite_value_android", + "//common/annotations", + "//common/internal:cel_lite_descriptor_pool_android", + "//common/internal:default_lite_descriptor_pool_android", + "//common/values:base_proto_cel_value_converter_android", + "//protobuf:cel_lite_descriptor", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", + "@maven_android//:com_google_protobuf_protobuf_javalite", + ], +) + +java_library( + name = "base_proto_message_value_provider", + srcs = ["BaseProtoMessageValueProvider.java"], + tags = [ + ], + deps = [ + "//common/annotations", + "//common/values:base_proto_cel_value_converter", + "//common/values:cel_value_provider", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +cel_android_library( + name = "base_proto_message_value_provider_android", + srcs = ["BaseProtoMessageValueProvider.java"], + tags = [ + ], + deps = [ + "//common/annotations", + "//common/values:base_proto_cel_value_converter_android", + "//common/values:cel_value_provider_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 new file mode 100644 index 000000000..9fc218abe --- /dev/null +++ b/common/src/main/java/dev/cel/common/values/BaseProtoCelValueConverter.java @@ -0,0 +1,149 @@ +// 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.values; + +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.collect.ImmutableMap.toImmutableMap; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.primitives.UnsignedLong; +import com.google.errorprone.annotations.Immutable; +import com.google.protobuf.BoolValue; +import com.google.protobuf.ByteString; +import com.google.protobuf.BytesValue; +import com.google.protobuf.DoubleValue; +import com.google.protobuf.Duration; +import com.google.protobuf.FloatValue; +import com.google.protobuf.Int32Value; +import com.google.protobuf.Int64Value; +import com.google.protobuf.ListValue; +import com.google.protobuf.MessageLiteOrBuilder; +import com.google.protobuf.StringValue; +import com.google.protobuf.Struct; +import com.google.protobuf.Timestamp; +import com.google.protobuf.UInt32Value; +import com.google.protobuf.UInt64Value; +import com.google.protobuf.Value; +import dev.cel.common.annotations.Internal; +import dev.cel.common.internal.ProtoTimeUtils; +import dev.cel.common.internal.WellKnownProto; + +/** + * {@code BaseProtoCelValueConverter} contains the common logic for converting between native Java + * and protobuf objects to {@link CelValue}. This base class is inherited by {@code + * ProtoCelValueConverter} and {@code ProtoLiteCelValueConverter} to perform the conversion using + * full and lite variants of protobuf messages respectively. + * + *

CEL Library Internals. Do Not Use. + */ +@Immutable +@Internal +public abstract class BaseProtoCelValueConverter extends CelValueConverter { + + /** {@inheritDoc} Protobuf semantics take precedence for conversion. */ + @Override + public Object toRuntimeValue(Object value) { + Preconditions.checkNotNull(value); + + if (value instanceof ByteString) { + return CelByteString.of(((ByteString) value).toByteArray()); + } else if (value instanceof com.google.protobuf.NullValue) { + return NullValue.NULL_VALUE; + } + + return super.toRuntimeValue(value); + } + + protected Object fromWellKnownProto(MessageLiteOrBuilder message, WellKnownProto wellKnownProto) { + switch (wellKnownProto) { + case JSON_VALUE: + return adaptJsonValue((Value) message); + case JSON_STRUCT_VALUE: + return adaptJsonStruct((Struct) message); + case JSON_LIST_VALUE: + return adaptJsonList((ListValue) message); + case DURATION: + return ProtoTimeUtils.toJavaDuration((Duration) message); + case TIMESTAMP: + return ProtoTimeUtils.toJavaInstant((Timestamp) message); + case BOOL_VALUE: + return normalizePrimitive(((BoolValue) message).getValue()); + case BYTES_VALUE: + return normalizePrimitive(((BytesValue) message).getValue().toByteArray()); + case DOUBLE_VALUE: + return normalizePrimitive(((DoubleValue) message).getValue()); + case FLOAT_VALUE: + return normalizePrimitive(((FloatValue) message).getValue()); + case INT32_VALUE: + return normalizePrimitive(((Int32Value) message).getValue()); + case INT64_VALUE: + return normalizePrimitive(((Int64Value) message).getValue()); + case STRING_VALUE: + return normalizePrimitive(((StringValue) message).getValue()); + case UINT32_VALUE: + 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); + } + } + + private Object adaptJsonValue(Value value) { + switch (value.getKindCase()) { + case BOOL_VALUE: + return normalizePrimitive(value.getBoolValue()); + case NUMBER_VALUE: + return normalizePrimitive(value.getNumberValue()); + case STRING_VALUE: + return normalizePrimitive(value.getStringValue()); + case LIST_VALUE: + return adaptJsonList(value.getListValue()); + case STRUCT_VALUE: + return adaptJsonStruct(value.getStructValue()); + case NULL_VALUE: + case KIND_NOT_SET: // Fall-through is intended + return NullValue.NULL_VALUE; + } + throw new UnsupportedOperationException( + "Unsupported Json to CelValue conversion: " + value.getKindCase()); + } + + private ImmutableList adaptJsonList(ListValue listValue) { + return listValue.getValuesList().stream().map(this::adaptJsonValue).collect(toImmutableList()); + } + + private ImmutableMap adaptJsonStruct(Struct struct) { + return struct.getFieldsMap().entrySet().stream() + .collect( + toImmutableMap( + e -> { + Object key = toRuntimeValue(e.getKey()); + if (!(key instanceof String)) { + throw new IllegalStateException( + "Expected a string type for the key, but instead got: " + key); + } + return (String) key; + }, + e -> adaptJsonValue(e.getValue()))); + } + + protected BaseProtoCelValueConverter() {} +} diff --git a/common/src/main/java/dev/cel/common/values/BoolValue.java b/common/src/main/java/dev/cel/common/values/BaseProtoMessageValueProvider.java similarity index 54% rename from common/src/main/java/dev/cel/common/values/BoolValue.java rename to common/src/main/java/dev/cel/common/values/BaseProtoMessageValueProvider.java index 1327e5691..f42a16179 100644 --- a/common/src/main/java/dev/cel/common/values/BoolValue.java +++ b/common/src/main/java/dev/cel/common/values/BaseProtoMessageValueProvider.java @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// 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. @@ -14,30 +14,18 @@ package dev.cel.common.values; -import com.google.auto.value.AutoValue; import com.google.errorprone.annotations.Immutable; -import dev.cel.common.types.CelType; -import dev.cel.common.types.SimpleType; +import dev.cel.common.annotations.Internal; -/** BoolValue is a simple CelValue wrapper around Java booleans. */ -@AutoValue +/** + * {@code BaseProtoMessageValueProvider} is a common parent to {@code ProtoMessageValueProvider} and + * {@code ProtoMessageLiteValueProvider}. + * + *

CEL-Java internals. Do not use. Use one of the inherited variants mentioned above. + */ +@Internal @Immutable -public abstract class BoolValue extends CelValue { +public abstract class BaseProtoMessageValueProvider implements CelValueProvider { - @Override - public abstract Boolean value(); - - @Override - public boolean isZeroValue() { - return !value(); - } - - @Override - public CelType celType() { - return SimpleType.BOOL; - } - - public static BoolValue create(Boolean value) { - return new AutoValue_BoolValue(value); - } + public abstract BaseProtoCelValueConverter protoCelValueConverter(); } diff --git a/common/src/main/java/dev/cel/common/values/CelByteString.java b/common/src/main/java/dev/cel/common/values/CelByteString.java index 196af790b..b37fc778d 100644 --- a/common/src/main/java/dev/cel/common/values/CelByteString.java +++ b/common/src/main/java/dev/cel/common/values/CelByteString.java @@ -14,9 +14,14 @@ package dev.cel.common.values; -import com.google.common.base.Preconditions; import com.google.errorprone.annotations.Immutable; +import java.nio.ByteBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.Comparator; /** CelByteString is an immutable sequence of a byte array. */ @Immutable @@ -30,13 +35,41 @@ public final class CelByteString { private volatile int hash = 0; public static CelByteString of(byte[] buffer) { - Preconditions.checkNotNull(buffer); + if (buffer == null) { + throw new NullPointerException("buffer cannot be null"); + } if (buffer.length == 0) { return EMPTY; } return new CelByteString(buffer); } + public static CelByteString copyFromUtf8(String utf8String) { + return new CelByteString(utf8String.getBytes(StandardCharsets.UTF_8)); + } + + public static Comparator unsignedLexicographicalComparator() { + return UNSIGNED_LEXICOGRAPHICAL_COMPARATOR; + } + + public String toStringUtf8() { + return new String(data, StandardCharsets.UTF_8); + } + + /** Checks if the byte array is a valid utf-8 encoded text. */ + public boolean isValidUtf8() { + CharsetDecoder charsetDecoder = StandardCharsets.UTF_8.newDecoder(); + charsetDecoder.onMalformedInput(CodingErrorAction.REPORT); + charsetDecoder.onUnmappableCharacter(CodingErrorAction.REPORT); + + try { + charsetDecoder.decode(ByteBuffer.wrap(data)); + } catch (CharacterCodingException unused) { + return false; + } + return true; + } + public int size() { return data.length; } @@ -54,6 +87,14 @@ public byte[] toByteArray() { return Arrays.copyOf(data, size); } + public CelByteString concat(CelByteString other) { + byte[] result = new byte[data.length + other.data.length]; + System.arraycopy(data, 0, result, 0, data.length); + System.arraycopy(other.data, 0, result, data.length, other.data.length); + + return CelByteString.of(result); + } + private CelByteString(byte[] buffer) { data = Arrays.copyOf(buffer, buffer.length); } @@ -90,4 +131,29 @@ public int hashCode() { return hash; } + + @Override + public String toString() { + return toStringUtf8(); + } + + private static final Comparator UNSIGNED_LEXICOGRAPHICAL_COMPARATOR = + (former, latter) -> { + // Once we're on Java 9+, we can replace this whole thing with Arrays.compareUnsigned + byte[] formerBytes = former.toByteArray(); + byte[] latterBytes = latter.toByteArray(); + int minLength = Math.min(formerBytes.length, latterBytes.length); + + for (int i = 0; i < minLength; i++) { + int formerUnsigned = Byte.toUnsignedInt(formerBytes[i]); + int latterUnsigned = Byte.toUnsignedInt(latterBytes[i]); + int result = Integer.compare(formerUnsigned, latterUnsigned); + + if (result != 0) { + return result; + } + } + + return Integer.compare(formerBytes.length, latterBytes.length); + }; } 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 83275e3c1..20deef1d3 100644 --- a/common/src/main/java/dev/cel/common/values/CelValueConverter.java +++ b/common/src/main/java/dev/cel/common/values/CelValueConverter.java @@ -17,12 +17,15 @@ import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import com.google.common.primitives.UnsignedLong; -import dev.cel.common.CelOptions; +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 @@ -32,121 +35,169 @@ */ @SuppressWarnings("unchecked") // Unchecked cast of generics due to type-erasure (ex: MapValue). @Internal -abstract class CelValueConverter { +@Immutable +public class CelValueConverter { - protected final CelOptions celOptions; + private static final CelValueConverter DEFAULT_INSTANCE = new CelValueConverter(); - /** Adapts a {@link CelValue} to a plain old Java Object. */ - public Object fromCelValueToJavaObject(CelValue celValue) { - Preconditions.checkNotNull(celValue); + @SuppressWarnings("Immutable") // Method reference is immutable + private final Function maybeUnwrapFunction; - if (celValue instanceof MapValue) { - MapValue mapValue = (MapValue) celValue; - ImmutableMap.Builder mapBuilder = ImmutableMap.builder(); - for (Entry entry : mapValue.value().entrySet()) { - Object key = fromCelValueToJavaObject(entry.getKey()); - Object value = fromCelValueToJavaObject(entry.getValue()); - mapBuilder.put(key, value); - } + @SuppressWarnings("Immutable") // Method reference is immutable + private final Function toRuntimeValueFunction; + + public static CelValueConverter getDefaultInstance() { + return DEFAULT_INSTANCE; + } + + /** + * 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; + } - return mapBuilder.buildOrThrow(); - } else if (celValue instanceof ListValue) { - ListValue listValue = (ListValue) celValue; - ImmutableList.Builder listBuilder = ImmutableList.builder(); - for (CelValue element : listValue.value()) { - listBuilder.add(fromCelValueToJavaObject(element)); + 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 listBuilder.build(); - } else if (celValue instanceof OptionalValue) { - OptionalValue optionalValue = (OptionalValue) celValue; - if (optionalValue.isZeroValue()) { - return Optional.empty(); + + // Zero allocations if unmodified + return 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(); + } - return Optional.of(fromCelValueToJavaObject(optionalValue.value())); + 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 celValue.value(); + return value; } - /** Adapts a plain old Java Object to a {@link CelValue}. */ - public CelValue fromJavaObjectToCelValue(Object value) { + public Object toRuntimeValue(Object value) { Preconditions.checkNotNull(value); - if (value instanceof CelValue) { - return (CelValue) value; + if (value instanceof CelValue || value instanceof CelPreAdaptedList) { + return value; + } + + Object mapped = mapContainer(value, toRuntimeValueFunction); + if (mapped != value) { + return mapped; } - if (value instanceof Iterable) { - return toListValue((Iterable) value); - } else if (value instanceof Map) { - return toMapValue((Map) value); - } else if (value instanceof Optional) { - Optional optionalValue = (Optional) value; + if (value instanceof Optional) { + Optional optionalValue = (Optional) value; return optionalValue - .map(o -> OptionalValue.create(fromJavaObjectToCelValue(o))) + .map(toRuntimeValueFunction) + .map(OptionalValue::create) .orElse(OptionalValue.EMPTY); - } else if (value instanceof Exception) { - return ErrorValue.create((Exception) value); } - return fromJavaPrimitiveToCelValue(value); + return normalizePrimitive(value); } - /** Adapts a plain old Java Object that are considered primitives to a {@link CelValue}. */ - protected CelValue fromJavaPrimitiveToCelValue(Object value) { + protected Object normalizePrimitive(Object value) { Preconditions.checkNotNull(value); - if (value instanceof Boolean) { - return BoolValue.create((Boolean) value); - } else if (value instanceof Long) { - return IntValue.create((Long) value); - } else if (value instanceof Integer) { - return IntValue.create((Integer) value); - } else if (value instanceof String) { - return StringValue.create((String) value); + if (value instanceof Integer) { + return ((Integer) value).longValue(); } else if (value instanceof byte[]) { - return BytesValue.create(CelByteString.of((byte[]) value)); - } else if (value instanceof Double) { - return DoubleValue.create((Double) value); + return CelByteString.of((byte[]) value); } else if (value instanceof Float) { - return DoubleValue.create(Double.valueOf((Float) value)); - } else if (value instanceof UnsignedLong) { - return UintValue.create(((UnsignedLong) value).longValue(), celOptions.enableUnsignedLongs()); + return ((Float) value).doubleValue(); } - // Fall back to an Opaque value, as a custom class was supplied in the runtime. The legacy - // interpreter allows this but this should not be allowed when a new runtime is introduced. - // TODO: Migrate consumers to directly supply an appropriate CelValue. - return OpaqueValue.create(value.toString(), value); + return value; } - private ListValue toListValue(Iterable iterable) { - Preconditions.checkNotNull(iterable); - - ImmutableList.Builder listBuilder = ImmutableList.builder(); - for (Object entry : iterable) { - listBuilder.add(fromJavaObjectToCelValue(entry)); - } + /** Adapts a {@link CelValue} to a plain old Java Object. */ + private Object unwrap(CelValue celValue) { + Preconditions.checkNotNull(celValue); - return ImmutableListValue.create(listBuilder.build()); - } + if (celValue instanceof OptionalValue) { + OptionalValue optionalValue = (OptionalValue) celValue; + if (optionalValue.isZeroValue()) { + return Optional.empty(); + } - private MapValue toMapValue(Map map) { - Preconditions.checkNotNull(map); + return Optional.of(maybeUnwrap(optionalValue.value())); + } - ImmutableMap.Builder mapBuilder = ImmutableMap.builder(); - for (Entry entry : map.entrySet()) { - CelValue mapKey = fromJavaObjectToCelValue(entry.getKey()); - CelValue mapValue = fromJavaObjectToCelValue(entry.getValue()); - mapBuilder.put(mapKey, mapValue); + if (celValue instanceof ErrorValue) { + return celValue; } - return ImmutableMapValue.create(mapBuilder.buildOrThrow()); + return celValue.value(); } - protected CelValueConverter(CelOptions celOptions) { - Preconditions.checkNotNull(celOptions); - this.celOptions = celOptions; + protected CelValueConverter() { + this.maybeUnwrapFunction = this::maybeUnwrap; + this.toRuntimeValueFunction = this::toRuntimeValue; } } diff --git a/common/src/main/java/dev/cel/common/values/CelValueProvider.java b/common/src/main/java/dev/cel/common/values/CelValueProvider.java index 0e896e7ac..20ae865e7 100644 --- a/common/src/main/java/dev/cel/common/values/CelValueProvider.java +++ b/common/src/main/java/dev/cel/common/values/CelValueProvider.java @@ -14,8 +14,6 @@ package dev.cel.common.values; -import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.Immutable; import java.util.Map; import java.util.Optional; @@ -25,38 +23,12 @@ public interface CelValueProvider { /** - * Constructs a new struct value. - * - *

Note that the return type is defined as CelValue rather than StructValue to account for - * special cases such as wrappers where its primitive is returned. + * Constructs a new struct value, or a primitive value in case the fully qualified struct name is + * a wrapper. */ - Optional newValue(String structType, Map fields); + Optional newValue(String structType, Map fields); - /** - * The {@link CombinedCelValueProvider} takes one or more {@link CelValueProvider} instances and - * attempts to create a {@link CelValue} instance for a given struct type name by calling each - * value provider in the order that they are provided to the constructor. - */ - @Immutable - final class CombinedCelValueProvider implements CelValueProvider { - private final ImmutableList celValueProviders; - - public CombinedCelValueProvider(CelValueProvider first, CelValueProvider second) { - Preconditions.checkNotNull(first); - Preconditions.checkNotNull(second); - celValueProviders = ImmutableList.of(first, second); - } - - @Override - public Optional newValue(String structType, Map fields) { - for (CelValueProvider provider : celValueProviders) { - Optional newValue = provider.newValue(structType, fields); - if (newValue.isPresent()) { - return newValue; - } - } - - return Optional.empty(); - } + default CelValueConverter celValueConverter() { + return CelValueConverter.getDefaultInstance(); } } 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 new file mode 100644 index 000000000..d51c3afce --- /dev/null +++ b/common/src/main/java/dev/cel/common/values/CombinedCelValueProvider.java @@ -0,0 +1,69 @@ +// 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.values; + +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; +import java.util.Map; +import java.util.Optional; + +/** + * The {@link CombinedCelValueProvider} takes one or more {@link CelValueProvider} instances and + * attempts to create a {@link CelValue} instance for a given struct type name by calling each value + * provider in the order that they are provided to the constructor. + */ +@Immutable +public final class CombinedCelValueProvider implements CelValueProvider { + private final ImmutableList celValueProviders; + + /** Combines the provided first and second {@link CelValueProvider}. */ + public static CombinedCelValueProvider combine(CelValueProvider... providers) { + checkArgument(providers.length >= 2, "You must provide two or more providers"); + return new CombinedCelValueProvider(ImmutableList.copyOf(providers)); + } + + @Override + public Optional newValue(String structType, Map fields) { + for (CelValueProvider provider : celValueProviders) { + Optional newValue = provider.newValue(structType, fields); + if (newValue.isPresent()) { + return newValue; + } + } + + 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; + } + + private CombinedCelValueProvider(ImmutableList providers) { + celValueProviders = checkNotNull(providers); + } +} diff --git a/common/src/main/java/dev/cel/common/values/DoubleValue.java b/common/src/main/java/dev/cel/common/values/DoubleValue.java deleted file mode 100644 index 97e0c86b6..000000000 --- a/common/src/main/java/dev/cel/common/values/DoubleValue.java +++ /dev/null @@ -1,74 +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.common.values; - -import com.google.errorprone.annotations.Immutable; -import dev.cel.common.types.CelType; -import dev.cel.common.types.SimpleType; - -/** DoubleValue is a simple CelValue wrapper around Java doubles. */ -@Immutable -public final class DoubleValue extends CelValue { - private final double value; - - @Override - public Double value() { - return value; - } - - public double doubleValue() { - return value; - } - - @Override - public boolean isZeroValue() { - return value() == 0; - } - - @Override - public CelType celType() { - return SimpleType.DOUBLE; - } - - public static DoubleValue create(double value) { - return new DoubleValue(value); - } - - @Override - public int hashCode() { - int h = 1; - h *= 1000003; - h ^= Double.hashCode(value); - return h; - } - - @Override - public boolean equals(Object o) { - if (o == this) { - return true; - } - - if (!(o instanceof DoubleValue)) { - return false; - } - - return Double.doubleToLongBits(((DoubleValue) o).doubleValue()) - == Double.doubleToLongBits(this.doubleValue()); - } - - private DoubleValue(double value) { - this.value = value; - } -} diff --git a/common/src/main/java/dev/cel/common/values/DurationValue.java b/common/src/main/java/dev/cel/common/values/DurationValue.java deleted file mode 100644 index 702109de2..000000000 --- a/common/src/main/java/dev/cel/common/values/DurationValue.java +++ /dev/null @@ -1,44 +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.common.values; - -import com.google.auto.value.AutoValue; -import com.google.errorprone.annotations.Immutable; -import dev.cel.common.types.CelType; -import dev.cel.common.types.SimpleType; -import java.time.Duration; - -/** DurationValue is a simple CelValue wrapper around {@link java.time.Duration} */ -@AutoValue -@Immutable -public abstract class DurationValue extends CelValue { - - @Override - public abstract Duration value(); - - @Override - public boolean isZeroValue() { - return value().isZero(); - } - - @Override - public CelType celType() { - return SimpleType.DURATION; - } - - public static DurationValue create(Duration value) { - return new AutoValue_DurationValue(value); - } -} diff --git a/common/src/main/java/dev/cel/common/values/EnumValue.java b/common/src/main/java/dev/cel/common/values/EnumValue.java deleted file mode 100644 index 1a8ea3e7c..000000000 --- a/common/src/main/java/dev/cel/common/values/EnumValue.java +++ /dev/null @@ -1,49 +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.common.values; - -import com.google.auto.value.AutoValue; -import com.google.errorprone.annotations.Immutable; -import dev.cel.common.types.CelType; -import dev.cel.common.types.SimpleType; - -/** - * EnumValue is a simple CelValue wrapper around Java enums. - * - *

Note: CEL-Java currently does not support strongly typed enum. This value class will not be - * used until the said support is added in. - */ -@AutoValue -@Immutable(containerOf = "E") -public abstract class EnumValue> extends CelValue { - - @Override - public abstract Enum value(); - - @Override - public boolean isZeroValue() { - return false; - } - - @Override - public CelType celType() { - // (b/178627883) Strongly typed enum is not supported yet - return SimpleType.INT; - } - - public static > EnumValue create(Enum value) { - return new AutoValue_EnumValue<>(value); - } -} diff --git a/common/src/main/java/dev/cel/common/values/ErrorValue.java b/common/src/main/java/dev/cel/common/values/ErrorValue.java index 818f86850..6bc04cda4 100644 --- a/common/src/main/java/dev/cel/common/values/ErrorValue.java +++ b/common/src/main/java/dev/cel/common/values/ErrorValue.java @@ -33,6 +33,8 @@ "Immutable") // Exception is technically not immutable as the stacktrace is malleable. public abstract class ErrorValue extends CelValue { + public abstract long exprId(); + @Override public abstract Exception value(); @@ -46,7 +48,7 @@ public CelType celType() { return SimpleType.ERROR; } - public static ErrorValue create(Exception value) { - return new AutoValue_ErrorValue(value); + public static ErrorValue create(long exprId, Exception value) { + return new AutoValue_ErrorValue(exprId, value); } } diff --git a/common/src/main/java/dev/cel/common/values/ImmutableListValue.java b/common/src/main/java/dev/cel/common/values/ImmutableListValue.java deleted file mode 100644 index 1a0d6a0ce..000000000 --- a/common/src/main/java/dev/cel/common/values/ImmutableListValue.java +++ /dev/null @@ -1,134 +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.common.values; - -import com.google.common.collect.ImmutableList; -import com.google.errorprone.annotations.Immutable; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; -import java.util.ListIterator; - -/** - * ImmutableListValue is a representation of an immutable list containing zero or more {@link - * CelValue}. - */ -@Immutable -@SuppressWarnings( - "PreferredInterfaceType") // We intentionally store List type to avoid copying on instantiation -public final class ImmutableListValue extends ListValue { - - @SuppressWarnings("Immutable") // ListValue APIs prohibit mutation. - private final List originalList; - - @SuppressWarnings("Immutable") // The value is lazily populated only once via synchronization. - private volatile ImmutableList cachedImmutableList = null; - - public static ImmutableListValue create(List value) { - return new ImmutableListValue<>(value); - } - - private ImmutableListValue(List originalList) { - this.originalList = ImmutableList.copyOf(originalList); - } - - @Override - public ImmutableList value() { - if (cachedImmutableList == null) { - synchronized (this) { - if (cachedImmutableList == null) { - cachedImmutableList = ImmutableList.copyOf(originalList); - } - } - } - - return cachedImmutableList; - } - - @Override - public int size() { - return originalList.size(); - } - - @Override - public boolean isEmpty() { - return originalList.isEmpty(); - } - - @Override - public boolean contains(Object o) { - return originalList.contains(o); - } - - @Override - public Iterator iterator() { - return originalList.iterator(); - } - - @Override - public Object[] toArray() { - return originalList.toArray(); - } - - @Override - public T[] toArray(T[] a) { - return originalList.toArray(a); - } - - @Override - public boolean containsAll(Collection c) { - return originalList.containsAll(c); - } - - @Override - public E get(int index) { - return originalList.get(index); - } - - @Override - public int indexOf(Object o) { - return originalList.indexOf(o); - } - - @Override - public int lastIndexOf(Object o) { - return originalList.lastIndexOf(o); - } - - @Override - public ListIterator listIterator() { - return originalList.listIterator(); - } - - @Override - public ListIterator listIterator(int index) { - return originalList.listIterator(index); - } - - @Override - public List subList(int fromIndex, int toIndex) { - return originalList.subList(fromIndex, toIndex); - } - - @Override - public int hashCode() { - return originalList.hashCode(); - } - - @Override - public boolean equals(Object obj) { - return originalList.equals(obj); - } -} diff --git a/common/src/main/java/dev/cel/common/values/ImmutableMapValue.java b/common/src/main/java/dev/cel/common/values/ImmutableMapValue.java deleted file mode 100644 index eb429a19d..000000000 --- a/common/src/main/java/dev/cel/common/values/ImmutableMapValue.java +++ /dev/null @@ -1,109 +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.common.values; - -import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableMap; -import com.google.errorprone.annotations.Immutable; -import java.util.Collection; -import java.util.Map; -import java.util.Set; - -/** - * MapValue is an abstract representation of an immutable map containing {@link CelValue} as keys - * and values. - */ -@Immutable(containerOf = {"K", "V"}) -@SuppressWarnings( - "PreferredInterfaceType") // We intentionally store List type to avoid copying on instantiation -public final class ImmutableMapValue - extends MapValue { - - @SuppressWarnings("Immutable") // MapValue APIs prohibit mutation. - private final Map originalMap; - - @SuppressWarnings("Immutable") // The value is lazily populated only once via synchronization. - private volatile ImmutableMap cachedImmutableMap = null; - - public static ImmutableMapValue create( - Map value) { - return new ImmutableMapValue<>(value); - } - - private ImmutableMapValue(Map originalMap) { - Preconditions.checkNotNull(originalMap); - this.originalMap = originalMap; - } - - @Override - public ImmutableMap value() { - if (cachedImmutableMap == null) { - synchronized (this) { - if (cachedImmutableMap == null) { - cachedImmutableMap = ImmutableMap.copyOf(originalMap); - } - } - } - - return cachedImmutableMap; - } - - @Override - public int size() { - return originalMap.size(); - } - - @Override - public boolean isEmpty() { - return originalMap.isEmpty(); - } - - @Override - public boolean containsKey(Object key) { - return originalMap.containsKey(key); - } - - @Override - public boolean containsValue(Object val) { - return originalMap.containsValue(val); - } - - @Override - public int hashCode() { - return originalMap.hashCode(); - } - - @Override - public boolean equals(Object obj) { - return originalMap.equals(obj); - } - - // Note that the following three methods are produced from the immutable map to avoid key/value - // mutation. - @Override - public Set keySet() { - return value().keySet(); - } - - @Override - public Collection values() { - return value().values(); - } - - @Override - public Set> entrySet() { - return value().entrySet(); - } -} diff --git a/common/src/main/java/dev/cel/common/values/IntValue.java b/common/src/main/java/dev/cel/common/values/IntValue.java deleted file mode 100644 index b756804f6..000000000 --- a/common/src/main/java/dev/cel/common/values/IntValue.java +++ /dev/null @@ -1,73 +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.common.values; - -import com.google.errorprone.annotations.Immutable; -import dev.cel.common.types.CelType; -import dev.cel.common.types.SimpleType; - -/** IntValue is a simple CelValue wrapper around Java longs. */ -@Immutable -public final class IntValue extends CelValue { - private final long value; - - @Override - public Long value() { - return value; - } - - public long longValue() { - return value; - } - - @Override - public boolean isZeroValue() { - return value() == 0; - } - - @Override - public CelType celType() { - return SimpleType.INT; - } - - public static IntValue create(long value) { - return new IntValue(value); - } - - @Override - public int hashCode() { - int h = 1; - h *= 1000003; - h ^= Long.hashCode(value); - return h; - } - - @Override - public boolean equals(Object o) { - if (o == this) { - return true; - } - - if (!(o instanceof IntValue)) { - return false; - } - - return ((IntValue) o).value == this.value; - } - - private IntValue(long value) { - this.value = value; - } -} diff --git a/common/src/main/java/dev/cel/common/values/ListValue.java b/common/src/main/java/dev/cel/common/values/ListValue.java deleted file mode 100644 index 9cf6aa806..000000000 --- a/common/src/main/java/dev/cel/common/values/ListValue.java +++ /dev/null @@ -1,211 +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.common.values; - -import com.google.errorprone.annotations.CanIgnoreReturnValue; -import com.google.errorprone.annotations.DoNotCall; -import com.google.errorprone.annotations.Immutable; -import dev.cel.common.types.CelType; -import dev.cel.common.types.ListType; -import dev.cel.common.types.SimpleType; -import java.util.Collection; -import java.util.Comparator; -import java.util.List; -import java.util.function.UnaryOperator; -import org.jspecify.nullness.Nullable; - -/** - * ListValue is an abstract representation of a generic list containing zero or more {@link - * CelValue}. - * - *

All methods that can mutate the list are disallowed. - */ -@Immutable -public abstract class ListValue extends CelValue implements List { - private static final ListType LIST_TYPE = ListType.create(SimpleType.DYN); - - @Override - @SuppressWarnings("Immutable") // ListValue APIs prohibit mutation. - public abstract List value(); - - @Override - public boolean isZeroValue() { - return isEmpty(); - } - - @Override - public CelType celType() { - return LIST_TYPE; - } - - /** - * Guaranteed to throw an exception and leave the list unmodified. - * - * @throws UnsupportedOperationException always - * @deprecated Unsupported operation. - */ - @Deprecated - @Override - @DoNotCall("Always throws UnsupportedOperationException") - public final void clear() { - throw new UnsupportedOperationException(); - } - - /** - * Guaranteed to throw an exception and leave the list unmodified. - * - * @throws UnsupportedOperationException always - * @deprecated Unsupported operation. - */ - @Deprecated - @Override - @DoNotCall("Always throws UnsupportedOperationException") - public final boolean retainAll(Collection c) { - return false; - } - - /** - * Guaranteed to throw an exception and leave the list unmodified. - * - * @throws UnsupportedOperationException always - * @deprecated Unsupported operation. - */ - @CanIgnoreReturnValue - @Deprecated - @Override - @DoNotCall("Always throws UnsupportedOperationException") - public final E set(int index, E element) { - throw new UnsupportedOperationException(); - } - - /** - * Guaranteed to throw an exception and leave the list unmodified. - * - * @throws UnsupportedOperationException always - * @deprecated Unsupported operation. - */ - @Deprecated - @Override - @DoNotCall("Always throws UnsupportedOperationException") - public final void replaceAll(UnaryOperator operator) { - throw new UnsupportedOperationException(); - } - - /** - * Guaranteed to throw an exception and leave the list unmodified. - * - * @throws UnsupportedOperationException always - * @deprecated Unsupported operation. - */ - @Deprecated - @Override - @DoNotCall("Always throws UnsupportedOperationException") - public final void sort(@Nullable Comparator c) { - throw new UnsupportedOperationException(); - } - - /** - * Guaranteed to throw an exception and leave the list unmodified. - * - * @throws UnsupportedOperationException always - * @deprecated Unsupported operation. - */ - @Deprecated - @Override - @DoNotCall("Always throws UnsupportedOperationException") - public final void add(int index, E element) { - throw new UnsupportedOperationException(); - } - - /** - * Guaranteed to throw an exception and leave the list unmodified. - * - * @throws UnsupportedOperationException always - * @deprecated Unsupported operation. - */ - @Deprecated - @Override - @DoNotCall("Always throws UnsupportedOperationException") - public final boolean add(E e) { - throw new UnsupportedOperationException(); - } - - /** - * Guaranteed to throw an exception and leave the list unmodified. - * - * @throws UnsupportedOperationException always - * @deprecated Unsupported operation. - */ - @CanIgnoreReturnValue - @Deprecated - @Override - @DoNotCall("Always throws UnsupportedOperationException") - public final boolean addAll(int index, Collection newElements) { - throw new UnsupportedOperationException(); - } - - /** - * Guaranteed to throw an exception and leave the list unmodified. - * - * @throws UnsupportedOperationException always - * @deprecated Unsupported operation. - */ - @Deprecated - @Override - @DoNotCall("Always throws UnsupportedOperationException") - public final boolean addAll(Collection c) { - throw new UnsupportedOperationException(); - } - - /** - * Guaranteed to throw an exception and leave the list unmodified. - * - * @throws UnsupportedOperationException always - * @deprecated Unsupported operation. - */ - @CanIgnoreReturnValue - @Deprecated - @Override - @DoNotCall("Always throws UnsupportedOperationException") - public final E remove(int index) { - throw new UnsupportedOperationException(); - } - - /** - * Guaranteed to throw an exception and leave the list unmodified. - * - * @throws UnsupportedOperationException always - * @deprecated Unsupported operation. - */ - @Deprecated - @Override - @DoNotCall("Always throws UnsupportedOperationException") - public final boolean remove(Object o) { - throw new UnsupportedOperationException(); - } - - /** - * Guaranteed to throw an exception and leave the list unmodified. - * - * @throws UnsupportedOperationException always - * @deprecated Unsupported operation. - */ - @Deprecated - @Override - @DoNotCall("Always throws UnsupportedOperationException") - public final boolean removeAll(Collection c) { - throw new UnsupportedOperationException(); - } -} diff --git a/common/src/main/java/dev/cel/common/values/MapValue.java b/common/src/main/java/dev/cel/common/values/MapValue.java deleted file mode 100644 index ff1bb3f56..000000000 --- a/common/src/main/java/dev/cel/common/values/MapValue.java +++ /dev/null @@ -1,253 +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.common.values; - -import com.google.errorprone.annotations.CanIgnoreReturnValue; -import com.google.errorprone.annotations.DoNotCall; -import com.google.errorprone.annotations.Immutable; -import dev.cel.common.CelErrorCode; -import dev.cel.common.CelRuntimeException; -import dev.cel.common.types.CelType; -import dev.cel.common.types.MapType; -import dev.cel.common.types.SimpleType; -import java.util.Map; -import java.util.Optional; -import java.util.function.BiFunction; -import java.util.function.Function; -import org.jspecify.nullness.Nullable; - -/** - * MapValue is an abstract representation of a generic map containing {@link CelValue} as keys and - * values. - * - *

All methods that can mutate the map are disallowed. - */ -@Immutable(containerOf = {"K", "V"}) -public abstract class MapValue extends CelValue - implements Map, SelectableValue { - - private static final MapType MAP_TYPE = MapType.create(SimpleType.DYN, SimpleType.DYN); - - @Override - public abstract Map value(); - - @Override - public boolean isZeroValue() { - return isEmpty(); - } - - @Override - @SuppressWarnings("unchecked") - public V get(Object key) { - return select((K) key); - } - - @Override - @SuppressWarnings("unchecked") - public V select(K field) { - return (V) - find(field) - .orElseThrow( - () -> - new CelRuntimeException( - new IllegalArgumentException( - String.format("key '%s' is not present in map.", field.value())), - CelErrorCode.ATTRIBUTE_NOT_FOUND)); - } - - @Override - public Optional find(K field) { - return value().containsKey(field) ? Optional.of(value().get(field)) : Optional.empty(); - } - - @Override - public CelType celType() { - return MAP_TYPE; - } - - /** - * Guaranteed to throw an exception and leave the map unmodified. - * - * @throws UnsupportedOperationException always - * @deprecated Unsupported operation. - */ - @CanIgnoreReturnValue - @Deprecated - @Override - @DoNotCall("Always throws UnsupportedOperationException") - public final V put(K key, V value) { - throw new UnsupportedOperationException(); - } - - /** - * Guaranteed to throw an exception and leave the map unmodified. - * - * @throws UnsupportedOperationException always - * @deprecated Unsupported operation. - */ - @CanIgnoreReturnValue - @Deprecated - @Override - @DoNotCall("Always throws UnsupportedOperationException") - public final V putIfAbsent(K key, V value) { - throw new UnsupportedOperationException(); - } - - /** - * Guaranteed to throw an exception and leave the map unmodified. - * - * @throws UnsupportedOperationException always - * @deprecated Unsupported operation. - */ - @Deprecated - @Override - @DoNotCall("Always throws UnsupportedOperationException") - public final boolean replace(K key, V oldValue, V newValue) { - throw new UnsupportedOperationException(); - } - - /** - * Guaranteed to throw an exception and leave the map unmodified. - * - * @throws UnsupportedOperationException always - * @deprecated Unsupported operation. - */ - @Deprecated - @Override - @DoNotCall("Always throws UnsupportedOperationException") - public final V replace(K key, V value) { - throw new UnsupportedOperationException(); - } - - /** - * Guaranteed to throw an exception and leave the map unmodified. - * - * @throws UnsupportedOperationException always - * @deprecated Unsupported operation. - */ - @Deprecated - @Override - @DoNotCall("Always throws UnsupportedOperationException") - public final V computeIfAbsent(K key, Function mappingFunction) { - throw new UnsupportedOperationException(); - } - - /** - * Guaranteed to throw an exception and leave the map unmodified. - * - * @throws UnsupportedOperationException always - * @deprecated Unsupported operation. - */ - @Deprecated - @Override - @DoNotCall("Always throws UnsupportedOperationException") - public final V computeIfPresent( - K key, BiFunction remappingFunction) { - throw new UnsupportedOperationException(); - } - - /** - * Guaranteed to throw an exception and leave the map unmodified. - * - * @throws UnsupportedOperationException always - * @deprecated Unsupported operation. - */ - @Deprecated - @Override - @DoNotCall("Always throws UnsupportedOperationException") - public final V compute( - K key, BiFunction remappingFunction) { - throw new UnsupportedOperationException(); - } - - /** - * Guaranteed to throw an exception and leave the map unmodified. - * - * @throws UnsupportedOperationException always - * @deprecated Unsupported operation. - */ - @Deprecated - @Override - @DoNotCall("Always throws UnsupportedOperationException") - public final V merge( - K key, V value, BiFunction function) { - throw new UnsupportedOperationException(); - } - - /** - * Guaranteed to throw an exception and leave the map unmodified. - * - * @throws UnsupportedOperationException always - * @deprecated Unsupported operation. - */ - @Deprecated - @Override - @DoNotCall("Always throws UnsupportedOperationException") - public final void putAll(Map map) { - throw new UnsupportedOperationException(); - } - - /** - * Guaranteed to throw an exception and leave the map unmodified. - * - * @throws UnsupportedOperationException always - * @deprecated Unsupported operation. - */ - @Deprecated - @Override - @DoNotCall("Always throws UnsupportedOperationException") - public final void replaceAll(BiFunction function) { - throw new UnsupportedOperationException(); - } - - /** - * Guaranteed to throw an exception and leave the map unmodified. - * - * @throws UnsupportedOperationException always - * @deprecated Unsupported operation. - */ - @Deprecated - @Override - @DoNotCall("Always throws UnsupportedOperationException") - public final V remove(Object o) { - throw new UnsupportedOperationException(); - } - - /** - * Guaranteed to throw an exception and leave the map unmodified. - * - * @throws UnsupportedOperationException always - * @deprecated Unsupported operation. - */ - @Deprecated - @Override - @DoNotCall("Always throws UnsupportedOperationException") - public final boolean remove(Object key, Object value) { - throw new UnsupportedOperationException(); - } - - /** - * Guaranteed to throw an exception and leave the map unmodified. - * - * @throws UnsupportedOperationException always - * @deprecated Unsupported operation. - */ - @Deprecated - @Override - @DoNotCall("Always throws UnsupportedOperationException") - public final void clear() { - throw new UnsupportedOperationException(); - } -} 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/NullValue.java b/common/src/main/java/dev/cel/common/values/NullValue.java index 320d85b9c..ca35c1550 100644 --- a/common/src/main/java/dev/cel/common/values/NullValue.java +++ b/common/src/main/java/dev/cel/common/values/NullValue.java @@ -43,5 +43,10 @@ public boolean isZeroValue() { return true; } + @Override + public String toString() { + return "NULL_VALUE"; + } + private NullValue() {} } 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/OptionalValue.java b/common/src/main/java/dev/cel/common/values/OptionalValue.java index 45270ebb2..9a2204fca 100644 --- a/common/src/main/java/dev/cel/common/values/OptionalValue.java +++ b/common/src/main/java/dev/cel/common/values/OptionalValue.java @@ -19,9 +19,10 @@ import com.google.errorprone.annotations.Immutable; import dev.cel.common.types.OptionalType; import dev.cel.common.types.SimpleType; +import java.util.Map; import java.util.NoSuchElementException; import java.util.Optional; -import org.jspecify.nullness.Nullable; +import org.jspecify.annotations.Nullable; /** * First-class support for CEL optionals. Supports similar semantics to java.util.Optional. Also @@ -30,12 +31,11 @@ */ @AutoValue @Immutable(containerOf = "E") -public abstract class OptionalValue extends CelValue - implements SelectableValue { +public abstract class OptionalValue extends CelValue implements SelectableValue { private static final OptionalType OPTIONAL_TYPE = OptionalType.create(SimpleType.DYN); /** Sentinel value representing an empty optional ('optional.none()' in CEL) */ - public static final OptionalValue EMPTY = empty(); + public static final OptionalValue EMPTY = empty(); // There is only one scenario where the value is null and it's `optional.none`. abstract @Nullable E innerValue(); @@ -68,28 +68,40 @@ public OptionalType celType() { * */ @Override - public CelValue select(CelValue field) { + public OptionalValue select(T field) { return find(field).orElse(EMPTY); } @Override @SuppressWarnings("unchecked") - public Optional find(CelValue field) { + public Optional> find(T field) { if (isZeroValue()) { return Optional.empty(); } - SelectableValue selectableValue = (SelectableValue) value(); - Optional selectedField = selectableValue.find(field); - return selectedField.map(OptionalValue::create); + E value = value(); + if (value instanceof Map) { + Map map = (Map) value; + Object selectedVal = map.get(field); + if (selectedVal == null) { + return Optional.empty(); + } + + return Optional.of(OptionalValue.create((E) selectedVal)); + } else if (value instanceof SelectableValue) { + SelectableValue selectableValue = (SelectableValue) value; + return selectableValue.find(field).map(OptionalValue::create); + } + + return Optional.empty(); } - public static OptionalValue create(E value) { + public static OptionalValue create(E value) { Preconditions.checkNotNull(value); return new AutoValue_OptionalValue<>(value); } - private static OptionalValue empty() { + private static OptionalValue empty() { return new AutoValue_OptionalValue<>(null); } } 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 cafa399f8..a4280e1ea 100644 --- a/common/src/main/java/dev/cel/common/values/ProtoCelValueConverter.java +++ b/common/src/main/java/dev/cel/common/values/ProtoCelValueConverter.java @@ -14,50 +14,31 @@ package dev.cel.common.values; -import static com.google.common.collect.ImmutableList.toImmutableList; -import static com.google.common.collect.ImmutableMap.toImmutableMap; -import static com.google.common.math.LongMath.checkedAdd; -import static com.google.common.math.LongMath.checkedSubtract; - import com.google.common.base.Preconditions; +import com.google.common.primitives.UnsignedLong; import com.google.errorprone.annotations.Immutable; import com.google.protobuf.Any; -import com.google.protobuf.BoolValue; -import com.google.protobuf.ByteString; import com.google.protobuf.Descriptors.EnumValueDescriptor; import com.google.protobuf.Descriptors.FieldDescriptor; -import com.google.protobuf.DoubleValue; import com.google.protobuf.DynamicMessage; -import com.google.protobuf.FloatValue; -import com.google.protobuf.Int32Value; -import com.google.protobuf.Int64Value; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.MapEntry; import com.google.protobuf.Message; +import com.google.protobuf.MessageLiteOrBuilder; import com.google.protobuf.MessageOrBuilder; -import com.google.protobuf.StringValue; -import com.google.protobuf.Struct; -import com.google.protobuf.Timestamp; -import com.google.protobuf.UInt32Value; -import com.google.protobuf.UInt64Value; -import com.google.protobuf.Value; -import com.google.protobuf.util.Durations; -import com.google.protobuf.util.Timestamps; import dev.cel.common.CelOptions; import dev.cel.common.annotations.Internal; import dev.cel.common.internal.CelDescriptorPool; import dev.cel.common.internal.DynamicProto; import dev.cel.common.internal.WellKnownProto; -import dev.cel.common.types.CelTypes; -import java.time.Duration; -import java.time.Instant; import java.util.HashMap; import java.util.List; import java.util.Map; /** - * {@code CelValueConverter} handles bidirectional conversion between native Java and protobuf - * objects to {@link CelValue}. + * {@code ProtoCelValueConverter} handles bidirectional conversion between native Java and protobuf + * objects to {@link CelValue}. This converter leverages descriptors, thus requires the full version + * of protobuf implementation. * *

Protobuf semantics take precedence for conversion. For example, CEL's TimestampValue will be * converted into Protobuf's Timestamp instead of java.time.Instant. @@ -66,255 +47,153 @@ */ @Immutable @Internal -public final class ProtoCelValueConverter extends CelValueConverter { +public final class ProtoCelValueConverter extends BaseProtoCelValueConverter { private final CelDescriptorPool celDescriptorPool; private final DynamicProto dynamicProto; + private final CelOptions celOptions; /** Constructs a new instance of ProtoCelValueConverter. */ public static ProtoCelValueConverter newInstance( - CelOptions celOptions, CelDescriptorPool celDescriptorPool, DynamicProto dynamicProto) { - return new ProtoCelValueConverter(celOptions, celDescriptorPool, dynamicProto); + CelDescriptorPool celDescriptorPool, DynamicProto dynamicProto, CelOptions celOptions) { + return new ProtoCelValueConverter(celDescriptorPool, dynamicProto, celOptions); } - /** - * Adapts a {@link CelValue} to a native Java object. The CelValue is adapted into protobuf object - * when an equivalent exists. - */ @Override - public Object fromCelValueToJavaObject(CelValue celValue) { - Preconditions.checkNotNull(celValue); - - if (celValue instanceof TimestampValue) { - return TimeUtils.toProtoTimestamp(((TimestampValue) celValue).value()); - } else if (celValue instanceof DurationValue) { - return TimeUtils.toProtoDuration(((DurationValue) celValue).value()); - } else if (celValue instanceof BytesValue) { - return ByteString.copyFrom(((BytesValue) celValue).value().toByteArray()); - } else if (NullValue.NULL_VALUE.equals(celValue)) { - return com.google.protobuf.NullValue.NULL_VALUE; - } - - return super.fromCelValueToJavaObject(celValue); - } - - /** Adapts a Protobuf message into a {@link CelValue}. */ - public CelValue fromProtoMessageToCelValue(MessageOrBuilder message) { - Preconditions.checkNotNull(message); - - // Attempt to convert the proto from a dynamic message into a concrete message if possible. - if (message instanceof DynamicMessage) { - message = dynamicProto.maybeAdaptDynamicMessage((DynamicMessage) message); - } - - WellKnownProto wellKnownProto = - WellKnownProto.getByDescriptorName(message.getDescriptorForType().getFullName()); - if (wellKnownProto == null) { - return ProtoMessageValue.create((Message) message, celDescriptorPool, this); - } - + protected Object fromWellKnownProto(MessageLiteOrBuilder msg, WellKnownProto wellKnownProto) { + MessageOrBuilder message = (MessageOrBuilder) msg; switch (wellKnownProto) { case ANY_VALUE: Message unpackedMessage; try { unpackedMessage = dynamicProto.unpack((Any) message); } catch (InvalidProtocolBufferException e) { - throw new IllegalStateException( + throw new IllegalArgumentException( "Unpacking failed for message: " + message.getDescriptorForType().getFullName(), e); } - return fromProtoMessageToCelValue(unpackedMessage); - case JSON_VALUE: - return adaptJsonValueToCelValue((Value) message); - case JSON_STRUCT_VALUE: - return adaptJsonStructToCelValue((Struct) message); - case JSON_LIST_VALUE: - return adaptJsonListToCelValue((com.google.protobuf.ListValue) message); - case DURATION_VALUE: - return DurationValue.create( - TimeUtils.toJavaDuration((com.google.protobuf.Duration) message)); - case TIMESTAMP_VALUE: - return TimestampValue.create(TimeUtils.toJavaInstant((Timestamp) message)); - case BOOL_VALUE: - return fromJavaPrimitiveToCelValue(((BoolValue) message).getValue()); - case BYTES_VALUE: - return fromJavaPrimitiveToCelValue( - ((com.google.protobuf.BytesValue) message).getValue().toByteArray()); - case DOUBLE_VALUE: - return fromJavaPrimitiveToCelValue(((DoubleValue) message).getValue()); - case FLOAT_VALUE: - return fromJavaPrimitiveToCelValue(((FloatValue) message).getValue()); - case INT32_VALUE: - return fromJavaPrimitiveToCelValue(((Int32Value) message).getValue()); - case INT64_VALUE: - return fromJavaPrimitiveToCelValue(((Int64Value) message).getValue()); - case STRING_VALUE: - return fromJavaPrimitiveToCelValue(((StringValue) message).getValue()); - case UINT32_VALUE: - return UintValue.create( - ((UInt32Value) message).getValue(), celOptions.enableUnsignedLongs()); - case UINT64_VALUE: - return UintValue.create( - ((UInt64Value) message).getValue(), celOptions.enableUnsignedLongs()); + return toRuntimeValue(unpackedMessage); + case FIELD_MASK: + return ProtoMessageValue.create( + (Message) message, celDescriptorPool, this, celOptions.enableJsonFieldNames()); + default: + return super.fromWellKnownProto(message, wellKnownProto); } - - throw new UnsupportedOperationException( - "Unsupported message to CelValue conversion - " + message); } - /** - * Adapts a plain old Java Object to a {@link CelValue}. Protobuf semantics take precedence for - * conversion. - */ @Override - public CelValue fromJavaObjectToCelValue(Object value) { - Preconditions.checkNotNull(value); - - if (value instanceof Message) { - return fromProtoMessageToCelValue((Message) value); - } else if (value instanceof Message.Builder) { - Message.Builder msgBuilder = (Message.Builder) value; - return fromProtoMessageToCelValue(msgBuilder.build()); - } else if (value instanceof ByteString) { - return BytesValue.create(CelByteString.of(((ByteString) value).toByteArray())); - } else if (value instanceof com.google.protobuf.NullValue) { - return NullValue.NULL_VALUE; - } else if (value instanceof EnumValueDescriptor) { + public Object toRuntimeValue(Object value) { + if (value instanceof EnumValueDescriptor) { // (b/178627883) Strongly typed enum is not supported yet - return IntValue.create(((EnumValueDescriptor) value).getNumber()); + return Long.valueOf(((EnumValueDescriptor) value).getNumber()); } - return super.fromJavaObjectToCelValue(value); + if (value instanceof MessageOrBuilder) { + Message message; + if (value instanceof Message.Builder) { + message = ((Message.Builder) value).build(); + } else { + message = (Message) value; + } + + // Attempt to convert the proto from a dynamic message into a concrete message if possible. + if (message instanceof DynamicMessage) { + message = dynamicProto.maybeAdaptDynamicMessage((DynamicMessage) message); + } + + WellKnownProto wellKnownProto = + WellKnownProto.getByTypeName(message.getDescriptorForType().getFullName()).orElse(null); + if (wellKnownProto == null) { + return ProtoMessageValue.create( + message, celDescriptorPool, this, celOptions.enableJsonFieldNames()); + } + + return fromWellKnownProto(message, wellKnownProto); + } + + return super.toRuntimeValue(value); } - /** Adapts the protobuf message field into {@link CelValue}. */ - @SuppressWarnings("unchecked") - public CelValue fromProtoMessageFieldToCelValue( - Message message, FieldDescriptor fieldDescriptor) { + /** Adapts the protobuf message field. */ + @SuppressWarnings({"unchecked", "rawtypes"}) + public Object fromProtoMessageFieldToCelValue(Message message, FieldDescriptor fieldDescriptor) { Preconditions.checkNotNull(message); Preconditions.checkNotNull(fieldDescriptor); Object result = message.getField(fieldDescriptor); switch (fieldDescriptor.getType()) { case MESSAGE: - if (CelTypes.isWrapperType(fieldDescriptor.getMessageType().getFullName()) + if (WellKnownProto.isWrapperType(fieldDescriptor.getMessageType().getFullName()) + && !fieldDescriptor.isRepeated() && !message.hasField(fieldDescriptor)) { // Special semantics for wrapper types per CEL specification. These all convert into null // instead of the default value. return NullValue.NULL_VALUE; - } else if (fieldDescriptor.isMapField()) { + } + + if (fieldDescriptor.isMapField()) { Map map = new HashMap<>(); - for (MapEntry entry : ((List>) result)) { - map.put(entry.getKey(), entry.getValue()); + Object mapKey; + Object mapValue; + for (Object entry : ((List) result)) { + if (entry instanceof MapEntry) { + MapEntry mapEntry = (MapEntry) entry; + mapKey = mapEntry.getKey(); + mapValue = mapEntry.getValue(); + } else if (entry instanceof DynamicMessage) { + DynamicMessage dynamicMessage = (DynamicMessage) entry; + FieldDescriptor keyFieldDescriptor = + fieldDescriptor.getMessageType().findFieldByNumber(1); + FieldDescriptor valueFieldDescriptor = + fieldDescriptor.getMessageType().findFieldByNumber(2); + mapKey = dynamicMessage.getField(keyFieldDescriptor); + mapValue = dynamicMessage.getField(valueFieldDescriptor); + } else { + throw new IllegalStateException("Unexpected map field type: " + entry); + } + + map.put(mapKey, mapValue); } - return fromJavaObjectToCelValue(map); + return toRuntimeValue(map); } - break; + + return toRuntimeValue(result); case UINT32: - return UintValue.create((int) result, celOptions.enableUnsignedLongs()); + case FIXED32: + if (!fieldDescriptor.isRepeated()) { + return UnsignedLong.valueOf((int) result); + } + break; case UINT64: - return UintValue.create((long) result, celOptions.enableUnsignedLongs()); + case FIXED64: + if (!fieldDescriptor.isRepeated()) { + return UnsignedLong.fromLongBits((long) result); + } + break; default: break; } - return fromJavaObjectToCelValue(result); - } - - private CelValue adaptJsonValueToCelValue(Value value) { - switch (value.getKindCase()) { - case BOOL_VALUE: - return fromJavaPrimitiveToCelValue(value.getBoolValue()); - case NUMBER_VALUE: - return fromJavaPrimitiveToCelValue(value.getNumberValue()); - case STRING_VALUE: - return fromJavaPrimitiveToCelValue(value.getStringValue()); - case LIST_VALUE: - return adaptJsonListToCelValue(value.getListValue()); - case STRUCT_VALUE: - return adaptJsonStructToCelValue(value.getStructValue()); - case NULL_VALUE: - case KIND_NOT_SET: // Fall-through is intended - return NullValue.NULL_VALUE; - } - throw new UnsupportedOperationException( - "Unsupported Json to CelValue conversion: " + value.getKindCase()); - } - - private ListValue adaptJsonListToCelValue(com.google.protobuf.ListValue listValue) { - return ImmutableListValue.create( - listValue.getValuesList().stream() - .map(this::adaptJsonValueToCelValue) - .collect(toImmutableList())); - } - - private MapValue adaptJsonStructToCelValue(Struct struct) { - return ImmutableMapValue.create( - struct.getFieldsMap().entrySet().stream() - .collect( - toImmutableMap( - e -> fromJavaObjectToCelValue(e.getKey()), - e -> adaptJsonValueToCelValue(e.getValue())))); - } - - /** Helper to convert between java.util.time and protobuf duration/timestamp. */ - private static class TimeUtils { - private static final int NANOS_PER_SECOND = 1000000000; - - private static Instant toJavaInstant(Timestamp timestamp) { - timestamp = normalizedTimestamp(timestamp.getSeconds(), timestamp.getNanos()); - return Instant.ofEpochSecond(timestamp.getSeconds(), timestamp.getNanos()); - } - - private static Duration toJavaDuration(com.google.protobuf.Duration duration) { - duration = normalizedDuration(duration.getSeconds(), duration.getNanos()); - return java.time.Duration.ofSeconds(duration.getSeconds(), duration.getNanos()); - } - - private static Timestamp toProtoTimestamp(Instant instant) { - return normalizedTimestamp(instant.getEpochSecond(), instant.getNano()); - } - - private static com.google.protobuf.Duration toProtoDuration(Duration duration) { - return normalizedDuration(duration.getSeconds(), duration.getNano()); - } - - private static Timestamp normalizedTimestamp(long seconds, int nanos) { - if (nanos <= -NANOS_PER_SECOND || nanos >= NANOS_PER_SECOND) { - seconds = checkedAdd(seconds, nanos / NANOS_PER_SECOND); - nanos = nanos % NANOS_PER_SECOND; - } - if (nanos < 0) { - nanos = nanos + NANOS_PER_SECOND; // no overflow since nanos is negative (and we're adding) - seconds = checkedSubtract(seconds, 1); + if (fieldDescriptor.isRepeated()) { + switch (fieldDescriptor.getType()) { + case INT64: + case BOOL: + case STRING: + case DOUBLE: + return CelPreAdaptedList.wrap((List) result); + default: + break; } - Timestamp timestamp = Timestamp.newBuilder().setSeconds(seconds).setNanos(nanos).build(); - return Timestamps.checkValid(timestamp); } - private static com.google.protobuf.Duration normalizedDuration(long seconds, int nanos) { - if (nanos <= -NANOS_PER_SECOND || nanos >= NANOS_PER_SECOND) { - seconds = checkedAdd(seconds, nanos / NANOS_PER_SECOND); - nanos %= NANOS_PER_SECOND; - } - if (seconds > 0 && nanos < 0) { - nanos += NANOS_PER_SECOND; // no overflow since nanos is negative (and we're adding) - seconds--; // no overflow since seconds is positive (and we're decrementing) - } - if (seconds < 0 && nanos > 0) { - nanos -= NANOS_PER_SECOND; // no overflow since nanos is positive (and we're subtracting) - seconds++; // no overflow since seconds is negative (and we're incrementing) - } - com.google.protobuf.Duration duration = - com.google.protobuf.Duration.newBuilder().setSeconds(seconds).setNanos(nanos).build(); - return Durations.checkValid(duration); - } + return toRuntimeValue(result); } private ProtoCelValueConverter( - CelOptions celOptions, CelDescriptorPool celDescriptorPool, DynamicProto dynamicProto) { - super(celOptions); + CelDescriptorPool celDescriptorPool, DynamicProto dynamicProto, CelOptions celOptions) { Preconditions.checkNotNull(celDescriptorPool); Preconditions.checkNotNull(dynamicProto); + Preconditions.checkNotNull(celOptions); this.celDescriptorPool = celDescriptorPool; this.dynamicProto = dynamicProto; + this.celOptions = celOptions; } } diff --git a/common/src/main/java/dev/cel/common/values/ProtoLiteCelValueConverter.java b/common/src/main/java/dev/cel/common/values/ProtoLiteCelValueConverter.java new file mode 100644 index 000000000..64d6ec1d4 --- /dev/null +++ b/common/src/main/java/dev/cel/common/values/ProtoLiteCelValueConverter.java @@ -0,0 +1,412 @@ +// 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.values; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.auto.value.AutoValue; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Defaults; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Multimap; +import com.google.common.collect.Multimaps; +import com.google.common.primitives.UnsignedLong; +import com.google.errorprone.annotations.Immutable; +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; +import dev.cel.common.internal.WellKnownProto; +import dev.cel.protobuf.CelLiteDescriptor.FieldLiteDescriptor; +import dev.cel.protobuf.CelLiteDescriptor.FieldLiteDescriptor.EncodingType; +import dev.cel.protobuf.CelLiteDescriptor.FieldLiteDescriptor.JavaType; +import dev.cel.protobuf.CelLiteDescriptor.MessageLiteDescriptor; +import java.io.IOException; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.TreeMap; + +/** + * {@code ProtoLiteCelValueConverter} handles bidirectional conversion between native Java and + * protobuf objects to {@link CelValue}. This converter is specifically designed for use with + * lite-variants of protobuf messages. + * + *

Protobuf semantics take precedence for conversion. For example, CEL's TimestampValue will be + * converted into Protobuf's Timestamp instead of java.time.Instant. + * + *

CEL Library Internals. Do Not Use. + */ +@Immutable +@Internal +public final class ProtoLiteCelValueConverter extends BaseProtoCelValueConverter { + private final CelLiteDescriptorPool descriptorPool; + + public static ProtoLiteCelValueConverter newInstance( + CelLiteDescriptorPool celLiteDescriptorPool) { + return new ProtoLiteCelValueConverter(celLiteDescriptorPool); + } + + private static Object readPrimitiveField( + CodedInputStream inputStream, FieldLiteDescriptor fieldDescriptor) throws IOException { + switch (fieldDescriptor.getProtoFieldType()) { + case SINT32: + return inputStream.readSInt32(); + case SINT64: + return inputStream.readSInt64(); + case INT32: + case ENUM: + return inputStream.readInt32(); + case INT64: + return inputStream.readInt64(); + case UINT32: + return UnsignedLong.fromLongBits(inputStream.readUInt32()); + case UINT64: + return UnsignedLong.fromLongBits(inputStream.readUInt64()); + case BOOL: + return inputStream.readBool(); + case FLOAT: + case FIXED32: + case SFIXED32: + return readFixed32BitField(inputStream, fieldDescriptor); + case DOUBLE: + case FIXED64: + case SFIXED64: + return readFixed64BitField(inputStream, fieldDescriptor); + default: + throw new IllegalStateException( + "Unexpected field type: " + fieldDescriptor.getProtoFieldType()); + } + } + + private static Object readFixed32BitField( + CodedInputStream inputStream, FieldLiteDescriptor fieldDescriptor) throws IOException { + switch (fieldDescriptor.getProtoFieldType()) { + case FLOAT: + return inputStream.readFloat(); + case FIXED32: + case SFIXED32: + return inputStream.readRawLittleEndian32(); + default: + throw new IllegalStateException( + "Unexpected field type: " + fieldDescriptor.getProtoFieldType()); + } + } + + private static Object readFixed64BitField( + CodedInputStream inputStream, FieldLiteDescriptor fieldDescriptor) throws IOException { + switch (fieldDescriptor.getProtoFieldType()) { + case DOUBLE: + return inputStream.readDouble(); + case FIXED64: + case SFIXED64: + return inputStream.readRawLittleEndian64(); + default: + throw new IllegalStateException( + "Unexpected field type: " + fieldDescriptor.getProtoFieldType()); + } + } + + private Object readLengthDelimitedField( + CodedInputStream inputStream, FieldLiteDescriptor fieldDescriptor) throws IOException { + FieldLiteDescriptor.Type fieldType = fieldDescriptor.getProtoFieldType(); + + switch (fieldType) { + case BYTES: + return inputStream.readBytes(); + case MESSAGE: + MessageLite.Builder builder = + getDefaultMessageBuilder(fieldDescriptor.getFieldProtoTypeName()); + + inputStream.readMessage(builder, ExtensionRegistryLite.getEmptyRegistry()); + return builder.build(); + case STRING: + return inputStream.readStringRequireUtf8(); + default: + throw new IllegalStateException("Unexpected field type: " + fieldType); + } + } + + private MessageLite.Builder getDefaultMessageBuilder(String protoTypeName) { + return descriptorPool.getDescriptorOrThrow(protoTypeName).newMessageBuilder(); + } + + Object getDefaultCelValue(String protoTypeName, String fieldName) { + MessageLiteDescriptor messageDescriptor = descriptorPool.getDescriptorOrThrow(protoTypeName); + FieldLiteDescriptor fieldDescriptor = messageDescriptor.getByFieldNameOrThrow(fieldName); + + Object defaultValue = getDefaultValue(fieldDescriptor); + + return toRuntimeValue(defaultValue); + } + + @Override + @SuppressWarnings("LiteProtoToString") // No alternative identifier to use. Debug only info is OK. + public Object toRuntimeValue(Object value) { + checkNotNull(value); + if (value instanceof MessageLite) { + MessageLite msg = (MessageLite) value; + + MessageLiteDescriptor descriptor = + descriptorPool + .findDescriptor(msg) + .orElseThrow( + () -> new NoSuchElementException("Could not find a descriptor for: " + msg)); + WellKnownProto wellKnownProto = + WellKnownProto.getByTypeName(descriptor.getProtoTypeName()).orElse(null); + + if (wellKnownProto == null) { + return ProtoMessageLiteValue.create(msg, descriptor.getProtoTypeName(), this); + } + + 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) { + case LIST: + return ImmutableList.of(); + case MAP: + return ImmutableMap.of(); + case SINGULAR: + return getScalarDefaultValue(fieldDescriptor); + } + throw new IllegalStateException("Unexpected encoding type: " + encodingType); + } + + private Object getScalarDefaultValue(FieldLiteDescriptor fieldDescriptor) { + JavaType type = fieldDescriptor.getJavaType(); + switch (type) { + case INT: + return fieldDescriptor.getProtoFieldType().equals(FieldLiteDescriptor.Type.UINT32) + ? UnsignedLong.ZERO + : Defaults.defaultValue(long.class); + case LONG: + return fieldDescriptor.getProtoFieldType().equals(FieldLiteDescriptor.Type.UINT64) + ? UnsignedLong.ZERO + : Defaults.defaultValue(long.class); + case ENUM: + return Defaults.defaultValue(long.class); + case FLOAT: + return Defaults.defaultValue(float.class); + case DOUBLE: + return Defaults.defaultValue(double.class); + case BOOLEAN: + return Defaults.defaultValue(boolean.class); + case STRING: + return ""; + case BYTE_STRING: + return CelByteString.EMPTY; + case MESSAGE: + if (WellKnownProto.isWrapperType(fieldDescriptor.getFieldProtoTypeName())) { + return NullValue.NULL_VALUE; + } + + return getDefaultMessageBuilder(fieldDescriptor.getFieldProtoTypeName()).build(); + } + throw new IllegalStateException("Unexpected java type: " + type); + } + + private ImmutableList readPackedRepeatedFields( + CodedInputStream inputStream, FieldLiteDescriptor fieldDescriptor) throws IOException { + int length = inputStream.readInt32(); + int oldLimit = inputStream.pushLimit(length); + ImmutableList.Builder builder = ImmutableList.builder(); + while (inputStream.getBytesUntilLimit() > 0) { + builder.add(readPrimitiveField(inputStream, fieldDescriptor)); + } + inputStream.popLimit(oldLimit); + return builder.build(); + } + + private Map.Entry readSingleMapEntry( + CodedInputStream inputStream, FieldLiteDescriptor fieldDescriptor) throws IOException { + ImmutableMap singleMapEntry = + readAllFields(inputStream.readByteArray(), fieldDescriptor.getFieldProtoTypeName()) + .values(); + Object key = checkNotNull(singleMapEntry.get("key")); + Object value = checkNotNull(singleMapEntry.get("value")); + + return new AbstractMap.SimpleEntry<>(key, value); + } + + @VisibleForTesting + MessageFields readAllFields(byte[] bytes, String protoTypeName) throws IOException { + MessageLiteDescriptor messageDescriptor = descriptorPool.getDescriptorOrThrow(protoTypeName); + CodedInputStream inputStream = CodedInputStream.newInstance(bytes); + + Multimap unknownFields = + Multimaps.newMultimap(new TreeMap<>(), ArrayList::new); + ImmutableMap.Builder fieldValues = ImmutableMap.builder(); + Map> repeatedFieldValues = new LinkedHashMap<>(); + Map> mapFieldValues = new LinkedHashMap<>(); + for (int tag = inputStream.readTag(); tag != 0; tag = inputStream.readTag()) { + int tagWireType = WireFormat.getTagWireType(tag); + int fieldNumber = WireFormat.getTagFieldNumber(tag); + FieldLiteDescriptor fieldDescriptor = + messageDescriptor.findByFieldNumber(fieldNumber).orElse(null); + if (fieldDescriptor == null) { + unknownFields.put(fieldNumber, readUnknownField(tagWireType, inputStream)); + continue; + } + + Object payload; + switch (tagWireType) { + case WireFormat.WIRETYPE_VARINT: + payload = readPrimitiveField(inputStream, fieldDescriptor); + break; + case WireFormat.WIRETYPE_FIXED32: + payload = readFixed32BitField(inputStream, fieldDescriptor); + break; + case WireFormat.WIRETYPE_FIXED64: + payload = readFixed64BitField(inputStream, fieldDescriptor); + break; + case WireFormat.WIRETYPE_LENGTH_DELIMITED: + EncodingType encodingType = fieldDescriptor.getEncodingType(); + switch (encodingType) { + case LIST: + if (fieldDescriptor.getIsPacked()) { + payload = readPackedRepeatedFields(inputStream, fieldDescriptor); + } else { + FieldLiteDescriptor.Type protoFieldType = fieldDescriptor.getProtoFieldType(); + boolean isLenDelimited = + protoFieldType.equals(FieldLiteDescriptor.Type.MESSAGE) + || protoFieldType.equals(FieldLiteDescriptor.Type.STRING) + || protoFieldType.equals(FieldLiteDescriptor.Type.BYTES); + if (!isLenDelimited) { + throw new IllegalStateException( + "Unexpected field type encountered for LEN-Delimited record: " + + protoFieldType); + } + + payload = readLengthDelimitedField(inputStream, fieldDescriptor); + } + break; + case MAP: + Map fieldMap = + mapFieldValues.computeIfAbsent(fieldNumber, (unused) -> new LinkedHashMap<>()); + Map.Entry mapEntry = readSingleMapEntry(inputStream, fieldDescriptor); + fieldMap.put(mapEntry.getKey(), mapEntry.getValue()); + payload = fieldMap; + break; + default: + payload = readLengthDelimitedField(inputStream, fieldDescriptor); + break; + } + break; + case WireFormat.WIRETYPE_START_GROUP: + case WireFormat.WIRETYPE_END_GROUP: + // TODO: Support groups + throw new UnsupportedOperationException("Groups are not supported"); + default: + throw new IllegalArgumentException("Unexpected wire type: " + tagWireType); + } + + if (fieldDescriptor.getEncodingType().equals(EncodingType.LIST)) { + String fieldName = fieldDescriptor.getFieldName(); + List repeatedValues = + repeatedFieldValues.computeIfAbsent( + fieldNumber, + (unused) -> { + List newList = new ArrayList<>(); + fieldValues.put(fieldName, newList); + return newList; + }); + + if (payload instanceof Collection) { + repeatedValues.addAll((Collection) payload); + } else { + repeatedValues.add(payload); + } + } else { + fieldValues.put(fieldDescriptor.getFieldName(), payload); + } + } + + // Protobuf encoding follows a "last one wins" semantics. This means for duplicated fields, + // we accept the last value encountered. + return MessageFields.create(fieldValues.buildKeepingLast(), unknownFields); + } + + ImmutableMap readAllFields(MessageLite msg, String protoTypeName) + throws IOException { + return readAllFields(msg.toByteArray(), protoTypeName).values(); + } + + private static Object readUnknownField(int tagWireType, CodedInputStream inputStream) + throws IOException { + switch (tagWireType) { + case WireFormat.WIRETYPE_VARINT: + return inputStream.readInt64(); + case WireFormat.WIRETYPE_FIXED64: + return inputStream.readFixed64(); + case WireFormat.WIRETYPE_LENGTH_DELIMITED: + return inputStream.readBytes(); + case WireFormat.WIRETYPE_FIXED32: + return inputStream.readFixed32(); + case WireFormat.WIRETYPE_START_GROUP: + case WireFormat.WIRETYPE_END_GROUP: + // TODO: Support groups + throw new UnsupportedOperationException("Groups are not supported"); + default: + throw new IllegalArgumentException("Unknown wire type: " + tagWireType); + } + } + + @AutoValue + @SuppressWarnings("AutoValueImmutableFields") // Unknowns are inaccessible to users. + abstract static class MessageFields { + + abstract ImmutableMap values(); + + abstract Multimap unknowns(); + + static MessageFields create( + ImmutableMap fieldValues, Multimap unknownFields) { + return new AutoValue_ProtoLiteCelValueConverter_MessageFields(fieldValues, unknownFields); + } + } + + private ProtoLiteCelValueConverter(CelLiteDescriptorPool celLiteDescriptorPool) { + this.descriptorPool = celLiteDescriptorPool; + } +} diff --git a/common/src/main/java/dev/cel/common/values/ProtoMessageLiteValue.java b/common/src/main/java/dev/cel/common/values/ProtoMessageLiteValue.java new file mode 100644 index 000000000..2e4d980c7 --- /dev/null +++ b/common/src/main/java/dev/cel/common/values/ProtoMessageLiteValue.java @@ -0,0 +1,82 @@ +// 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.values; + +import com.google.auto.value.AutoValue; +import com.google.auto.value.extension.memoized.Memoized; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; +import com.google.errorprone.annotations.Immutable; +import com.google.protobuf.MessageLite; +import dev.cel.common.types.CelType; +import dev.cel.common.types.StructTypeReference; +import java.io.IOException; +import java.util.Optional; + +/** + * ProtoMessageLiteValue is a struct value with protobuf support for {@link MessageLite}. + * Specifically, it does not rely on full message descriptors, thus field selections can be + * performed without the reliance of proto-reflection. + * + *

If the codebase has access to full protobuf messages with descriptors, use {@code + * ProtoMessageValue} instead. + */ +@AutoValue +@Immutable +public abstract class ProtoMessageLiteValue extends StructValue { + + @Override + public abstract MessageLite value(); + + @Override + public abstract CelType celType(); + + abstract ProtoLiteCelValueConverter protoLiteCelValueConverter(); + + @Memoized + ImmutableMap fieldValues() { + try { + return protoLiteCelValueConverter().readAllFields(value(), celType().name()); + } catch (IOException e) { + throw new IllegalStateException("Unable to read message fields for " + celType().name(), e); + } + } + + @Override + public boolean isZeroValue() { + return value().getDefaultInstanceForType().equals(value()); + } + + @Override + public Object select(String field) { + return find(field) + .orElseGet(() -> protoLiteCelValueConverter().getDefaultCelValue(celType().name(), field)); + } + + @Override + public Optional find(String field) { + Object fieldValue = fieldValues().get(field); + return Optional.ofNullable(fieldValue) + .map(value -> protoLiteCelValueConverter().toRuntimeValue(fieldValue)); + } + + public static ProtoMessageLiteValue create( + MessageLite value, String typeName, ProtoLiteCelValueConverter protoLiteCelValueConverter) { + Preconditions.checkNotNull(value); + Preconditions.checkNotNull(typeName); + return new AutoValue_ProtoMessageLiteValue( + value, StructTypeReference.create(typeName), protoLiteCelValueConverter); + } +} diff --git a/common/src/main/java/dev/cel/common/values/ProtoMessageLiteValueProvider.java b/common/src/main/java/dev/cel/common/values/ProtoMessageLiteValueProvider.java new file mode 100644 index 000000000..041d850a6 --- /dev/null +++ b/common/src/main/java/dev/cel/common/values/ProtoMessageLiteValueProvider.java @@ -0,0 +1,78 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.common.values; + +import com.google.common.collect.ImmutableSet; +import com.google.errorprone.annotations.Immutable; +import com.google.protobuf.MessageLite; +import dev.cel.common.annotations.Beta; +import dev.cel.common.internal.CelLiteDescriptorPool; +import dev.cel.common.internal.DefaultLiteDescriptorPool; +import dev.cel.protobuf.CelLiteDescriptor; +import dev.cel.protobuf.CelLiteDescriptor.MessageLiteDescriptor; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +/** + * {@code ProtoMessageValueProvider} constructs new instances of protobuf lite-message given its + * fully qualified name and its fields to populate. + */ +@Immutable +@Beta +public class ProtoMessageLiteValueProvider extends BaseProtoMessageValueProvider { + private final CelLiteDescriptorPool descriptorPool; + private final ProtoLiteCelValueConverter protoLiteCelValueConverter; + + @Override + public BaseProtoCelValueConverter protoCelValueConverter() { + return protoLiteCelValueConverter; + } + + @Override + public Optional newValue(String structType, Map fields) { + MessageLiteDescriptor descriptor = descriptorPool.findDescriptor(structType).orElse(null); + if (descriptor == null) { + return Optional.empty(); + } + + if (!fields.isEmpty()) { + // TODO: Add support for this + throw new UnsupportedOperationException( + "Message creation with prepopulated fields is not supported yet."); + } + + MessageLite message = descriptor.newMessageBuilder().build(); + return Optional.of(protoLiteCelValueConverter.toRuntimeValue(message)); + } + + public static ProtoMessageLiteValueProvider newInstance(CelLiteDescriptor... descriptors) { + return newInstance(ImmutableSet.copyOf(descriptors)); + } + + public static ProtoMessageLiteValueProvider newInstance(Set descriptors) { + DefaultLiteDescriptorPool descriptorPool = + DefaultLiteDescriptorPool.newInstance(ImmutableSet.copyOf(descriptors)); + ProtoLiteCelValueConverter protoLiteCelValueConverter = + ProtoLiteCelValueConverter.newInstance(descriptorPool); + return new ProtoMessageLiteValueProvider(protoLiteCelValueConverter, descriptorPool); + } + + private ProtoMessageLiteValueProvider( + ProtoLiteCelValueConverter protoLiteCelValueConverter, CelLiteDescriptorPool descriptorPool) { + this.protoLiteCelValueConverter = protoLiteCelValueConverter; + this.descriptorPool = descriptorPool; + } +} 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 f9abdbea1..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(); @@ -40,23 +40,25 @@ public abstract class ProtoMessageValue extends StructValue { abstract ProtoCelValueConverter protoCelValueConverter(); + abstract boolean enableJsonFieldNames(); + @Override public boolean isZeroValue() { return value().getDefaultInstanceForType().equals(value()); } @Override - public CelValue select(StringValue field) { + public Object select(String field) { FieldDescriptor fieldDescriptor = - findField(celDescriptorPool(), value().getDescriptorForType(), field.value()); + findField(celDescriptorPool(), value().getDescriptorForType(), field); return protoCelValueConverter().fromProtoMessageFieldToCelValue(value(), fieldDescriptor); } @Override - public Optional find(StringValue field) { + public Optional find(String field) { FieldDescriptor fieldDescriptor = - findField(celDescriptorPool(), value().getDescriptorForType(), field.value()); + findField(celDescriptorPool(), value().getDescriptorForType(), field); // Selecting a field on a protobuf message yields a default value even if the field is not // declared. Therefore, we must exhaustively test whether they are actually declared. @@ -75,7 +77,8 @@ public Optional find(StringValue field) { public static ProtoMessageValue create( Message value, CelDescriptorPool celDescriptorPool, - ProtoCelValueConverter protoCelValueConverter) { + ProtoCelValueConverter protoCelValueConverter, + boolean enableJsonFieldNames) { Preconditions.checkNotNull(value); Preconditions.checkNotNull(celDescriptorPool); Preconditions.checkNotNull(protoCelValueConverter); @@ -83,11 +86,20 @@ public static ProtoMessageValue create( value, StructTypeReference.create(value.getDescriptorForType().getFullName()), celDescriptorPool, - protoCelValueConverter); + protoCelValueConverter, + enableJsonFieldNames); } private FieldDescriptor findField( CelDescriptorPool celDescriptorPool, Descriptor descriptor, String fieldName) { + if (enableJsonFieldNames()) { + for (FieldDescriptor fd : descriptor.getFields()) { + if (fd.getJsonName().equals(fieldName)) { + return fd; + } + } + } + FieldDescriptor fieldDescriptor = descriptor.findFieldByName(fieldName); if (fieldDescriptor != null) { return fieldDescriptor; 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 430328596..7beb40c61 100644 --- a/common/src/main/java/dev/cel/common/values/ProtoMessageValueProvider.java +++ b/common/src/main/java/dev/cel/common/values/ProtoMessageValueProvider.java @@ -18,9 +18,7 @@ import com.google.protobuf.Descriptors.Descriptor; import com.google.protobuf.Descriptors.FieldDescriptor; import com.google.protobuf.Message; -import dev.cel.common.CelErrorCode; import dev.cel.common.CelOptions; -import dev.cel.common.CelRuntimeException; import dev.cel.common.annotations.Internal; import dev.cel.common.internal.DynamicProto; import dev.cel.common.internal.ProtoAdapter; @@ -40,11 +38,20 @@ public class ProtoMessageValueProvider implements CelValueProvider { private final ProtoAdapter protoAdapter; private final ProtoMessageFactory protoMessageFactory; private final ProtoCelValueConverter protoCelValueConverter; + private final CelOptions celOptions; @Override - public Optional newValue(String structType, Map fields) { + public CelValueConverter celValueConverter() { + return protoCelValueConverter; + } + + @Override + public Optional newValue(String structType, Map fields) { + Message.Builder builder = protoMessageFactory.newBuilder(structType).orElse(null); + if (builder == null) { + return Optional.empty(); + } try { - Message.Builder builder = getMessageBuilderOrThrow(structType); Descriptor descriptor = builder.getDescriptorForType(); for (Map.Entry entry : fields.entrySet()) { FieldDescriptor fieldDescriptor = findField(descriptor, entry.getKey()); @@ -54,24 +61,21 @@ public Optional newValue(String structType, Map fields fieldValue.ifPresent(o -> builder.setField(fieldDescriptor, o)); } - return Optional.of(protoCelValueConverter.fromProtoMessageToCelValue(builder.build())); + return Optional.of(protoCelValueConverter.toRuntimeValue(builder.build())); } catch (ArrayIndexOutOfBoundsException e) { throw new IllegalArgumentException(e.getMessage()); } } - private Message.Builder getMessageBuilderOrThrow(String messageName) { - return protoMessageFactory - .newBuilder(messageName) - .orElseThrow( - () -> - new CelRuntimeException( - new IllegalArgumentException( - String.format("cannot resolve '%s' as a message", messageName)), - CelErrorCode.ATTRIBUTE_NOT_FOUND)); - } - private FieldDescriptor findField(Descriptor descriptor, String fieldName) { + if (celOptions.enableJsonFieldNames()) { + for (FieldDescriptor fd : descriptor.getFields()) { + if (fd.getJsonName().equals(fieldName)) { + return fd; + } + } + } + FieldDescriptor fieldDescriptor = descriptor.findFieldByName(fieldName); if (fieldDescriptor != null) { return fieldDescriptor; @@ -89,15 +93,16 @@ private FieldDescriptor findField(Descriptor descriptor, String fieldName) { } public static ProtoMessageValueProvider newInstance( - DynamicProto dynamicProto, CelOptions celOptions) { - return new ProtoMessageValueProvider(dynamicProto, celOptions); + CelOptions celOptions, DynamicProto dynamicProto) { + return new ProtoMessageValueProvider(celOptions, dynamicProto); } - private ProtoMessageValueProvider(DynamicProto dynamicProto, CelOptions celOptions) { + private ProtoMessageValueProvider(CelOptions celOptions, DynamicProto dynamicProto) { this.protoMessageFactory = dynamicProto.getProtoMessageFactory(); this.protoCelValueConverter = ProtoCelValueConverter.newInstance( - celOptions, protoMessageFactory.getDescriptorPool(), dynamicProto); - this.protoAdapter = new ProtoAdapter(dynamicProto, celOptions.enableUnsignedLongs()); + protoMessageFactory.getDescriptorPool(), dynamicProto, celOptions); + this.protoAdapter = new ProtoAdapter(dynamicProto, celOptions); + this.celOptions = celOptions; } } diff --git a/common/src/main/java/dev/cel/common/values/SelectableValue.java b/common/src/main/java/dev/cel/common/values/SelectableValue.java index 5fa0a8939..4db43d144 100644 --- a/common/src/main/java/dev/cel/common/values/SelectableValue.java +++ b/common/src/main/java/dev/cel/common/values/SelectableValue.java @@ -20,18 +20,18 @@ * SelectableValue is an interface for representing a value that supports field selection and * presence tests. Few examples are structs, protobuf messages, maps and optional values. */ -public interface SelectableValue { +public interface SelectableValue { /** * Performs field selection. The behavior depends on the concrete implementation of the value * being selected. For structs and maps, this must throw an exception if the field does not exist. * For optional values, this will return an {@code optional.none()}. */ - CelValue select(T field); + Object select(T field); /** * Finds the field. This will return an {@link Optional#empty()} if the field does not exist. This * can be used for presence testing. */ - Optional find(T field); + Optional find(T field); } 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 8a2351ded..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,14 +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/main/java/dev/cel/common/values/TimestampValue.java b/common/src/main/java/dev/cel/common/values/TimestampValue.java deleted file mode 100644 index a03ee2eda..000000000 --- a/common/src/main/java/dev/cel/common/values/TimestampValue.java +++ /dev/null @@ -1,44 +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.common.values; - -import com.google.auto.value.AutoValue; -import com.google.errorprone.annotations.Immutable; -import dev.cel.common.types.CelType; -import dev.cel.common.types.SimpleType; -import java.time.Instant; - -/** TimestampValue is a simple CelValue wrapper around {@link java.time.Instant} */ -@AutoValue -@Immutable -public abstract class TimestampValue extends CelValue { - - @Override - public abstract Instant value(); - - @Override - public boolean isZeroValue() { - return Instant.EPOCH.equals(value()); - } - - @Override - public CelType celType() { - return SimpleType.TIMESTAMP; - } - - public static TimestampValue create(Instant value) { - return new AutoValue_TimestampValue(value); - } -} diff --git a/common/src/main/java/dev/cel/common/values/UintValue.java b/common/src/main/java/dev/cel/common/values/UintValue.java deleted file mode 100644 index 71fcfae83..000000000 --- a/common/src/main/java/dev/cel/common/values/UintValue.java +++ /dev/null @@ -1,54 +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.common.values; - -import com.google.auto.value.AutoValue; -import com.google.common.primitives.UnsignedLong; -import com.google.errorprone.annotations.Immutable; -import dev.cel.common.types.CelType; -import dev.cel.common.types.SimpleType; - -/** - * UintValue represents CelValue for unsigned longs. This either leverages Guava's implementation of - * {@link UnsignedLong}, or just holds a primitive long. - */ -@Immutable -@AutoValue -@AutoValue.CopyAnnotations -@SuppressWarnings("Immutable") // value is either a boxed long or an immutable UnsignedLong. -public abstract class UintValue extends CelValue { - - @Override - public abstract Number value(); - - @Override - public boolean isZeroValue() { - return value().longValue() == 0; - } - - @Override - public CelType celType() { - return SimpleType.UINT; - } - - public static UintValue create(UnsignedLong value) { - return new AutoValue_UintValue(value); - } - - public static UintValue create(long value, boolean enableUnsignedLongs) { - Number unsignedLong = enableUnsignedLongs ? UnsignedLong.fromLongBits(value) : value; - return new AutoValue_UintValue(unsignedLong); - } -} diff --git a/common/src/main/resources/testdata/proto2/BUILD.bazel b/common/src/main/resources/testdata/proto2/BUILD.bazel deleted file mode 100644 index 6d374ad01..000000000 --- a/common/src/main/resources/testdata/proto2/BUILD.bazel +++ /dev/null @@ -1,59 +0,0 @@ -package( - default_applicable_licenses = [ - "//:license", - ], - default_testonly = True, - default_visibility = [ - "//common/resources/testdata/proto2:__pkg__", - ], -) - -proto_library( - name = "messages_proto2_proto", - srcs = [ - "messages_proto2.proto", - ], - deps = [ - ":test_all_types_proto", - ], -) - -java_proto_library( - name = "messages_proto2_java_proto", - deps = [":messages_proto2_proto"], -) - -proto_library( - name = "messages_extensions_proto2_proto", - srcs = [ - "messages_extensions_proto2.proto", - ], - deps = [ - ":messages_proto2_proto", - ":test_all_types_proto", - ], -) - -java_proto_library( - name = "messages_extensions_proto2_java_proto", - deps = [":messages_extensions_proto2_proto"], -) - -proto_library( - name = "test_all_types_proto", - srcs = [ - "test_all_types.proto", - ], - deps = [ - "@com_google_protobuf//:any_proto", - "@com_google_protobuf//:duration_proto", - "@com_google_protobuf//:struct_proto", - "@com_google_protobuf//:timestamp_proto", - "@com_google_protobuf//:wrappers_proto", - ], -) - -java_proto_library( - name = "test_all_types_java_proto", - deps = [":test_all_types_proto"], -) diff --git a/common/src/main/resources/testdata/proto2/messages_extensions_proto2.proto b/common/src/main/resources/testdata/proto2/messages_extensions_proto2.proto deleted file mode 100644 index d1c6ee06d..000000000 --- a/common/src/main/resources/testdata/proto2/messages_extensions_proto2.proto +++ /dev/null @@ -1,49 +0,0 @@ -// 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. - -syntax = "proto2"; - -import "common/src/main/resources/testdata/proto2/messages_proto2.proto"; -import "common/src/main/resources/testdata/proto2/test_all_types.proto"; - -package dev.cel.testing.testdata.proto2; - -option java_outer_classname = "MessagesProto2Extensions"; -option java_package = "dev.cel.testing.testdata.proto2"; -option java_multiple_files = true; - -// Package scoped extensions -extend Proto2Message { - optional Proto2Message nested_ext = 100; - optional int32 int32_ext = 101; - optional dev.cel.testing.testdata.proto2.TestAllTypes test_all_types_ext = - 102; - optional dev.cel.testing.testdata.proto2.TestAllTypes.NestedEnum - nested_enum_ext = 103; - repeated StringHolder repeated_string_holder_ext = 104; -} - -// Message scoped extensions -message Proto2ExtensionScopedMessage { - extend Proto2Message { - optional Proto2Message message_scoped_nested_ext = 105; - optional NestedMessageInsideExtensions nested_message_inside_ext = 106; - optional int64 int64_ext = 107; - optional string string_ext = 108; - } -} - -message NestedMessageInsideExtensions { - optional string field = 1; -} diff --git a/common/src/main/resources/testdata/proto2/messages_proto2.proto b/common/src/main/resources/testdata/proto2/messages_proto2.proto deleted file mode 100644 index 6f8f082e6..000000000 --- a/common/src/main/resources/testdata/proto2/messages_proto2.proto +++ /dev/null @@ -1,44 +0,0 @@ -// 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. - -// LINT: ALLOW_GROUPS -syntax = "proto2"; - -package dev.cel.testing.testdata.proto2; - -import "common/src/main/resources/testdata/proto2/test_all_types.proto"; - -option java_outer_classname = "MessagesProto2"; -option java_package = "dev.cel.testing.testdata.proto2"; -option java_multiple_files = true; - -message Proto2Message { - optional int32 single_int32 = 1; - optional fixed32 single_fixed32 = 2; - optional fixed64 single_fixed64 = 3; - optional dev.cel.testing.testdata.proto2.GlobalEnum single_enum = 4; - optional dev.cel.testing.testdata.proto2.NestedTestAllTypes - single_nested_test_all_types = 5; - - optional group NestedGroup = 6 { - optional int32 single_id = 7; - optional string single_name = 8; - } - - extensions 100 to max; -} - -message StringHolder { - optional string s = 1; -} diff --git a/common/src/main/resources/testdata/proto2/test_all_types.proto b/common/src/main/resources/testdata/proto2/test_all_types.proto deleted file mode 100644 index 7307b2970..000000000 --- a/common/src/main/resources/testdata/proto2/test_all_types.proto +++ /dev/null @@ -1,134 +0,0 @@ -// 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. - -// Keep this file synced with: -// https://github.com/google/cel-spec/blob/master/proto/test/v1/proto2/test_all_types.proto - -syntax = "proto2"; - -package dev.cel.testing.testdata.proto2; - -import "google/protobuf/any.proto"; -import "google/protobuf/duration.proto"; -import "google/protobuf/struct.proto"; -import "google/protobuf/timestamp.proto"; -import "google/protobuf/wrappers.proto"; - -option java_outer_classname = "TestAllTypesProto"; -option java_package = "dev.cel.testing.testdata.proto2"; - -// This proto includes every type of field in both singular and repeated -// forms. -message TestAllTypes { - message NestedMessage { - // The field name "b" fails to compile in proto1 because it conflicts with - // a local variable named "b" in one of the generated methods. - // This file needs to compile in proto1 to test backwards-compatibility. - optional int32 bb = 1; - } - - enum NestedEnum { - FOO = 0; - BAR = 1; - BAZ = 2; - } - - // Singular - optional int32 single_int32 = 1 [default = -32]; - optional int64 single_int64 = 2 [default = -64]; - optional uint32 single_uint32 = 3 [default = 32]; - optional uint64 single_uint64 = 4 [default = 64]; - optional sint32 single_sint32 = 5; - optional sint64 single_sint64 = 6; - optional fixed32 single_fixed32 = 7; - optional fixed64 single_fixed64 = 8; - optional sfixed32 single_sfixed32 = 9; - optional sfixed64 single_sfixed64 = 10; - optional float single_float = 11 [default = 3.0]; - optional double single_double = 12 [default = 6.4]; - optional bool single_bool = 13 [default = true]; - optional string single_string = 14 [default = "empty"]; - optional bytes single_bytes = 15 [default = "none"]; - - // Wellknown. - optional google.protobuf.Any single_any = 100; - optional google.protobuf.Duration single_duration = 101; - optional google.protobuf.Timestamp single_timestamp = 102; - optional google.protobuf.Struct single_struct = 103; - optional google.protobuf.Value single_value = 104; - optional google.protobuf.Int64Value single_int64_wrapper = 105; - optional google.protobuf.Int32Value single_int32_wrapper = 106; - optional google.protobuf.DoubleValue single_double_wrapper = 107; - optional google.protobuf.FloatValue single_float_wrapper = 108; - optional google.protobuf.UInt64Value single_uint64_wrapper = 109; - optional google.protobuf.UInt32Value single_uint32_wrapper = 110; - optional google.protobuf.StringValue single_string_wrapper = 111; - optional google.protobuf.BoolValue single_bool_wrapper = 112; - optional google.protobuf.BytesValue single_bytes_wrapper = 113; - optional google.protobuf.ListValue list_value = 114; - - // Nested messages - oneof nested_type { - NestedMessage single_nested_message = 21; - NestedEnum single_nested_enum = 22 [default = BAR]; - } - optional NestedMessage standalone_message = 23; - optional NestedEnum standalone_enum = 24; - - // Repeated - repeated int32 repeated_int32 = 31; - repeated int64 repeated_int64 = 32; - repeated uint32 repeated_uint32 = 33; - repeated uint64 repeated_uint64 = 34; - repeated sint32 repeated_sint32 = 35; - repeated sint64 repeated_sint64 = 36; - repeated fixed32 repeated_fixed32 = 37; - repeated fixed64 repeated_fixed64 = 38; - repeated sfixed32 repeated_sfixed32 = 39; - repeated sfixed64 repeated_sfixed64 = 40; - repeated float repeated_float = 41; - repeated double repeated_double = 42; - repeated bool repeated_bool = 43; - repeated string repeated_string = 44; - repeated bytes repeated_bytes = 45; - - // Repeated and nested - repeated NestedMessage repeated_nested_message = 51; - repeated NestedEnum repeated_nested_enum = 52; - repeated string repeated_string_piece = 53 [ctype = STRING_PIECE]; - repeated string repeated_cord = 54 [ctype = CORD]; - repeated NestedMessage repeated_lazy_message = 55; - - // Map - map map_string_string = 61; - map map_int64_nested_type = 62; -} - -// This proto includes a recursively nested message. -message NestedTestAllTypes { - optional NestedTestAllTypes child = 1; - optional TestAllTypes payload = 2; -} - -// This proto has a required field. -message TestRequired { - required int32 required_int32 = 1; -} - -// This proto tests that global enums are resolved correctly. -enum GlobalEnum { - GOO = 0; - GAR = 1; - GAZ = 2; -} diff --git a/common/src/main/resources/testdata/proto3/BUILD.bazel b/common/src/main/resources/testdata/proto3/BUILD.bazel index d76c8ae59..a7db554d5 100644 --- a/common/src/main/resources/testdata/proto3/BUILD.bazel +++ b/common/src/main/resources/testdata/proto3/BUILD.bazel @@ -1,3 +1,6 @@ +load("@com_google_protobuf//bazel:java_proto_library.bzl", "java_proto_library") +load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") + package( default_applicable_licenses = [ "//:license", @@ -8,25 +11,9 @@ package( ], ) -proto_library( - name = "test_all_types_proto", - srcs = [ - "test_all_types.proto", - ], - deps = [ - "@com_google_protobuf//:any_proto", - "@com_google_protobuf//:duration_proto", - "@com_google_protobuf//:struct_proto", - "@com_google_protobuf//:timestamp_proto", - "@com_google_protobuf//:wrappers_proto", - ], -) - -java_proto_library( - name = "test_all_types_java_proto", - tags = [ - ], - deps = [":test_all_types_proto"], +filegroup( + name = "test_all_types_file_descriptor_set", + srcs = ["test_all_types.fds"], ) proto_library( diff --git a/common/src/main/resources/testdata/proto3/test_all_types.fds b/common/src/main/resources/testdata/proto3/test_all_types.fds new file mode 100644 index 000000000..5a2b9778c --- /dev/null +++ b/common/src/main/resources/testdata/proto3/test_all_types.fds @@ -0,0 +1,871 @@ +file { + name: "common/src/main/resources/testdata/proto3/test_all_types_serialized.proto" + package: "dev.cel.testing.testdata.serialized.proto3" + dependency: "google/protobuf/any.proto" + dependency: "google/protobuf/duration.proto" + dependency: "google/protobuf/struct.proto" + dependency: "google/protobuf/timestamp.proto" + dependency: "google/protobuf/wrappers.proto" + message_type { + name: "TestAllTypes" + field { + name: "single_int32" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_INT32 + } + field { + name: "single_int64" + number: 2 + label: LABEL_OPTIONAL + type: TYPE_INT64 + } + field { + name: "single_uint32" + number: 3 + label: LABEL_OPTIONAL + type: TYPE_UINT32 + } + field { + name: "single_uint64" + number: 4 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + } + field { + name: "single_sint32" + number: 5 + label: LABEL_OPTIONAL + type: TYPE_SINT32 + } + field { + name: "single_sint64" + number: 6 + label: LABEL_OPTIONAL + type: TYPE_SINT64 + } + field { + name: "single_fixed32" + number: 7 + label: LABEL_OPTIONAL + type: TYPE_FIXED32 + } + field { + name: "single_fixed64" + number: 8 + label: LABEL_OPTIONAL + type: TYPE_FIXED64 + } + field { + name: "single_sfixed32" + number: 9 + label: LABEL_OPTIONAL + type: TYPE_SFIXED32 + } + field { + name: "single_sfixed64" + number: 10 + label: LABEL_OPTIONAL + type: TYPE_SFIXED64 + } + field { + name: "single_float" + number: 11 + label: LABEL_OPTIONAL + type: TYPE_FLOAT + } + field { + name: "single_double" + number: 12 + label: LABEL_OPTIONAL + type: TYPE_DOUBLE + } + field { + name: "single_bool" + number: 13 + label: LABEL_OPTIONAL + type: TYPE_BOOL + } + field { + name: "single_string" + number: 14 + label: LABEL_OPTIONAL + type: TYPE_STRING + } + field { + name: "single_bytes" + number: 15 + label: LABEL_OPTIONAL + type: TYPE_BYTES + } + field { + name: "optional_bool" + number: 16 + label: LABEL_OPTIONAL + type: TYPE_BOOL + oneof_index: 2 + proto3_optional: true + } + field { + name: "optional_string" + number: 17 + label: LABEL_OPTIONAL + type: TYPE_BOOL + oneof_index: 3 + proto3_optional: true + } + field { + name: "single_any" + number: 100 + label: LABEL_OPTIONAL + type: TYPE_MESSAGE + type_name: ".google.protobuf.Any" + } + field { + name: "single_duration" + number: 101 + label: LABEL_OPTIONAL + type: TYPE_MESSAGE + type_name: ".google.protobuf.Duration" + } + field { + name: "single_timestamp" + number: 102 + label: LABEL_OPTIONAL + type: TYPE_MESSAGE + type_name: ".google.protobuf.Timestamp" + } + field { + name: "single_struct" + number: 103 + label: LABEL_OPTIONAL + type: TYPE_MESSAGE + type_name: ".google.protobuf.Struct" + } + field { + name: "single_value" + number: 104 + label: LABEL_OPTIONAL + type: TYPE_MESSAGE + type_name: ".google.protobuf.Value" + } + field { + name: "single_int64_wrapper" + number: 105 + label: LABEL_OPTIONAL + type: TYPE_MESSAGE + type_name: ".google.protobuf.Int64Value" + } + field { + name: "single_int32_wrapper" + number: 106 + label: LABEL_OPTIONAL + type: TYPE_MESSAGE + type_name: ".google.protobuf.Int32Value" + } + field { + name: "single_double_wrapper" + number: 107 + label: LABEL_OPTIONAL + type: TYPE_MESSAGE + type_name: ".google.protobuf.DoubleValue" + } + field { + name: "single_float_wrapper" + number: 108 + label: LABEL_OPTIONAL + type: TYPE_MESSAGE + type_name: ".google.protobuf.FloatValue" + } + field { + name: "single_uint64_wrapper" + number: 109 + label: LABEL_OPTIONAL + type: TYPE_MESSAGE + type_name: ".google.protobuf.UInt64Value" + } + field { + name: "single_uint32_wrapper" + number: 110 + label: LABEL_OPTIONAL + type: TYPE_MESSAGE + type_name: ".google.protobuf.UInt32Value" + } + field { + name: "single_string_wrapper" + number: 111 + label: LABEL_OPTIONAL + type: TYPE_MESSAGE + type_name: ".google.protobuf.StringValue" + } + field { + name: "single_bool_wrapper" + number: 112 + label: LABEL_OPTIONAL + type: TYPE_MESSAGE + type_name: ".google.protobuf.BoolValue" + } + field { + name: "single_bytes_wrapper" + number: 113 + label: LABEL_OPTIONAL + type: TYPE_MESSAGE + type_name: ".google.protobuf.BytesValue" + } + field { + name: "single_list_value" + number: 114 + label: LABEL_OPTIONAL + type: TYPE_MESSAGE + type_name: ".google.protobuf.ListValue" + } + field { + name: "single_nested_message" + number: 21 + label: LABEL_OPTIONAL + type: TYPE_MESSAGE + type_name: ".dev.cel.testing.testdata.serialized.proto3.TestAllTypes.NestedMessage" + oneof_index: 0 + } + field { + name: "single_nested_enum" + number: 22 + label: LABEL_OPTIONAL + type: TYPE_ENUM + type_name: ".dev.cel.testing.testdata.serialized.proto3.TestAllTypes.NestedEnum" + oneof_index: 0 + } + field { + name: "standalone_message" + number: 23 + label: LABEL_OPTIONAL + type: TYPE_MESSAGE + type_name: ".dev.cel.testing.testdata.serialized.proto3.TestAllTypes.NestedMessage" + } + field { + name: "standalone_enum" + number: 24 + label: LABEL_OPTIONAL + type: TYPE_ENUM + type_name: ".dev.cel.testing.testdata.serialized.proto3.TestAllTypes.NestedEnum" + } + field { + name: "repeated_int32" + number: 31 + label: LABEL_REPEATED + type: TYPE_INT32 + } + field { + name: "repeated_int64" + number: 32 + label: LABEL_REPEATED + type: TYPE_INT64 + } + field { + name: "repeated_uint32" + number: 33 + label: LABEL_REPEATED + type: TYPE_UINT32 + } + field { + name: "repeated_uint64" + number: 34 + label: LABEL_REPEATED + type: TYPE_UINT64 + } + field { + name: "repeated_sint32" + number: 35 + label: LABEL_REPEATED + type: TYPE_SINT32 + } + field { + name: "repeated_sint64" + number: 36 + label: LABEL_REPEATED + type: TYPE_SINT64 + } + field { + name: "repeated_fixed32" + number: 37 + label: LABEL_REPEATED + type: TYPE_FIXED32 + } + field { + name: "repeated_fixed64" + number: 38 + label: LABEL_REPEATED + type: TYPE_FIXED64 + } + field { + name: "repeated_sfixed32" + number: 39 + label: LABEL_REPEATED + type: TYPE_SFIXED32 + } + field { + name: "repeated_sfixed64" + number: 40 + label: LABEL_REPEATED + type: TYPE_SFIXED64 + } + field { + name: "repeated_float" + number: 41 + label: LABEL_REPEATED + type: TYPE_FLOAT + } + field { + name: "repeated_double" + number: 42 + label: LABEL_REPEATED + type: TYPE_DOUBLE + } + field { + name: "repeated_bool" + number: 43 + label: LABEL_REPEATED + type: TYPE_BOOL + } + field { + name: "repeated_string" + number: 44 + label: LABEL_REPEATED + type: TYPE_STRING + } + field { + name: "repeated_bytes" + number: 45 + label: LABEL_REPEATED + type: TYPE_BYTES + } + field { + name: "repeated_nested_message" + number: 51 + label: LABEL_REPEATED + type: TYPE_MESSAGE + type_name: ".dev.cel.testing.testdata.serialized.proto3.TestAllTypes.NestedMessage" + } + field { + name: "repeated_nested_enum" + number: 52 + label: LABEL_REPEATED + type: TYPE_ENUM + type_name: ".dev.cel.testing.testdata.serialized.proto3.TestAllTypes.NestedEnum" + } + field { + name: "repeated_string_piece" + number: 53 + label: LABEL_REPEATED + type: TYPE_STRING + options { + ctype: STRING_PIECE + } + } + field { + name: "repeated_cord" + number: 54 + label: LABEL_REPEATED + type: TYPE_STRING + options { + ctype: CORD + } + } + field { + name: "repeated_lazy_message" + number: 55 + label: LABEL_REPEATED + type: TYPE_MESSAGE + type_name: ".dev.cel.testing.testdata.serialized.proto3.TestAllTypes.NestedMessage" + } + field { + name: "map_int32_int64" + number: 56 + label: LABEL_REPEATED + type: TYPE_MESSAGE + type_name: ".dev.cel.testing.testdata.serialized.proto3.TestAllTypes.MapInt32Int64Entry" + } + field { + name: "map_string_string" + number: 61 + label: LABEL_REPEATED + type: TYPE_MESSAGE + type_name: ".dev.cel.testing.testdata.serialized.proto3.TestAllTypes.MapStringStringEntry" + } + field { + name: "map_int64_nested_type" + number: 62 + label: LABEL_REPEATED + type: TYPE_MESSAGE + type_name: ".dev.cel.testing.testdata.serialized.proto3.TestAllTypes.MapInt64NestedTypeEntry" + } + field { + name: "oneof_type" + number: 63 + label: LABEL_OPTIONAL + type: TYPE_MESSAGE + type_name: ".dev.cel.testing.testdata.serialized.proto3.NestedTestAllTypes" + oneof_index: 1 + } + field { + name: "oneof_msg" + number: 64 + label: LABEL_OPTIONAL + type: TYPE_MESSAGE + type_name: ".dev.cel.testing.testdata.serialized.proto3.TestAllTypes.NestedMessage" + oneof_index: 1 + } + field { + name: "oneof_bool" + number: 65 + label: LABEL_OPTIONAL + type: TYPE_BOOL + oneof_index: 1 + } + nested_type { + name: "NestedMessage" + field { + name: "bb" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_INT32 + } + } + nested_type { + name: "MapInt32Int64Entry" + field { + name: "key" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_INT32 + } + field { + name: "value" + number: 2 + label: LABEL_OPTIONAL + type: TYPE_INT64 + } + options { + map_entry: true + } + } + nested_type { + name: "MapStringStringEntry" + field { + name: "key" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_STRING + } + field { + name: "value" + number: 2 + label: LABEL_OPTIONAL + type: TYPE_STRING + } + options { + map_entry: true + } + } + nested_type { + name: "MapInt64NestedTypeEntry" + field { + name: "key" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_INT64 + } + field { + name: "value" + number: 2 + label: LABEL_OPTIONAL + type: TYPE_MESSAGE + type_name: ".dev.cel.testing.testdata.serialized.proto3.NestedTestAllTypes" + } + options { + map_entry: true + } + } + enum_type { + name: "NestedEnum" + value { + name: "FOO" + number: 0 + } + value { + name: "BAR" + number: 1 + } + value { + name: "BAZ" + number: 2 + } + } + oneof_decl { + name: "nested_type" + } + oneof_decl { + name: "kind" + } + oneof_decl { + name: "_optional_bool" + } + oneof_decl { + name: "_optional_string" + } + } + message_type { + name: "NestedTestAllTypes" + field { + name: "child" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_MESSAGE + type_name: ".dev.cel.testing.testdata.serialized.proto3.NestedTestAllTypes" + } + field { + name: "payload" + number: 2 + label: LABEL_OPTIONAL + type: TYPE_MESSAGE + type_name: ".dev.cel.testing.testdata.serialized.proto3.TestAllTypes" + } + } + enum_type { + name: "GlobalEnum" + value { + name: "GOO" + number: 0 + } + value { + name: "GAR" + number: 1 + } + value { + name: "GAZ" + number: 2 + } + } + options { + java_package: "dev.cel.testing.testdata.serialized.proto3" + java_outer_classname: "TestAllTypesProto" + } + syntax: "proto3" +} +file { + name: "google/protobuf/any.proto" + package: "google.protobuf" + message_type { + name: "Any" + field { + name: "type_url" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_STRING + json_name: "typeUrl" + } + field { + name: "value" + number: 2 + label: LABEL_OPTIONAL + type: TYPE_BYTES + json_name: "value" + } + } + options { + java_package: "com.google.protobuf" + java_outer_classname: "AnyProto" + java_multiple_files: true + go_package: "google.golang.org/protobuf/types/known/anypb" + objc_class_prefix: "GPB" + csharp_namespace: "Google.Protobuf.WellKnownTypes" + } + syntax: "proto3" +} +file { + name: "google/protobuf/duration.proto" + package: "google.protobuf" + message_type { + name: "Duration" + field { + name: "seconds" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_INT64 + json_name: "seconds" + } + field { + name: "nanos" + number: 2 + label: LABEL_OPTIONAL + type: TYPE_INT32 + json_name: "nanos" + } + } + options { + java_package: "com.google.protobuf" + java_outer_classname: "DurationProto" + java_multiple_files: true + go_package: "google.golang.org/protobuf/types/known/durationpb" + cc_enable_arenas: true + objc_class_prefix: "GPB" + csharp_namespace: "Google.Protobuf.WellKnownTypes" + } + syntax: "proto3" +} +file { + name: "google/protobuf/struct.proto" + package: "google.protobuf" + message_type { + name: "Struct" + field { + name: "fields" + number: 1 + label: LABEL_REPEATED + type: TYPE_MESSAGE + type_name: ".google.protobuf.Struct.FieldsEntry" + json_name: "fields" + } + nested_type { + name: "FieldsEntry" + field { + name: "key" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_STRING + json_name: "key" + } + field { + name: "value" + number: 2 + label: LABEL_OPTIONAL + type: TYPE_MESSAGE + type_name: ".google.protobuf.Value" + json_name: "value" + } + options { + map_entry: true + } + } + } + message_type { + name: "Value" + field { + name: "null_value" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_ENUM + type_name: ".google.protobuf.NullValue" + oneof_index: 0 + json_name: "nullValue" + } + field { + name: "number_value" + number: 2 + label: LABEL_OPTIONAL + type: TYPE_DOUBLE + oneof_index: 0 + json_name: "numberValue" + } + field { + name: "string_value" + number: 3 + label: LABEL_OPTIONAL + type: TYPE_STRING + oneof_index: 0 + json_name: "stringValue" + } + field { + name: "bool_value" + number: 4 + label: LABEL_OPTIONAL + type: TYPE_BOOL + oneof_index: 0 + json_name: "boolValue" + } + field { + name: "struct_value" + number: 5 + label: LABEL_OPTIONAL + type: TYPE_MESSAGE + type_name: ".google.protobuf.Struct" + oneof_index: 0 + json_name: "structValue" + } + field { + name: "list_value" + number: 6 + label: LABEL_OPTIONAL + type: TYPE_MESSAGE + type_name: ".google.protobuf.ListValue" + oneof_index: 0 + json_name: "listValue" + } + oneof_decl { + name: "kind" + } + } + message_type { + name: "ListValue" + field { + name: "values" + number: 1 + label: LABEL_REPEATED + type: TYPE_MESSAGE + type_name: ".google.protobuf.Value" + json_name: "values" + } + } + enum_type { + name: "NullValue" + value { + name: "NULL_VALUE" + number: 0 + } + } + options { + java_package: "com.google.protobuf" + java_outer_classname: "StructProto" + java_multiple_files: true + go_package: "google.golang.org/protobuf/types/known/structpb" + cc_enable_arenas: true + objc_class_prefix: "GPB" + csharp_namespace: "Google.Protobuf.WellKnownTypes" + } + syntax: "proto3" +} +file { + name: "google/protobuf/timestamp.proto" + package: "google.protobuf" + message_type { + name: "Timestamp" + field { + name: "seconds" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_INT64 + json_name: "seconds" + } + field { + name: "nanos" + number: 2 + label: LABEL_OPTIONAL + type: TYPE_INT32 + json_name: "nanos" + } + } + options { + java_package: "com.google.protobuf" + java_outer_classname: "TimestampProto" + java_multiple_files: true + go_package: "google.golang.org/protobuf/types/known/timestamppb" + cc_enable_arenas: true + objc_class_prefix: "GPB" + csharp_namespace: "Google.Protobuf.WellKnownTypes" + } + syntax: "proto3" +} +file { + name: "google/protobuf/wrappers.proto" + package: "google.protobuf" + message_type { + name: "DoubleValue" + field { + name: "value" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_DOUBLE + json_name: "value" + } + } + message_type { + name: "FloatValue" + field { + name: "value" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_FLOAT + json_name: "value" + } + } + message_type { + name: "Int64Value" + field { + name: "value" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_INT64 + json_name: "value" + } + } + message_type { + name: "UInt64Value" + field { + name: "value" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "value" + } + } + message_type { + name: "Int32Value" + field { + name: "value" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_INT32 + json_name: "value" + } + } + message_type { + name: "UInt32Value" + field { + name: "value" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_UINT32 + json_name: "value" + } + } + message_type { + name: "BoolValue" + field { + name: "value" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_BOOL + json_name: "value" + } + } + message_type { + name: "StringValue" + field { + name: "value" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_STRING + json_name: "value" + } + } + message_type { + name: "BytesValue" + field { + name: "value" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_BYTES + json_name: "value" + } + } + options { + java_package: "com.google.protobuf" + java_outer_classname: "WrappersProto" + java_multiple_files: true + go_package: "google.golang.org/protobuf/types/known/wrapperspb" + cc_enable_arenas: true + objc_class_prefix: "GPB" + csharp_namespace: "Google.Protobuf.WellKnownTypes" + } + syntax: "proto3" +} diff --git a/common/src/main/resources/testdata/proto3/test_all_types.proto b/common/src/main/resources/testdata/proto3/test_all_types.proto deleted file mode 100644 index 2ed2d9900..000000000 --- a/common/src/main/resources/testdata/proto3/test_all_types.proto +++ /dev/null @@ -1,138 +0,0 @@ -// 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. - -// Keep this file synced with: -// https://raw.githubusercontent.com/google/cel-spec/master/proto/test/v1/proto3/test_all_types.proto - -syntax = "proto3"; - -package dev.cel.testing.testdata.proto3; - -import "google/protobuf/any.proto"; -import "google/protobuf/duration.proto"; -import "google/protobuf/struct.proto"; -import "google/protobuf/timestamp.proto"; -import "google/protobuf/wrappers.proto"; - -option java_outer_classname = "TestAllTypesProto"; -option java_package = "dev.cel.testing.testdata.proto3"; - -// This proto includes every type of field in both singular and repeated -// forms. -message TestAllTypes { - message NestedMessage { - // The field name "b" fails to compile in proto1 because it conflicts with - // a local variable named "b" in one of the generated methods. - // This file needs to compile in proto1 to test backwards-compatibility. - int32 bb = 1; - } - - enum NestedEnum { - FOO = 0; - BAR = 1; - BAZ = 2; - } - - // Singular - int32 single_int32 = 1; - int64 single_int64 = 2; - uint32 single_uint32 = 3; - uint64 single_uint64 = 4; - sint32 single_sint32 = 5; - sint64 single_sint64 = 6; - fixed32 single_fixed32 = 7; - fixed64 single_fixed64 = 8; - sfixed32 single_sfixed32 = 9; - sfixed64 single_sfixed64 = 10; - float single_float = 11; - double single_double = 12; - bool single_bool = 13; - string single_string = 14; - bytes single_bytes = 15; - optional bool optional_bool = 16; - optional bool optional_string = 17; - - // Wellknown. - google.protobuf.Any single_any = 100; - google.protobuf.Duration single_duration = 101; - google.protobuf.Timestamp single_timestamp = 102; - google.protobuf.Struct single_struct = 103; - google.protobuf.Value single_value = 104; - google.protobuf.Int64Value single_int64_wrapper = 105; - google.protobuf.Int32Value single_int32_wrapper = 106; - google.protobuf.DoubleValue single_double_wrapper = 107; - google.protobuf.FloatValue single_float_wrapper = 108; - google.protobuf.UInt64Value single_uint64_wrapper = 109; - google.protobuf.UInt32Value single_uint32_wrapper = 110; - google.protobuf.StringValue single_string_wrapper = 111; - google.protobuf.BoolValue single_bool_wrapper = 112; - google.protobuf.BytesValue single_bytes_wrapper = 113; - google.protobuf.ListValue single_list_value = 114; - - // Nested messages - oneof nested_type { - NestedMessage single_nested_message = 21; - NestedEnum single_nested_enum = 22; - } - NestedMessage standalone_message = 23; - NestedEnum standalone_enum = 24; - - // Repeated - repeated int32 repeated_int32 = 31; - repeated int64 repeated_int64 = 32; - repeated uint32 repeated_uint32 = 33; - repeated uint64 repeated_uint64 = 34; - repeated sint32 repeated_sint32 = 35; - repeated sint64 repeated_sint64 = 36; - repeated fixed32 repeated_fixed32 = 37; - repeated fixed64 repeated_fixed64 = 38; - repeated sfixed32 repeated_sfixed32 = 39; - repeated sfixed64 repeated_sfixed64 = 40; - repeated float repeated_float = 41; - repeated double repeated_double = 42; - repeated bool repeated_bool = 43; - repeated string repeated_string = 44; - repeated bytes repeated_bytes = 45; - - // Repeated and nested - repeated NestedMessage repeated_nested_message = 51; - repeated NestedEnum repeated_nested_enum = 52; - repeated string repeated_string_piece = 53 [ctype = STRING_PIECE]; - repeated string repeated_cord = 54 [ctype = CORD]; - repeated NestedMessage repeated_lazy_message = 55; - - // Map - map map_int32_int64 = 56; - map map_string_string = 61; - map map_int64_nested_type = 62; - - oneof kind { - NestedTestAllTypes oneof_type = 63; - NestedMessage oneof_msg = 64; - bool oneof_bool = 65; - } -} - -// This proto includes a recursively nested message. -message NestedTestAllTypes { - NestedTestAllTypes child = 1; - TestAllTypes payload = 2; -} - -// This proto tests that global enums are resolved correctly. -enum GlobalEnum { - GOO = 0; - GAR = 1; - GAZ = 2; -} diff --git a/common/src/test/java/dev/cel/common/BUILD.bazel b/common/src/test/java/dev/cel/common/BUILD.bazel index d22f00e7f..98be87d17 100644 --- a/common/src/test/java/dev/cel/common/BUILD.bazel +++ b/common/src/test/java/dev/cel/common/BUILD.bazel @@ -1,8 +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", @@ -10,21 +13,36 @@ java_library( srcs = glob(["*.java"]), deps = [ "//:java_truth", - "//common", + "//common:cel_ast", + "//common:cel_descriptor_util", + "//common:cel_descriptors", + "//common:cel_source", "//common:compiler_common", - "//common:features", + "//common:container", + "//common:operator", "//common:options", + "//common:proto_ast", + "//common:proto_json_adapter", "//common:proto_v1alpha1_ast", + "//common:source_location", "//common/ast", "//common/internal", + "//common/internal:code_point_stream", "//common/types", - "//common/types:cel_types", + "//common/types:cel_proto_types", "//common/types:cel_v1alpha1_types", - "@cel_spec//proto/cel/expr:expr_java_proto", + "//common/values:cel_byte_string", + "//compiler", + "//compiler:compiler_builder", + "//parser:macro", + "@cel_spec//proto/cel/expr:checked_java_proto", + "@cel_spec//proto/cel/expr:syntax_java_proto", + "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", "@com_google_googleapis//google/api/expr/v1alpha1:expr_java_proto", - "@maven//:com_google_api_grpc_proto_google_common_protos", "@maven//:com_google_guava_guava", + "@maven//:com_google_guava_guava_testlib", "@maven//:com_google_protobuf_protobuf_java", + "@maven//:com_google_testparameterinjector_test_parameter_injector", "@maven//:com_google_truth_extensions_truth_proto_extension", "@maven//:junit_junit", "@maven//:org_antlr_antlr4_runtime", diff --git a/common/src/test/java/dev/cel/common/CelAbstractSyntaxTreeTest.java b/common/src/test/java/dev/cel/common/CelAbstractSyntaxTreeTest.java index 78c16c165..b04f9f406 100644 --- a/common/src/test/java/dev/cel/common/CelAbstractSyntaxTreeTest.java +++ b/common/src/test/java/dev/cel/common/CelAbstractSyntaxTreeTest.java @@ -15,7 +15,6 @@ package dev.cel.common; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; import dev.cel.expr.CheckedExpr; import dev.cel.expr.Constant; @@ -26,10 +25,14 @@ import dev.cel.expr.SourceInfo; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.testing.EqualsTester; import dev.cel.common.ast.CelConstant; import dev.cel.common.ast.CelExpr; -import dev.cel.common.types.CelTypes; +import dev.cel.common.types.CelProtoTypes; import dev.cel.common.types.SimpleType; +import dev.cel.compiler.CelCompiler; +import dev.cel.compiler.CelCompilerFactory; +import dev.cel.parser.CelStandardMacro; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -54,9 +57,9 @@ public final class CelAbstractSyntaxTreeTest { private static final CelAbstractSyntaxTree CHECKED_ENUM_AST = CelProtoAbstractSyntaxTree.fromCheckedExpr( CheckedExpr.newBuilder() - .putTypeMap(1L, CelTypes.INT64) - .putTypeMap(2L, CelTypes.INT64) - .putTypeMap(3L, CelTypes.BOOL) + .putTypeMap(1L, CelProtoTypes.INT64) + .putTypeMap(2L, CelProtoTypes.INT64) + .putTypeMap(3L, CelProtoTypes.BOOL) .putReferenceMap( 2L, Reference.newBuilder() @@ -91,7 +94,6 @@ public final class CelAbstractSyntaxTreeTest { public void getResultType_isDynWhenParsedExpr() { CelAbstractSyntaxTree ast = PARSED_AST; - assertThat(ast.getProtoResultType()).isEqualTo(CelTypes.DYN); assertThat(ast.getResultType()).isEqualTo(SimpleType.DYN); } @@ -99,7 +101,6 @@ public void getResultType_isDynWhenParsedExpr() { public void getResultType_isStaticWhenCheckedExpr() { CelAbstractSyntaxTree ast = CHECKED_AST; - assertThat(ast.getProtoResultType()).isEqualTo(CelTypes.BOOL); assertThat(ast.getResultType()).isEqualTo(SimpleType.BOOL); } @@ -146,15 +147,35 @@ public void getSource_hasDescriptionEqualToSourceLocation() { assertThat(PARSED_AST.getSource().getDescription()).isEqualTo("test/location.cel"); } + @Test + public void equalityTest() throws Exception { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .setOptions(CelOptions.current().populateMacroCalls(true).build()) + .build(); + new EqualsTester() + .addEqualityGroup( + CelAbstractSyntaxTree.newParsedAst( + CelExpr.newBuilder().build(), CelSource.newBuilder().build())) + .addEqualityGroup( + celCompiler.compile("'foo'").getAst(), celCompiler.compile("'foo'").getAst()) // ASCII + .addEqualityGroup( + celCompiler.compile("'가나다'").getAst(), celCompiler.compile("'가나다'").getAst()) // BMP + .addEqualityGroup( + celCompiler.compile("'😦😁😑'").getAst(), + celCompiler.compile("'😦😁😑'").getAst()) // SMP + .addEqualityGroup( + celCompiler.compile("[1,2,3].exists(x, x > 0)").getAst(), + celCompiler.compile("[1,2,3].exists(x, x > 0)").getAst()) + .testEquals(); + } + @Test public void parsedExpression_createAst() { CelExpr celExpr = CelExpr.newBuilder().setId(1).setConstant(CelConstant.ofValue(2)).build(); CelSource celSource = - CelSource.newBuilder("expression") - .setDescription("desc") - .addPositions(1, 5) - .addLineOffsets(10) - .build(); + CelSource.newBuilder("expression").setDescription("desc").addPositions(1, 5).build(); CelAbstractSyntaxTree ast = CelAbstractSyntaxTree.newParsedAst(celExpr, celSource); diff --git a/common/src/test/java/dev/cel/common/CelContainerTest.java b/common/src/test/java/dev/cel/common/CelContainerTest.java new file mode 100644 index 000000000..cd7b73576 --- /dev/null +++ b/common/src/test/java/dev/cel/common/CelContainerTest.java @@ -0,0 +1,208 @@ +// 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; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableSet; +import com.google.testing.junit.testparameterinjector.TestParameter; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import com.google.testing.junit.testparameterinjector.TestParameters; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public class CelContainerTest { + + @Test + public void resolveCandidateName_singleContainer_resolvesAllCandidates() { + CelContainer container = CelContainer.ofName("a.b.c.M.N"); + + ImmutableSet resolvedNames = container.resolveCandidateNames("R.s"); + + assertThat(resolvedNames) + .containsExactly("a.b.c.M.N.R.s", "a.b.c.M.R.s", "a.b.c.R.s", "a.b.R.s", "a.R.s", "R.s") + .inOrder(); + } + + @Test + public void resolveCandidateName_fullyQualifiedTypeName_resolveSingleCandidate() { + CelContainer container = CelContainer.ofName("a.b.c.M.N"); + + ImmutableSet resolvedNames = container.resolveCandidateNames(".R.s"); + + assertThat(resolvedNames).containsExactly("R.s"); + } + + @Test + @TestParameters("{typeName: bigex, resolved: my.example.pkg.verbose}") + @TestParameters("{typeName: .bigex, resolved: my.example.pkg.verbose}") + @TestParameters("{typeName: bigex.Execute, resolved: my.example.pkg.verbose.Execute}") + @TestParameters("{typeName: .bigex.Execute, resolved: my.example.pkg.verbose.Execute}") + public void resolveCandidateName_withAlias_resolvesSingleCandidate( + String typeName, String resolved) { + CelContainer container = + CelContainer.newBuilder() + .setName("a.b.c") // Note: alias takes precedence + .addAlias("bigex", "my.example.pkg.verbose") + .build(); + + ImmutableSet resolvedNames = container.resolveCandidateNames(typeName); + + assertThat(resolvedNames).containsExactly(resolved); + } + + @Test + @TestParameters("{typeName: R, resolved: my.alias.R}") + @TestParameters("{typeName: R.S.T, resolved: my.alias.R.S.T}") + public void resolveCandidateName_withMatchingAbbreviation_resolvesSingleCandidate( + String typeName, String resolved) { + CelContainer container = + CelContainer.newBuilder().setName("a.b.c").addAbbreviations("my.alias.R").build(); + + ImmutableSet resolvedNames = container.resolveCandidateNames(typeName); + + assertThat(resolvedNames).containsExactly(resolved); + } + + @Test + public void resolveCandidateName_withUnmatchedAbbreviation_resolvesMultipleCandidates() { + CelContainer container = + CelContainer.newBuilder().setName("a.b.c").addAbbreviations("my.alias.R").build(); + + ImmutableSet resolvedNames = container.resolveCandidateNames("S"); + + assertThat(resolvedNames).containsExactly("a.b.c.S", "a.b.S", "a.S", "S").inOrder(); + } + + @Test + public void containerBuilder_duplicateAliases_throws() { + IllegalArgumentException e = + assertThrows( + IllegalArgumentException.class, + () -> CelContainer.newBuilder().addAlias("foo", "a.b").addAlias("foo", "b.c")); + assertThat(e) + .hasMessageThat() + .contains("alias collides with existing reference: name=b.c, alias=foo, existing=a.b"); + } + + @Test + public void containerBuilder_aliasCollidesWithContainer_throws() { + IllegalArgumentException e = + assertThrows( + IllegalArgumentException.class, + () -> CelContainer.newBuilder().setName("foo").addAlias("foo", "a.b")); + assertThat(e) + .hasMessageThat() + .contains("alias collides with container name: name=a.b, alias=foo, container=foo"); + } + + @Test + public void containerBuilder_addAliasError_throws(@TestParameter AliasingErrorTestCase testCase) { + IllegalArgumentException e = + assertThrows( + IllegalArgumentException.class, + () -> CelContainer.newBuilder().addAlias(testCase.alias, testCase.qualifiedName)); + assertThat(e).hasMessageThat().contains(testCase.errorMessage); + } + + private enum AliasingErrorTestCase { + BAD_QUALIFIED_NAME( + "foo", ".bad.name", "qualified name must not begin with a leading '.': .bad.name"), + BAD_ALIAS_NAME_1( + "bad.alias", "b.c", "alias must be non-empty and simple (not qualified): alias=bad.alias"), + BAD_ALIAS_NAME_2("", "b.c", "alias must be non-empty and simple (not qualified): alias="); + + private final String alias; + private final String qualifiedName; + private final String errorMessage; + + AliasingErrorTestCase(String alias, String qualifiedName, String errorMessage) { + this.alias = alias; + this.qualifiedName = qualifiedName; + this.errorMessage = errorMessage; + } + } + + @Test + public void containerBuilder_addAbbreviationsError_throws( + @TestParameter AbbreviationErrorTestCase testCase) { + IllegalArgumentException e = + assertThrows( + IllegalArgumentException.class, + () -> CelContainer.newBuilder().addAbbreviations(testCase.qualifiedNames)); + assertThat(e).hasMessageThat().contains(testCase.errorMessage); + } + + private enum AbbreviationErrorTestCase { + ABBREVIATION_COLLISION( + ImmutableSet.of("my.alias.R", "yer.other.R"), + "abbreviation collides with existing reference: name=yer.other.R, abbreviation=R," + + " existing=my.alias.R"), + INVALID_DOT_PREFIX( + ".bad", "invalid qualified name: .bad, wanted name of the form 'qualified.name'"), + INVALID_DOT_SUFFIX( + "bad.alias.", + "invalid qualified name: bad.alias., wanted name of the form 'qualified.name'"), + NO_QUALIFIER( + " bad_alias1 ", + "invalid qualified name: bad_alias1, wanted name of the form 'qualified.name'"), + INVALID_IDENTIFIER( + " bad.alias!", + "invalid qualified name: bad.alias!, wanted name of the form 'qualified.name'"), + ; + + private final ImmutableSet qualifiedNames; + private final String errorMessage; + + AbbreviationErrorTestCase(String qualifiedNames, String errorMessage) { + this(ImmutableSet.of(qualifiedNames), errorMessage); + } + + AbbreviationErrorTestCase(ImmutableSet qualifiedNames, String errorMessage) { + this.qualifiedNames = qualifiedNames; + this.errorMessage = errorMessage; + } + } + + @Test + public void containerBuilder_addAbbreviationsCollidesWithContainer_throws() { + IllegalArgumentException e = + assertThrows( + IllegalArgumentException.class, + () -> + CelContainer.newBuilder() + .setName("a.b.c.M.N") + .addAbbreviations("my.alias.a", "yer.other.b")); + + assertThat(e) + .hasMessageThat() + .contains( + "abbreviation collides with container name: name=my.alias.a, abbreviation=a," + + " container=a.b.c.M.N"); + } + + @Test + public void container_toBuilderRoundTrip_retainsExistingProperties() { + CelContainer container = + CelContainer.newBuilder().setName("hello").addAlias("foo", "x.y").build(); + + container = container.toBuilder().addAlias("bar", "a.b").build(); + + assertThat(container.name()).isEqualTo("hello"); + assertThat(container.aliases()).containsExactly("foo", "x.y", "bar", "a.b").inOrder(); + } +} diff --git a/common/src/test/java/dev/cel/common/CelDescriptorUtilTest.java b/common/src/test/java/dev/cel/common/CelDescriptorUtilTest.java index 548a18ba5..3184f5624 100644 --- a/common/src/test/java/dev/cel/common/CelDescriptorUtilTest.java +++ b/common/src/test/java/dev/cel/common/CelDescriptorUtilTest.java @@ -16,26 +16,34 @@ import static com.google.common.collect.ImmutableSet.toImmutableSet; 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.ImmutableSet; import com.google.protobuf.Any; +import com.google.protobuf.BoolValue; +import com.google.protobuf.BytesValue; import com.google.protobuf.DescriptorProtos.FileDescriptorSet; import com.google.protobuf.Descriptors.FileDescriptor; +import com.google.protobuf.DoubleValue; import com.google.protobuf.Duration; +import com.google.protobuf.Empty; +import com.google.protobuf.FieldMask; +import com.google.protobuf.FloatValue; +import com.google.protobuf.Int32Value; +import com.google.protobuf.Int64Value; import com.google.protobuf.ListValue; import com.google.protobuf.NullValue; +import com.google.protobuf.StringValue; import com.google.protobuf.Struct; import com.google.protobuf.Timestamp; +import com.google.protobuf.UInt32Value; +import com.google.protobuf.UInt64Value; import com.google.protobuf.Value; -import com.google.rpc.context.AttributeContext; -import com.google.rpc.context.AttributeContext.Api; -import com.google.rpc.context.AttributeContext.Auth; -import com.google.rpc.context.AttributeContext.Peer; -import com.google.rpc.context.AttributeContext.Request; -import com.google.rpc.context.AttributeContext.Resource; -import com.google.rpc.context.AttributeContext.Response; +import com.google.protobuf.WrappersProto; +import dev.cel.expr.conformance.proto3.GlobalEnum; +import dev.cel.expr.conformance.proto3.NestedTestAllTypes; +import dev.cel.expr.conformance.proto3.TestAllTypes; +import dev.cel.expr.conformance.proto3.TestAllTypes.NestedEnum; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -47,74 +55,95 @@ public final class CelDescriptorUtilTest { public void getAllDescriptorsFromFileDescriptor() { CelDescriptors celDescriptors = CelDescriptorUtil.getAllDescriptorsFromFileDescriptor( - ImmutableList.of(AttributeContext.getDescriptor().getFile())); + ImmutableList.of(TestAllTypes.getDescriptor().getFile())); assertThat(celDescriptors.messageTypeDescriptors()) .containsExactly( Any.getDescriptor(), + BoolValue.getDescriptor(), + BytesValue.getDescriptor(), + DoubleValue.getDescriptor(), Duration.getDescriptor(), - Struct.getDescriptor(), - Value.getDescriptor(), + Empty.getDescriptor(), + FieldMask.getDescriptor(), + FloatValue.getDescriptor(), + Int32Value.getDescriptor(), + Int64Value.getDescriptor(), ListValue.getDescriptor(), + NestedTestAllTypes.getDescriptor(), + StringValue.getDescriptor(), + Struct.getDescriptor(), + TestAllTypes.NestedMessage.getDescriptor(), + TestAllTypes.getDescriptor(), Timestamp.getDescriptor(), - AttributeContext.getDescriptor(), - Peer.getDescriptor(), - Api.getDescriptor(), - Auth.getDescriptor(), - Request.getDescriptor(), - Response.getDescriptor(), - Resource.getDescriptor()); - assertThat(celDescriptors.enumDescriptors()).containsExactly(NullValue.getDescriptor()); + UInt32Value.getDescriptor(), + UInt64Value.getDescriptor(), + Value.getDescriptor()); + assertThat(celDescriptors.enumDescriptors()) + .containsExactly( + NullValue.getDescriptor(), GlobalEnum.getDescriptor(), NestedEnum.getDescriptor()); assertThat(celDescriptors.fileDescriptors()) .containsExactly( - AttributeContext.getDescriptor().getFile(), - // The following fileDescriptors are defined as imports of AttributeContext proto + TestAllTypes.getDescriptor().getFile(), + // The following fileDescriptors are defined as imports of TestAllTypes proto Any.getDescriptor().getFile(), - Timestamp.getDescriptor().getFile(), + Duration.getDescriptor().getFile(), + Empty.getDescriptor().getFile(), + FieldMask.getDescriptor().getFile(), Struct.getDescriptor().getFile(), - Duration.getDescriptor().getFile()); + Timestamp.getDescriptor().getFile(), + WrappersProto.getDescriptor().getFile()); } @Test public void getFileDescriptorsForDescriptors() { ImmutableSet fileDescriptors = CelDescriptorUtil.getFileDescriptorsForDescriptors( - ImmutableList.of(AttributeContext.getDescriptor())); + ImmutableList.of(TestAllTypes.getDescriptor())); assertThat(fileDescriptors) .containsExactly( + TestAllTypes.getDescriptor().getFile(), Any.getDescriptor().getFile(), Duration.getDescriptor().getFile(), + Empty.getDescriptor().getFile(), + FieldMask.getDescriptor().getFile(), Struct.getDescriptor().getFile(), Timestamp.getDescriptor().getFile(), - AttributeContext.getDescriptor().getFile()); + WrappersProto.getDescriptor().getFile()); } @Test public void getFileDescriptorsForDescriptors_duplicateDescriptors() { ImmutableSet fileDescriptors = CelDescriptorUtil.getFileDescriptorsForDescriptors( - ImmutableList.of(AttributeContext.getDescriptor(), AttributeContext.getDescriptor())); + ImmutableList.of(TestAllTypes.getDescriptor(), TestAllTypes.getDescriptor())); assertThat(fileDescriptors) .containsExactly( + TestAllTypes.getDescriptor().getFile(), Any.getDescriptor().getFile(), Duration.getDescriptor().getFile(), + Empty.getDescriptor().getFile(), + FieldMask.getDescriptor().getFile(), Struct.getDescriptor().getFile(), Timestamp.getDescriptor().getFile(), - AttributeContext.getDescriptor().getFile()); + WrappersProto.getDescriptor().getFile()); } @Test public void getFileDescriptorsForDescriptors_duplicateAncestorDescriptors() { ImmutableSet fileDescriptors = CelDescriptorUtil.getFileDescriptorsForDescriptors( - ImmutableList.of(AttributeContext.getDescriptor(), Any.getDescriptor())); + ImmutableList.of(TestAllTypes.getDescriptor(), Any.getDescriptor())); assertThat(fileDescriptors) .containsExactly( + TestAllTypes.getDescriptor().getFile(), Any.getDescriptor().getFile(), Duration.getDescriptor().getFile(), + Empty.getDescriptor().getFile(), + FieldMask.getDescriptor().getFile(), Struct.getDescriptor().getFile(), Timestamp.getDescriptor().getFile(), - AttributeContext.getDescriptor().getFile()); + WrappersProto.getDescriptor().getFile()); } @Test @@ -122,20 +151,26 @@ public void getFileDescriptorsFromFileDescriptorSet() { FileDescriptorSet fds = FileDescriptorSet.newBuilder() .addFile(Any.getDescriptor().getFile().toProto()) + .addFile(Empty.getDescriptor().getFile().toProto()) + .addFile(FieldMask.getDescriptor().getFile().toProto()) + .addFile(WrappersProto.getDescriptor().getFile().toProto()) .addFile(Duration.getDescriptor().getFile().toProto()) .addFile(Struct.getDescriptor().getFile().toProto()) .addFile(Timestamp.getDescriptor().getFile().toProto()) - .addFile(AttributeContext.getDescriptor().getFile().toProto()) + .addFile(TestAllTypes.getDescriptor().getFile().toProto()) .build(); ImmutableSet fileDescriptors = CelDescriptorUtil.getFileDescriptorsFromFileDescriptorSet(fds); assertThat(fileDescriptors.stream().map(FileDescriptor::getName).collect(toImmutableSet())) .containsExactly( + TestAllTypes.getDescriptor().getFile().getName(), Any.getDescriptor().getFile().getName(), Duration.getDescriptor().getFile().getName(), + Empty.getDescriptor().getFile().getName(), + FieldMask.getDescriptor().getFile().getName(), Struct.getDescriptor().getFile().getName(), Timestamp.getDescriptor().getFile().getName(), - AttributeContext.getDescriptor().getFile().getName()); + WrappersProto.getDescriptor().getFile().getName()); } @Test @@ -145,13 +180,18 @@ public void getFileDescriptorsFromFileDescriptorSet_incompleteFileSet() { .addFile(Duration.getDescriptor().getFile().toProto()) .addFile(Struct.getDescriptor().getFile().toProto()) .addFile(Timestamp.getDescriptor().getFile().toProto()) - .addFile(AttributeContext.getDescriptor().getFile().toProto()) + .addFile(TestAllTypes.getDescriptor().getFile().toProto()) .build(); - IllegalArgumentException e = - assertThrows( - IllegalArgumentException.class, - () -> CelDescriptorUtil.getFileDescriptorsFromFileDescriptorSet(fds)); - assertThat(e).hasMessageThat().contains("google/protobuf/any.proto"); + + ImmutableSet fileDescriptors = + CelDescriptorUtil.getFileDescriptorsFromFileDescriptorSet(fds); + + assertThat(fileDescriptors.stream().map(FileDescriptor::getName).collect(toImmutableSet())) + .containsExactly( + TestAllTypes.getDescriptor().getFile().getName(), + Duration.getDescriptor().getFile().getName(), + Struct.getDescriptor().getFile().getName(), + Timestamp.getDescriptor().getFile().getName()); } @Test diff --git a/common/src/test/java/dev/cel/common/CelOptionsTest.java b/common/src/test/java/dev/cel/common/CelOptionsTest.java index 4f6c3fc7a..751d85ed8 100644 --- a/common/src/test/java/dev/cel/common/CelOptionsTest.java +++ b/common/src/test/java/dev/cel/common/CelOptionsTest.java @@ -16,7 +16,6 @@ import static com.google.common.truth.Truth.assertThat; -import com.google.common.collect.ImmutableSet; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -31,49 +30,9 @@ public void current_success_celOptions() { assertThat(options.enableRegexPartialMatch()).isTrue(); } - @Test - public void fromExprFeatures_success_allRoundtrip() { - ImmutableSet.Builder allFeatures = ImmutableSet.builder(); - allFeatures.add( - ExprFeatures.COMPILE_TIME_OVERLOAD_RESOLUTION, - ExprFeatures.HOMOGENEOUS_LITERALS, - ExprFeatures.REGEX_PARTIAL_MATCH, - ExprFeatures.RESERVED_IDS, - ExprFeatures.RETAIN_REPEATED_UNARY_OPERATORS, - ExprFeatures.RETAIN_UNBALANCED_LOGICAL_EXPRESSIONS, - ExprFeatures.UNSIGNED_COMPARISON_AND_ARITHMETIC_IS_UNSIGNED, - ExprFeatures.ERROR_ON_WRAP, - ExprFeatures.ERROR_ON_DUPLICATE_KEYS, - ExprFeatures.POPULATE_MACRO_CALLS, - ExprFeatures.ENABLE_TIMESTAMP_EPOCH, - ExprFeatures.ENABLE_HETEROGENEOUS_NUMERIC_COMPARISONS, - ExprFeatures.ENABLE_NAMESPACED_DECLARATIONS, - ExprFeatures.ENABLE_UNSIGNED_LONGS, - ExprFeatures.PROTO_DIFFERENCER_EQUALITY); - assertThat(CelOptions.fromExprFeatures(allFeatures.build()).toExprFeatures()) - .containsExactlyElementsIn(allFeatures.build()); - } - - @Test - public void toExprFeatures_success_includesExprFeaturesCurrent() { - assertThat(CelOptions.current().build().toExprFeatures()).isEqualTo(ExprFeatures.CURRENT); - } - - @Test - public void fromExprFeatures_success_currentRoundtrip() { - assertThat(CelOptions.fromExprFeatures(ExprFeatures.CURRENT).toExprFeatures()) - .isEqualTo(ExprFeatures.CURRENT); - } - - @Test - public void fromExprFeatures_success_legacyRoundtrip() { - assertThat(CelOptions.fromExprFeatures(ExprFeatures.LEGACY).toExprFeatures()) - .isEqualTo(ExprFeatures.LEGACY); - } - @Test public void current_defaults() { - // Defaults that aren't represented in deprecated ExprFeatures. + // Defaults that aren't represented in deprecated CelOptions assertThat(CelOptions.current().build().enableUnknownTracking()).isFalse(); assertThat(CelOptions.current().build().resolveTypeDependencies()).isTrue(); } diff --git a/common/src/test/java/dev/cel/common/CelProtoAbstractSyntaxTreeTest.java b/common/src/test/java/dev/cel/common/CelProtoAbstractSyntaxTreeTest.java index 1d6170f56..585828549 100644 --- a/common/src/test/java/dev/cel/common/CelProtoAbstractSyntaxTreeTest.java +++ b/common/src/test/java/dev/cel/common/CelProtoAbstractSyntaxTreeTest.java @@ -28,7 +28,7 @@ import dev.cel.expr.SourceInfo.Extension; import dev.cel.expr.SourceInfo.Extension.Component; import dev.cel.expr.SourceInfo.Extension.Version; -import dev.cel.common.types.CelTypes; +import dev.cel.common.types.CelProtoTypes; import java.util.Arrays; import org.junit.Test; import org.junit.runner.RunWith; @@ -79,7 +79,7 @@ public class CelProtoAbstractSyntaxTreeTest { private static final CheckedExpr CHECKED_EXPR = CheckedExpr.newBuilder() - .putTypeMap(1L, CelTypes.BOOL) + .putTypeMap(1L, CelProtoTypes.BOOL) .putReferenceMap(1L, Reference.newBuilder().addOverloadId("not_equals").build()) .setSourceInfo(SOURCE_INFO) .setExpr(EXPR) @@ -114,13 +114,13 @@ public void getSourceInfo_yieldsEquivalentMessage() { @Test public void getProtoResultType_isDynWhenParsedExpr() { CelProtoAbstractSyntaxTree ast = CelProtoAbstractSyntaxTree.fromParsedExpr(PARSED_EXPR); - assertThat(ast.getProtoResultType()).isEqualTo(CelTypes.DYN); + assertThat(ast.getProtoResultType()).isEqualTo(CelProtoTypes.DYN); } @Test public void getProtoResultType_isStaticWhenCheckedExpr() { CelProtoAbstractSyntaxTree ast = CelProtoAbstractSyntaxTree.fromCheckedExpr(CHECKED_EXPR); - assertThat(ast.getProtoResultType()).isEqualTo(CelTypes.BOOL); + assertThat(ast.getProtoResultType()).isEqualTo(CelProtoTypes.BOOL); } @Test diff --git a/common/src/test/java/dev/cel/common/CelProtoJsonAdapterTest.java b/common/src/test/java/dev/cel/common/CelProtoJsonAdapterTest.java new file mode 100644 index 000000000..bc845e5ab --- /dev/null +++ b/common/src/test/java/dev/cel/common/CelProtoJsonAdapterTest.java @@ -0,0 +1,80 @@ +// 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. + +package dev.cel.common; + +import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableMap; +import com.google.common.primitives.UnsignedLong; +import com.google.protobuf.ListValue; +import com.google.protobuf.Struct; +import com.google.protobuf.Value; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.common.values.CelByteString; +import java.util.Arrays; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public class CelProtoJsonAdapterTest { + + @Test + public void adaptValueToJsonValue_asymmetricJsonConversion() { + assertThat(CelProtoJsonAdapter.adaptValueToJsonValue(UnsignedLong.valueOf(1L))) + .isEqualTo(Value.newBuilder().setNumberValue(1).build()); + assertThat(CelProtoJsonAdapter.adaptValueToJsonValue(UnsignedLong.fromLongBits(-1L))) + .isEqualTo(Value.newBuilder().setStringValue(Long.toUnsignedString(-1L)).build()); + assertThat(CelProtoJsonAdapter.adaptValueToJsonValue(1L)) + .isEqualTo(Value.newBuilder().setNumberValue(1).build()); + assertThat(CelProtoJsonAdapter.adaptValueToJsonValue(Long.MAX_VALUE)) + .isEqualTo(Value.newBuilder().setStringValue(Long.toString(Long.MAX_VALUE)).build()); + assertThat(CelProtoJsonAdapter.adaptValueToJsonValue(CelByteString.copyFromUtf8("foo"))) + .isEqualTo(Value.newBuilder().setStringValue("Zm9v").build()); + } + + @Test + public void adaptValueToJsonList() { + ListValue listValue = CelProtoJsonAdapter.adaptToJsonListValue(Arrays.asList("hello", "world")); + + assertThat(listValue) + .isEqualTo( + ListValue.newBuilder() + .addValues(Value.newBuilder().setStringValue("hello")) + .addValues(Value.newBuilder().setStringValue("world")) + .build()); + } + + @Test + public void adaptToJsonStructValue() { + Struct struct = CelProtoJsonAdapter.adaptToJsonStructValue(ImmutableMap.of("key", "value")); + + assertThat(struct) + .isEqualTo( + Struct.newBuilder() + .putFields("key", Value.newBuilder().setStringValue("value").build()) + .build()); + } + + @Test + public void adaptValueToJsonValue_unsupportedJsonConversion() { + assertThrows( + ClassCastException.class, + () -> CelProtoJsonAdapter.adaptValueToJsonValue(ImmutableMap.of(1, 1))); + assertThrows( + IllegalArgumentException.class, + () -> CelProtoJsonAdapter.adaptValueToJsonValue(CelSource.newBuilder().build())); + } +} diff --git a/common/src/test/java/dev/cel/common/CelSourceTest.java b/common/src/test/java/dev/cel/common/CelSourceTest.java index 74e350a35..d8b3701e3 100644 --- a/common/src/test/java/dev/cel/common/CelSourceTest.java +++ b/common/src/test/java/dev/cel/common/CelSourceTest.java @@ -18,6 +18,7 @@ import static org.antlr.v4.runtime.IntStream.UNKNOWN_SOURCE_NAME; import static org.junit.Assert.assertThrows; +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; @@ -172,7 +173,7 @@ public void source_withExtension() { Component.COMPONENT_TYPE_CHECKER)) .build(); - Extension extension = celSource.getExtensions().get(0); + Extension extension = Iterables.getOnlyElement(celSource.getExtensions()); assertThat(extension.id()).isEqualTo("extension_id"); assertThat(extension.version().major()).isEqualTo(5L); assertThat(extension.version().minor()).isEqualTo(3L); @@ -180,4 +181,15 @@ public void source_withExtension() { .containsExactly(Component.COMPONENT_PARSER, Component.COMPONENT_TYPE_CHECKER); assertThat(celSource.getExtensions()).hasSize(1); } + + @Test + public void source_lineOffsetsAlreadyComputed_throws() { + CelSource.Builder sourceBuilder = CelSource.newBuilder("text"); + + IllegalStateException e = + assertThrows(IllegalStateException.class, () -> sourceBuilder.addLineOffsets(1)); + assertThat(e) + .hasMessageThat() + .contains("Line offsets were already been computed through the provided code points."); + } } diff --git a/common/src/test/java/dev/cel/common/CelValidationExceptionTest.java b/common/src/test/java/dev/cel/common/CelValidationExceptionTest.java index be428a64c..703531d81 100644 --- a/common/src/test/java/dev/cel/common/CelValidationExceptionTest.java +++ b/common/src/test/java/dev/cel/common/CelValidationExceptionTest.java @@ -37,6 +37,6 @@ public void construct_withLargeErrorCount() { assertThat(celValidationException.getErrors()).hasSize(1500); assertThat(celValidationException) .hasMessageThat() - .endsWith("...and 500 more errors (truncated)"); + .endsWith("...and 1400 more errors (truncated)"); } } diff --git a/parser/src/test/java/dev/cel/parser/OperatorTest.java b/common/src/test/java/dev/cel/common/OperatorTest.java similarity index 67% rename from parser/src/test/java/dev/cel/parser/OperatorTest.java rename to common/src/test/java/dev/cel/common/OperatorTest.java index 8f1a0bb86..28d941c6e 100644 --- a/parser/src/test/java/dev/cel/parser/OperatorTest.java +++ b/common/src/test/java/dev/cel/common/OperatorTest.java @@ -12,17 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -package dev.cel.parser; +package dev.cel.common; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +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.ast.CelExpr; -import dev.cel.common.ast.CelExpr.CelCall; import java.util.Optional; import org.junit.Test; import org.junit.runner.RunWith; @@ -50,6 +47,11 @@ public void findReverse_returnsCorrectOperator() { assertThat(Operator.findReverse("_+_")).hasValue(Operator.ADD); } + @Test + public void findReverse_allOperators(@TestParameter Operator operator) { + assertThat(Operator.findReverse(operator.getFunction())).hasValue(operator); + } + @Test public void findReverseBinaryOperator_returnsEmptyWhenNotFound() { assertThat(Operator.findReverseBinaryOperator("+")).isEmpty(); @@ -144,70 +146,4 @@ public void lookupBinaryOperator_nonEmpty(String operator, String value) { public void lookupBinaryOperator_empty(String operator) { assertEquals(Operator.lookupBinaryOperator(operator), Optional.empty()); } - - @Test - @TestParameters({ - "{operator1: '_[_]', operator2: '_&&_'}", - "{operator1: '_&&_', operator2: '_||_'}", - "{operator1: '_||_', operator2: '_?_:_'}", - "{operator1: '!_', operator2: '_*_'}", - "{operator1: '_==_', operator2: '_&&_'}", - "{operator1: '_!=_', operator2: '_?_:_'}", - }) - public void operatorLowerPrecedence(String operator1, String operator2) { - CelExpr expr = - CelExpr.newBuilder().setCall(CelCall.newBuilder().setFunction(operator2).build()).build(); - - assertTrue(Operator.isOperatorLowerPrecedence(operator1, expr)); - } - - @Test - @TestParameters({ - "{operator1: '_?_:_', operator2: '_&&_'}", - "{operator1: '_&&_', operator2: '_[_]'}", - "{operator1: '_||_', operator2: '!_'}", - "{operator1: '!_', operator2: '-_'}", - "{operator1: '_==_', operator2: '_!=_'}", - "{operator1: '_!=_', operator2: '_-_'}", - }) - public void operatorNotLowerPrecedence(String operator1, String operator2) { - CelExpr expr = - CelExpr.newBuilder().setCall(CelCall.newBuilder().setFunction(operator2).build()).build(); - - assertFalse(Operator.isOperatorLowerPrecedence(operator1, expr)); - } - - @Test - @TestParameters({ - "{operator: '_[_]'}", - "{operator: '!_'}", - "{operator: '_==_'}", - "{operator: '_?_:_'}", - "{operator: '_!=_'}", - "{operator: '_<_'}", - "{operator: '_<=_'}", - "{operator: '_>_'}", - "{operator: '_>=_'}", - "{operator: '_+_'}", - "{operator: '_-_'}", - "{operator: '_*_'}", - "{operator: '_/_'}", - "{operator: '_%_'}", - "{operator: '-_'}", - "{operator: 'has'}", - "{operator: '_[?_]'}", - "{operator: '@not_strictly_false'}", - }) - public void operatorLeftRecursive(String operator) { - assertTrue(Operator.isOperatorLeftRecursive(operator)); - } - - @Test - @TestParameters({ - "{operator: '_&&_'}", - "{operator: '_||_'}", - }) - public void operatorNotLeftRecursive(String operator) { - assertFalse(Operator.isOperatorLeftRecursive(operator)); - } } 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 595d516ee..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,8 +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,26 +14,36 @@ java_library( deps = [ "//:auto_value", "//:java_truth", - "//common", + "//common:cel_ast", "//common:compiler_common", + "//common:container", + "//common:mutable_ast", + "//common:mutable_source", + "//common:operator", + "//common:options", "//common/ast", "//common/ast:cel_expr_visitor", "//common/ast:expr_converter", "//common/ast:expr_factory", "//common/ast:expr_v1alpha1_converter", - "//common/resources/testdata/proto3:test_all_types_java_proto", + "//common/ast:mutable_expr", "//common/types", + "//common/values", + "//common/values:cel_byte_string", "//compiler", "//compiler:compiler_builder", "//extensions:optional_library", "//parser:macro", - "//parser:operator", - "@cel_spec//proto/cel/expr:expr_java_proto", + "@cel_spec//proto/cel/expr:checked_java_proto", + "@cel_spec//proto/cel/expr:syntax_java_proto", + "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", "@com_google_googleapis//google/api/expr/v1alpha1:expr_java_proto", "@maven//:com_google_guava_guava", + "@maven//:com_google_guava_guava_testlib", "@maven//:com_google_protobuf_protobuf_java", "@maven//:com_google_testparameterinjector_test_parameter_injector", "@maven//:junit_junit", + "@maven_android//:com_google_protobuf_protobuf_javalite", ], ) diff --git a/common/src/test/java/dev/cel/common/ast/CelConstantTest.java b/common/src/test/java/dev/cel/common/ast/CelConstantTest.java index 462267ba7..947073220 100644 --- a/common/src/test/java/dev/cel/common/ast/CelConstantTest.java +++ b/common/src/test/java/dev/cel/common/ast/CelConstantTest.java @@ -15,16 +15,17 @@ package dev.cel.common.ast; 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.primitives.UnsignedLong; -import com.google.protobuf.ByteString; import com.google.protobuf.Duration; -import com.google.protobuf.NullValue; import com.google.protobuf.Timestamp; import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; import dev.cel.common.ast.CelConstant.Kind; +import dev.cel.common.values.CelByteString; +import dev.cel.common.values.NullValue; import org.junit.Test; import org.junit.runner.RunWith; @@ -42,8 +43,8 @@ public void equality_objectsAreValueEqual_success() { .isEqualTo(CelConstant.ofValue(UnsignedLong.valueOf(2))); assertThat(CelConstant.ofValue(2.1)).isEqualTo(CelConstant.ofValue(2.1)); assertThat(CelConstant.ofValue("Hello world!")).isEqualTo(CelConstant.ofValue("Hello world!")); - assertThat(CelConstant.ofValue(ByteString.copyFromUtf8("Test"))) - .isEqualTo(CelConstant.ofValue(ByteString.copyFromUtf8("Test"))); + assertThat(CelConstant.ofValue(CelByteString.of("Test".getBytes(UTF_8)))) + .isEqualTo(CelConstant.ofValue(CelByteString.of("Test".getBytes(UTF_8)))); assertThat(CelConstant.ofValue(Duration.newBuilder().setSeconds(100L).build())) .isEqualTo(CelConstant.ofValue(Duration.newBuilder().setSeconds(100L).build())); assertThat(CelConstant.ofValue(Timestamp.newBuilder().setSeconds(100L).build())) @@ -52,8 +53,6 @@ public void equality_objectsAreValueEqual_success() { @Test public void equality_valueEqualityUnsatisfied_fails() { - assertThat(CelConstant.ofValue(NullValue.NULL_VALUE)) - .isNotEqualTo(CelConstant.ofValue(NullValue.UNRECOGNIZED)); assertThat(CelConstant.ofValue(true)).isNotEqualTo(CelConstant.ofValue(false)); assertThat(CelConstant.ofValue(false)).isNotEqualTo(CelConstant.ofValue(true)); assertThat(CelConstant.ofValue(3)).isNotEqualTo(CelConstant.ofValue(2)); @@ -63,8 +62,8 @@ public void equality_valueEqualityUnsatisfied_fails() { assertThat(CelConstant.ofValue(3)).isNotEqualTo(CelConstant.ofValue(3.0)); assertThat(CelConstant.ofValue(3.1)).isNotEqualTo(CelConstant.ofValue(2.1)); assertThat(CelConstant.ofValue("world!")).isNotEqualTo(CelConstant.ofValue("Hello world!")); - assertThat(CelConstant.ofValue(ByteString.copyFromUtf8("T"))) - .isNotEqualTo(CelConstant.ofValue(ByteString.copyFromUtf8("Test"))); + assertThat(CelConstant.ofValue(CelByteString.of("T".getBytes(UTF_8)))) + .isNotEqualTo(CelConstant.ofValue(CelByteString.of("Test".getBytes(UTF_8)))); assertThat(CelConstant.ofValue(Duration.newBuilder().setSeconds(100L).build())) .isNotEqualTo(CelConstant.ofValue(Duration.newBuilder().setSeconds(50).build())); assertThat(CelConstant.ofValue(Timestamp.newBuilder().setSeconds(100L).build())) @@ -117,9 +116,9 @@ public void constructStringValue() { @Test public void constructBytesValue() { - CelConstant constant = CelConstant.ofValue(ByteString.copyFromUtf8("Test")); + CelConstant constant = CelConstant.ofValue(CelByteString.of("Test".getBytes(UTF_8))); - assertThat(constant.bytesValue()).isEqualTo(ByteString.copyFromUtf8("Test")); + assertThat(constant.bytesValue()).isEqualTo(CelByteString.of("Test".getBytes(UTF_8))); } @Test @@ -152,7 +151,7 @@ private enum CelConstantTestCase { UINT64(CelConstant.ofValue(UnsignedLong.valueOf(2))), DOUBLE(CelConstant.ofValue(2.1)), STRING(CelConstant.ofValue("Hello world!")), - BYTES(CelConstant.ofValue(ByteString.copyFromUtf8("Test"))), + BYTES(CelConstant.ofValue(CelByteString.of("Test".getBytes(UTF_8)))), DURATION(CelConstant.ofValue(Duration.newBuilder().setSeconds(100L).build())), TIMESTAMP(CelConstant.ofValue(Timestamp.newBuilder().setSeconds(100L).build())); @@ -207,8 +206,8 @@ public void getObjectValue_success() { .isEqualTo(CelConstant.ofValue(UnsignedLong.valueOf(3L))); assertThat(CelConstant.ofObjectValue(3.0d)).isEqualTo(CelConstant.ofValue(3.0d)); assertThat(CelConstant.ofObjectValue("test")).isEqualTo(CelConstant.ofValue("test")); - assertThat(CelConstant.ofObjectValue(ByteString.copyFromUtf8("hello"))) - .isEqualTo(CelConstant.ofValue(ByteString.copyFromUtf8("hello"))); + assertThat(CelConstant.ofObjectValue(CelByteString.of("hello".getBytes(UTF_8)))) + .isEqualTo(CelConstant.ofValue(CelByteString.of("hello".getBytes(UTF_8)))); } @Test diff --git a/common/src/test/java/dev/cel/common/ast/CelExprConverterTest.java b/common/src/test/java/dev/cel/common/ast/CelExprConverterTest.java index 55820c3a3..ec064b4bb 100644 --- a/common/src/test/java/dev/cel/common/ast/CelExprConverterTest.java +++ b/common/src/test/java/dev/cel/common/ast/CelExprConverterTest.java @@ -33,6 +33,7 @@ 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.values.CelByteString; import java.util.Optional; import org.junit.Test; import org.junit.runner.RunWith; @@ -43,50 +44,50 @@ public class CelExprConverterTest { private enum ConstantTestCase { NOT_SET( Expr.newBuilder().setId(1).setConstExpr(Constant.getDefaultInstance()).build(), - CelExpr.ofConstantExpr(1, CelConstant.ofNotSet())), + CelExpr.ofConstant(1, CelConstant.ofNotSet())), NULL( Expr.newBuilder() .setId(1) .setConstExpr(Constant.newBuilder().setNullValue(NullValue.NULL_VALUE).build()) .build(), - CelExpr.ofConstantExpr(1, CelConstant.ofValue(NullValue.NULL_VALUE))), + CelExpr.ofConstant(1, CelConstant.ofValue(dev.cel.common.values.NullValue.NULL_VALUE))), BOOLEAN( Expr.newBuilder() .setId(1) .setConstExpr(Constant.newBuilder().setBoolValue(true).build()) .build(), - CelExpr.ofConstantExpr(1, CelConstant.ofValue(true))), + CelExpr.ofConstant(1, CelConstant.ofValue(true))), INT64( Expr.newBuilder() .setId(1) .setConstExpr(Constant.newBuilder().setInt64Value(10).build()) .build(), - CelExpr.ofConstantExpr(1, CelConstant.ofValue(10))), + CelExpr.ofConstant(1, CelConstant.ofValue(10))), UINT64( Expr.newBuilder() .setId(1) .setConstExpr(Constant.newBuilder().setUint64Value(15).build()) .build(), - CelExpr.ofConstantExpr(1, CelConstant.ofValue(UnsignedLong.valueOf(15)))), + CelExpr.ofConstant(1, CelConstant.ofValue(UnsignedLong.valueOf(15)))), DOUBLE( Expr.newBuilder() .setId(1) .setConstExpr(Constant.newBuilder().setDoubleValue(1.5).build()) .build(), - CelExpr.ofConstantExpr(1, CelConstant.ofValue(1.5))), + CelExpr.ofConstant(1, CelConstant.ofValue(1.5))), STRING( Expr.newBuilder() .setId(1) .setConstExpr(Constant.newBuilder().setStringValue("Test").build()) .build(), - CelExpr.ofConstantExpr(1, CelConstant.ofValue("Test"))), + CelExpr.ofConstant(1, CelConstant.ofValue("Test"))), BYTES( Expr.newBuilder() .setId(1) .setConstExpr( Constant.newBuilder().setBytesValue(ByteString.copyFromUtf8("TEST")).build()) .build(), - CelExpr.ofConstantExpr(1, CelConstant.ofValue(ByteString.copyFromUtf8("TEST")))); + CelExpr.ofConstant(1, CelConstant.ofValue(CelByteString.copyFromUtf8("TEST")))); final Expr protoExpr; final CelExpr celExpr; @@ -122,7 +123,7 @@ public void convertExprIdent_toCelIdent() { CelExpr celExpr = CelExprConverter.fromExpr(expr); - assertThat(celExpr).isEqualTo(CelExpr.ofIdentExpr(2, "Test")); + assertThat(celExpr).isEqualTo(CelExpr.ofIdent(2, "Test")); } @Test @@ -146,8 +147,8 @@ public void convertExprSelect_toCelSelect(boolean isTestOnly) { assertThat(celExpr) .isEqualTo( - CelExpr.ofSelectExpr( - 3, CelExpr.ofConstantExpr(4, CelConstant.ofValue(true)), "field", isTestOnly)); + CelExpr.ofSelect( + 3, CelExpr.ofConstant(4, CelConstant.ofValue(true)), "field", isTestOnly)); } @Test @@ -167,11 +168,11 @@ public void convertExprCall_toCelCall() { assertThat(celExpr) .isEqualTo( - CelExpr.ofCallExpr( + CelExpr.ofCall( 1, - Optional.of(CelExpr.ofConstantExpr(2, CelConstant.ofValue(10))), + Optional.of(CelExpr.ofConstant(2, CelConstant.ofValue(10))), "func", - ImmutableList.of(CelExpr.ofConstantExpr(3, CelConstant.ofValue(20))))); + ImmutableList.of(CelExpr.ofConstant(3, CelConstant.ofValue(20))))); } @Test @@ -191,11 +192,11 @@ public void convertExprList_toCelList() { assertThat(celExpr) .isEqualTo( - CelExpr.ofCreateListExpr( + CelExpr.ofList( 1, ImmutableList.of( - CelExpr.ofConstantExpr(2, CelConstant.ofValue(10)), - CelExpr.ofConstantExpr(3, CelConstant.ofValue(15))), + CelExpr.ofConstant(2, CelConstant.ofValue(10)), + CelExpr.ofConstant(3, CelConstant.ofValue(15))), ImmutableList.of(1))); } @@ -221,12 +222,12 @@ public void convertExprStructExpr_toCelStruct() { assertThat(celExpr) .isEqualTo( - CelExpr.ofCreateStructExpr( + CelExpr.ofStruct( 1, "messageName", ImmutableList.of( - CelExpr.ofCreateStructEntryExpr( - 2, "fieldKey", CelExpr.ofConstantExpr(3, CelConstant.ofValue(10)), true)))); + CelExpr.ofStructEntry( + 2, "fieldKey", CelExpr.ofConstant(3, CelConstant.ofValue(10)), true)))); } @Test @@ -299,13 +300,13 @@ public void convertExprStructExpr_toCelMap() { assertThat(celExpr) .isEqualTo( - CelExpr.ofCreateMapExpr( + CelExpr.ofMap( 1, ImmutableList.of( - CelExpr.ofCreateMapEntryExpr( + CelExpr.ofMapEntry( 2, - CelExpr.ofConstantExpr(3, CelConstant.ofValue(15)), - CelExpr.ofConstantExpr(4, CelConstant.ofValue(10)), + CelExpr.ofConstant(3, CelConstant.ofValue(15)), + CelExpr.ofConstant(4, CelConstant.ofValue(10)), true)))); } @@ -341,12 +342,12 @@ public void convertExprComprehensionExpr_toCelComprehension() { CelExpr.ofComprehension( 1, "iterVar", - CelExpr.ofConstantExpr(2, CelConstant.ofValue(10)), + CelExpr.ofConstant(2, CelConstant.ofValue(10)), "accuVar", - CelExpr.ofConstantExpr(3, CelConstant.ofValue(20)), - CelExpr.ofCallExpr(4, Optional.empty(), "testCondition", ImmutableList.of()), - CelExpr.ofCallExpr(5, Optional.empty(), "testStep", ImmutableList.of()), - CelExpr.ofConstantExpr(6, CelConstant.ofValue(30)))); + CelExpr.ofConstant(3, CelConstant.ofValue(20)), + CelExpr.ofCall(4, Optional.empty(), "testCondition", ImmutableList.of()), + CelExpr.ofCall(5, Optional.empty(), "testStep", ImmutableList.of()), + CelExpr.ofConstant(6, CelConstant.ofValue(30)))); } @Test @@ -390,7 +391,7 @@ public void convertCelNotSet_toExprNotSet() { @Test public void convertCelIdent_toExprIdent() { - CelExpr celExpr = CelExpr.ofIdentExpr(2, "Test"); + CelExpr celExpr = CelExpr.ofIdent(2, "Test"); Expr expr = CelExprConverter.fromCelExpr(celExpr); @@ -407,8 +408,7 @@ public void convertCelIdent_toExprIdent() { @TestParameters("{isTestOnly: false}") public void convertCelSelect_toExprSelect(boolean isTestOnly) { CelExpr celExpr = - CelExpr.ofSelectExpr( - 3, CelExpr.ofConstantExpr(4, CelConstant.ofValue(true)), "field", isTestOnly); + CelExpr.ofSelect(3, CelExpr.ofConstant(4, CelConstant.ofValue(true)), "field", isTestOnly); Expr expr = CelExprConverter.fromCelExpr(celExpr); @@ -430,11 +430,11 @@ public void convertCelSelect_toExprSelect(boolean isTestOnly) { @Test public void convertCelCall_toExprCall() { CelExpr celExpr = - CelExpr.ofCallExpr( + CelExpr.ofCall( 1, - Optional.of(CelExpr.ofConstantExpr(2, CelConstant.ofValue(10))), + Optional.of(CelExpr.ofConstant(2, CelConstant.ofValue(10))), "func", - ImmutableList.of(CelExpr.ofConstantExpr(3, CelConstant.ofValue(20)))); + ImmutableList.of(CelExpr.ofConstant(3, CelConstant.ofValue(20)))); Expr expr = CelExprConverter.fromCelExpr(celExpr); @@ -454,11 +454,11 @@ public void convertCelCall_toExprCall() { @Test public void convertCelList_toExprList() { CelExpr celExpr = - CelExpr.ofCreateListExpr( + CelExpr.ofList( 1, ImmutableList.of( - CelExpr.ofConstantExpr(2, CelConstant.ofValue(10)), - CelExpr.ofConstantExpr(3, CelConstant.ofValue(15))), + CelExpr.ofConstant(2, CelConstant.ofValue(10)), + CelExpr.ofConstant(3, CelConstant.ofValue(15))), ImmutableList.of(1)); Expr expr = CelExprConverter.fromCelExpr(celExpr); @@ -479,12 +479,12 @@ public void convertCelList_toExprList() { @Test public void convertCelStructExpr_toExprStruct_withFieldKey() { CelExpr celExpr = - CelExpr.ofCreateStructExpr( + CelExpr.ofStruct( 1, "messageName", ImmutableList.of( - CelExpr.ofCreateStructEntryExpr( - 2, "fieldKey", CelExpr.ofConstantExpr(3, CelConstant.ofValue(10)), true))); + CelExpr.ofStructEntry( + 2, "fieldKey", CelExpr.ofConstant(3, CelConstant.ofValue(10)), true))); Expr expr = CelExprConverter.fromCelExpr(celExpr); @@ -509,13 +509,13 @@ public void convertCelStructExpr_toExprStruct_withFieldKey() { @Test public void convertCelMapExpr_toExprStruct() { CelExpr celExpr = - CelExpr.ofCreateMapExpr( + CelExpr.ofMap( 1, ImmutableList.of( - CelExpr.ofCreateMapEntryExpr( + CelExpr.ofMapEntry( 2, - CelExpr.ofConstantExpr(3, CelConstant.ofValue(15)), - CelExpr.ofConstantExpr(4, CelConstant.ofValue(10)), + CelExpr.ofConstant(3, CelConstant.ofValue(15)), + CelExpr.ofConstant(4, CelConstant.ofValue(10)), true))); Expr expr = CelExprConverter.fromCelExpr(celExpr); @@ -543,12 +543,12 @@ public void convertCelComprehensionExpr_toExprComprehension() { CelExpr.ofComprehension( 1, "iterVar", - CelExpr.ofConstantExpr(2, CelConstant.ofValue(10)), + CelExpr.ofConstant(2, CelConstant.ofValue(10)), "accuVar", - CelExpr.ofConstantExpr(3, CelConstant.ofValue(20)), - CelExpr.ofCallExpr(4, Optional.empty(), "testCondition", ImmutableList.of()), - CelExpr.ofCallExpr(5, Optional.empty(), "testStep", ImmutableList.of()), - CelExpr.ofConstantExpr(6, CelConstant.ofValue(30))); + CelExpr.ofConstant(3, CelConstant.ofValue(20)), + CelExpr.ofCall(4, Optional.empty(), "testCondition", ImmutableList.of()), + CelExpr.ofCall(5, Optional.empty(), "testStep", ImmutableList.of()), + CelExpr.ofConstant(6, CelConstant.ofValue(30))); Expr expr = CelExprConverter.fromCelExpr(celExpr); diff --git a/common/src/test/java/dev/cel/common/ast/CelExprFactoryTest.java b/common/src/test/java/dev/cel/common/ast/CelExprFactoryTest.java index 30e8f5e41..f3d77ea9e 100644 --- a/common/src/test/java/dev/cel/common/ast/CelExprFactoryTest.java +++ b/common/src/test/java/dev/cel/common/ast/CelExprFactoryTest.java @@ -16,7 +16,6 @@ import static com.google.common.truth.Truth.assertThat; -import dev.cel.common.ast.CelExprIdGeneratorFactory.StableIdGenerator; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -38,15 +37,4 @@ public void nextExprId_startingDefaultIsOne() { assertThat(exprFactory.nextExprId()).isEqualTo(1L); assertThat(exprFactory.nextExprId()).isEqualTo(2L); } - - @Test - public void nextExprId_usingStableIdGenerator() { - StableIdGenerator stableIdGenerator = CelExprIdGeneratorFactory.newStableIdGenerator(0); - CelExprFactory exprFactory = CelExprFactory.newInstance(stableIdGenerator::nextExprId); - - assertThat(exprFactory.nextExprId()).isEqualTo(1L); - assertThat(exprFactory.nextExprId()).isEqualTo(2L); - assertThat(stableIdGenerator.hasId(-1)).isFalse(); - assertThat(stableIdGenerator.hasId(0)).isFalse(); - } } diff --git a/common/src/test/java/dev/cel/common/ast/CelExprFormatterTest.java b/common/src/test/java/dev/cel/common/ast/CelExprFormatterTest.java index 0efb890c1..116222d68 100644 --- a/common/src/test/java/dev/cel/common/ast/CelExprFormatterTest.java +++ b/common/src/test/java/dev/cel/common/ast/CelExprFormatterTest.java @@ -19,15 +19,17 @@ import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; 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.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.extensions.CelOptionalLibrary; import dev.cel.parser.CelStandardMacro; -import dev.cel.testing.testdata.proto3.TestAllTypesProto.TestAllTypes; import org.junit.Test; import org.junit.runner.RunWith; @@ -136,7 +138,7 @@ public void call_member() throws Exception { } @Test - public void create_list() throws Exception { + public void list() throws Exception { CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder() .addLibraries(CelOptionalLibrary.INSTANCE) @@ -148,7 +150,7 @@ public void create_list() throws Exception { assertThat(formattedExpr) .isEqualTo( - "CREATE_LIST [1] {\n" + "LIST [1] {\n" + " elements: {\n" + " CONSTANT [2] { value: 1 }\n" + " CONSTANT [3] { value: 2 }\n" @@ -170,10 +172,10 @@ public void create_list() throws Exception { } @Test - public void create_struct() throws Exception { + public void struct() throws Exception { CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder() - .setContainer("dev.cel.testing.testdata.proto3") + .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) .addMessageTypes(TestAllTypes.getDescriptor()) .addLibraries(CelOptionalLibrary.INSTANCE) .build(); @@ -188,8 +190,8 @@ public void create_struct() throws Exception { assertThat(formattedExpr) .isEqualTo( - "CREATE_STRUCT [1] {\n" - + " name: TestAllTypes\n" + "STRUCT [1] {\n" + + " name: cel.expr.conformance.proto3.TestAllTypes\n" + " entries: {\n" + " ENTRY [2] {\n" + " field_key: single_int64\n" @@ -220,10 +222,10 @@ public void create_struct() throws Exception { } @Test - public void create_map() throws Exception { + public void map() throws Exception { CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder() - .setContainer("dev.cel.testing.testdata.proto3") + .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) .addMessageTypes(TestAllTypes.getDescriptor()) .addLibraries(CelOptionalLibrary.INSTANCE) .build(); @@ -234,7 +236,7 @@ public void create_map() throws Exception { assertThat(formattedExpr) .isEqualTo( - "CREATE_MAP [1] {\n" + "MAP [1] {\n" + " MAP_ENTRY [2] {\n" + " key: {\n" + " CONSTANT [3] { value: 1 }\n" @@ -272,6 +274,7 @@ public void create_map() throws Exception { public void comprehension() throws Exception { CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder() + .setOptions(CelOptions.current().enableHiddenAccumulatorVar(true).build()) .setStandardMacros(CelStandardMacro.STANDARD_MACROS) .build(); CelAbstractSyntaxTree ast = celCompiler.compile("[1, 2, 3].exists(x, x > 0)").getAst(); @@ -283,7 +286,7 @@ public void comprehension() throws Exception { "COMPREHENSION [17] {\n" + " iter_var: x\n" + " iter_range: {\n" - + " CREATE_LIST [1] {\n" + + " LIST [1] {\n" + " elements: {\n" + " CONSTANT [2] { value: 1 }\n" + " CONSTANT [3] { value: 2 }\n" @@ -291,7 +294,7 @@ public void comprehension() throws Exception { + " }\n" + " }\n" + " }\n" - + " accu_var: __result__\n" + + " accu_var: @result\n" + " accu_init: {\n" + " CONSTANT [10] { value: false }\n" + " }\n" @@ -303,7 +306,7 @@ public void comprehension() throws Exception { + " function: !_\n" + " args: {\n" + " IDENT [11] {\n" - + " name: __result__\n" + + " name: @result\n" + " }\n" + " }\n" + " }\n" @@ -315,7 +318,7 @@ public void comprehension() throws Exception { + " function: _||_\n" + " args: {\n" + " IDENT [14] {\n" - + " name: __result__\n" + + " name: @result\n" + " }\n" + " CALL [8] {\n" + " function: _>_\n" @@ -331,7 +334,7 @@ public void comprehension() throws Exception { + " }\n" + " result: {\n" + " IDENT [16] {\n" - + " name: __result__\n" + + " name: @result\n" + " }\n" + " }\n" + "}"); diff --git a/common/src/test/java/dev/cel/common/ast/CelExprTest.java b/common/src/test/java/dev/cel/common/ast/CelExprTest.java index 2e252e459..e1bd48692 100644 --- a/common/src/test/java/dev/cel/common/ast/CelExprTest.java +++ b/common/src/test/java/dev/cel/common/ast/CelExprTest.java @@ -21,11 +21,11 @@ import com.google.testing.junit.testparameterinjector.TestParameterInjector; import dev.cel.common.ast.CelExpr.CelCall; import dev.cel.common.ast.CelExpr.CelComprehension; -import dev.cel.common.ast.CelExpr.CelCreateList; -import dev.cel.common.ast.CelExpr.CelCreateMap; -import dev.cel.common.ast.CelExpr.CelCreateStruct; import dev.cel.common.ast.CelExpr.CelIdent; +import dev.cel.common.ast.CelExpr.CelList; +import dev.cel.common.ast.CelExpr.CelMap; import dev.cel.common.ast.CelExpr.CelSelect; +import dev.cel.common.ast.CelExpr.CelStruct; import dev.cel.common.ast.CelExpr.ExprKind; import dev.cel.common.ast.CelExpr.ExprKind.Kind; import org.junit.Test; @@ -66,15 +66,9 @@ private enum BuilderExprKindTestCase { .build()) .build(), Kind.SELECT), - CREATE_MAP( - CelExpr.newBuilder().setCreateMap(CelCreateMap.newBuilder().build()).build(), - Kind.CREATE_MAP), - CREATE_LIST( - CelExpr.newBuilder().setCreateList(CelCreateList.newBuilder().build()).build(), - Kind.CREATE_LIST), - CREATE_STRUCT( - CelExpr.newBuilder().setCreateStruct(CelCreateStruct.newBuilder().build()).build(), - Kind.CREATE_STRUCT), + MAP(CelExpr.newBuilder().setMap(CelMap.newBuilder().build()).build(), Kind.MAP), + LIST(CelExpr.newBuilder().setList(CelList.newBuilder().build()).build(), Kind.LIST), + STRUCT(CelExpr.newBuilder().setStruct(CelStruct.newBuilder().build()).build(), Kind.STRUCT), COMPREHENSION( CelExpr.newBuilder() .setComprehension( @@ -141,7 +135,7 @@ public void celExprBuilder_setCall_clearTarget() { CelCall celCall = CelCall.newBuilder() .setFunction("function") - .setTarget(CelExpr.ofConstantExpr(1, CelConstant.ofValue("test"))) + .setTarget(CelExpr.ofConstant(1, CelConstant.ofValue("test"))) .build(); CelExpr celExpr = CelExpr.newBuilder().setCall(celCall.toBuilder().clearTarget().build()).build(); @@ -156,11 +150,11 @@ public void callBuilder_getArgs() { CelCall celCall = CelCall.newBuilder() .setFunction("function") - .addArgs(CelExpr.ofConstantExpr(1, CelConstant.ofValue("test"))) + .addArgs(CelExpr.ofConstant(1, CelConstant.ofValue("test"))) .build(); assertThat(celCall.toBuilder().getArgs()) - .containsExactly(CelExpr.ofConstantExpr(1, CelConstant.ofValue("test"))); + .containsExactly(CelExpr.ofConstant(1, CelConstant.ofValue("test"))); } @Test @@ -169,15 +163,15 @@ public void celExprBuilder_setCall_setArgByIndex() { CelCall.newBuilder() .setFunction("function") .addArgs( - CelExpr.ofConstantExpr(5, CelConstant.ofValue("hello")), - CelExpr.ofConstantExpr(6, CelConstant.ofValue(5))) + CelExpr.ofConstant(5, CelConstant.ofValue("hello")), + CelExpr.ofConstant(6, CelConstant.ofValue(5))) .build(); CelExpr celExpr = CelExpr.newBuilder() .setCall( celCall.toBuilder() - .setArg(1, CelExpr.ofConstantExpr(7, CelConstant.ofValue("world"))) + .setArg(1, CelExpr.ofConstant(7, CelConstant.ofValue("world"))) .build()) .build(); @@ -186,8 +180,8 @@ public void celExprBuilder_setCall_setArgByIndex() { CelCall.newBuilder() .setFunction("function") .addArgs( - CelExpr.ofConstantExpr(5, CelConstant.ofValue("hello")), - CelExpr.ofConstantExpr(7, CelConstant.ofValue("world"))) + CelExpr.ofConstant(5, CelConstant.ofValue("hello")), + CelExpr.ofConstant(7, CelConstant.ofValue("world"))) .build()); } @@ -203,87 +197,83 @@ public void celExprBuilder_setSelect() { } @Test - public void celExprBuilder_setCreateList() { - CelCreateList celCreateList = - CelCreateList.newBuilder() - .addElements(CelExpr.ofConstantExpr(1, CelConstant.ofValue(2))) - .build(); - CelExpr celExpr = CelExpr.newBuilder().setCreateList(celCreateList).build(); + public void celExprBuilder_setList() { + CelList celList = + CelList.newBuilder().addElements(CelExpr.ofConstant(1, CelConstant.ofValue(2))).build(); + CelExpr celExpr = CelExpr.newBuilder().setList(celList).build(); - assertThat(celExpr.createList()).isEqualTo(celCreateList); - assertThat(celExpr.toBuilder().createList()).isEqualTo(celCreateList); + assertThat(celExpr.list()).isEqualTo(celList); + assertThat(celExpr.toBuilder().list()).isEqualTo(celList); } @Test - public void createListBuilder_getArgs() { - CelCreateList celCreateList = - CelCreateList.newBuilder() - .addElements(CelExpr.ofConstantExpr(1, CelConstant.ofValue(2))) - .build(); + public void listBuilder_getArgs() { + CelList celList = + CelList.newBuilder().addElements(CelExpr.ofConstant(1, CelConstant.ofValue(2))).build(); - assertThat(celCreateList.toBuilder().getElements()) - .containsExactly(CelExpr.ofConstantExpr(1, CelConstant.ofValue(2))); + assertThat(celList.toBuilder().getElements()) + .containsExactly(CelExpr.ofConstant(1, CelConstant.ofValue(2))); } @Test - public void celExprBuilder_setCreateList_setElementByIndex() { - CelCreateList celCreateList = - CelCreateList.newBuilder() + public void celExprBuilder_setList_setElementByIndex() { + CelList celList = + CelList.newBuilder() .addElements( - CelExpr.ofConstantExpr(5, CelConstant.ofValue("hello")), - CelExpr.ofConstantExpr(6, CelConstant.ofValue(5))) + CelExpr.ofConstant(5, CelConstant.ofValue("hello")), + CelExpr.ofConstant(6, CelConstant.ofValue(5))) .build(); CelExpr celExpr = CelExpr.newBuilder() - .setCreateList( - celCreateList.toBuilder() - .setElement(1, CelExpr.ofConstantExpr(7, CelConstant.ofValue("world"))) + .setList( + celList.toBuilder() + .setElement(1, CelExpr.ofConstant(7, CelConstant.ofValue("world"))) .build()) .build(); - assertThat(celExpr.createList()) + assertThat(celExpr.list()) .isEqualTo( - CelCreateList.newBuilder() + CelList.newBuilder() .addElements( - CelExpr.ofConstantExpr(5, CelConstant.ofValue("hello")), - CelExpr.ofConstantExpr(7, CelConstant.ofValue("world"))) + CelExpr.ofConstant(5, CelConstant.ofValue("hello")), + CelExpr.ofConstant(7, CelConstant.ofValue("world"))) .build()); } @Test - public void celExprBuilder_setCreateStruct() { - CelCreateStruct celCreateStruct = - CelCreateStruct.newBuilder() + public void celExprBuilder_setStruct() { + CelStruct celStruct = + CelStruct.newBuilder() .addEntries( - CelCreateStruct.Entry.newBuilder() + CelStruct.Entry.newBuilder() .setId(1) .setValue(CelExpr.newBuilder().build()) .setFieldKey("field_key") .build()) .build(); - CelExpr celExpr = CelExpr.newBuilder().setCreateStruct(celCreateStruct).build(); + CelExpr celExpr = CelExpr.newBuilder().setStruct(celStruct).build(); - assertThat(celExpr.createStruct().entries().get(0).optionalEntry()).isFalse(); - assertThat(celExpr.createStruct()).isEqualTo(celCreateStruct); - assertThat(celExpr.toBuilder().createStruct()).isEqualTo(celCreateStruct); + assertThat(celExpr.struct().entries().get(0).optionalEntry()).isFalse(); + assertThat(celExpr.struct()).isEqualTo(celStruct); + assertThat(celExpr.toBuilder().struct()).isEqualTo(celStruct); } @Test - public void createStructBuilder_getArgs() { - CelCreateStruct celCreateStruct = - CelCreateStruct.newBuilder() + public void structBuilder_getArgs() { + CelStruct celStruct = + CelStruct.newBuilder() .addEntries( - CelCreateStruct.Entry.newBuilder() + CelStruct.Entry.newBuilder() .setId(1) .setValue(CelExpr.newBuilder().build()) .setFieldKey("field_key") .build()) .build(); - assertThat(celCreateStruct.toBuilder().getEntries()) + assertThat(celStruct.toBuilder().getEntries()) .containsExactly( - CelCreateStruct.Entry.newBuilder() + CelStruct.Entry.newBuilder() .setId(1) .setValue(CelExpr.newBuilder().build()) .setFieldKey("field_key") @@ -291,56 +281,53 @@ public void createStructBuilder_getArgs() { } @Test - public void celExprBuilder_setCreateStruct_setEntryByIndex() { - CelCreateStruct celCreateStruct = - CelCreateStruct.newBuilder() + public void celExprBuilder_setStruct_setEntryByIndex() { + CelStruct celStruct = + CelStruct.newBuilder() .addEntries( - CelCreateStruct.Entry.newBuilder() + CelStruct.Entry.newBuilder() .setId(2) .setValue( - CelExpr.ofConstantExpr(5, CelConstant.ofValue("hello")).toBuilder().build()) + CelExpr.ofConstant(5, CelConstant.ofValue("hello")).toBuilder().build()) .setFieldKey("field_key") .build(), - CelCreateStruct.Entry.newBuilder() + CelStruct.Entry.newBuilder() .setId(3) - .setValue( - CelExpr.ofConstantExpr(6, CelConstant.ofValue(100)).toBuilder().build()) + .setValue(CelExpr.ofConstant(6, CelConstant.ofValue(100)).toBuilder().build()) .setFieldKey("field_key") .build()) .build(); CelExpr celExpr = CelExpr.newBuilder() - .setCreateStruct( - celCreateStruct.toBuilder() + .setStruct( + celStruct.toBuilder() .setEntry( 1, - CelCreateStruct.Entry.newBuilder() + CelStruct.Entry.newBuilder() .setId(4) .setValue( - CelExpr.ofConstantExpr(6, CelConstant.ofValue("world")).toBuilder() + CelExpr.ofConstant(6, CelConstant.ofValue("world")).toBuilder() .build()) .setFieldKey("field_key") .build()) .build()) .build(); - assertThat(celExpr.createStruct()) + assertThat(celExpr.struct()) .isEqualTo( - CelCreateStruct.newBuilder() + CelStruct.newBuilder() .addEntries( - CelCreateStruct.Entry.newBuilder() + CelStruct.Entry.newBuilder() .setId(2) .setValue( - CelExpr.ofConstantExpr(5, CelConstant.ofValue("hello")).toBuilder() - .build()) + CelExpr.ofConstant(5, CelConstant.ofValue("hello")).toBuilder().build()) .setFieldKey("field_key") .build(), - CelCreateStruct.Entry.newBuilder() + CelStruct.Entry.newBuilder() .setId(4) .setValue( - CelExpr.ofConstantExpr(6, CelConstant.ofValue("world")).toBuilder() - .build()) + CelExpr.ofConstant(6, CelConstant.ofValue("world")).toBuilder().build()) .setFieldKey("field_key") .build()) .build()); @@ -364,6 +351,25 @@ public void celExprBuilder_setComprehension() { assertThat(celExpr.toBuilder().comprehension()).isEqualTo(celComprehension); } + @Test + public void celExprBuilder_setComprehensionV2() { + CelComprehension celComprehension = + CelComprehension.newBuilder() + .setIterVar("iterVar") + .setIterVar2("iterVar2") + .setIterRange(CelExpr.newBuilder().build()) + .setAccuVar("accuVar") + .setAccuInit(CelExpr.newBuilder().build()) + .setLoopCondition(CelExpr.newBuilder().build()) + .setLoopStep(CelExpr.newBuilder().build()) + .setResult(CelExpr.newBuilder().build()) + .build(); + CelExpr celExpr = CelExpr.newBuilder().setComprehension(celComprehension).build(); + + assertThat(celExpr.comprehension()).isEqualTo(celComprehension); + assertThat(celExpr.toBuilder().comprehension()).isEqualTo(celComprehension); + } + @Test public void getUnderlyingExpression_unmatchedKind_throws( @TestParameter BuilderExprKindTestCase testCase) { @@ -388,20 +394,17 @@ public void getUnderlyingExpression_unmatchedKind_throws( assertThrows(UnsupportedOperationException.class, testCase.expr::call); assertThrows(UnsupportedOperationException.class, () -> testCase.expr.toBuilder().call()); } - if (!testCase.expectedExprKind.equals(Kind.CREATE_LIST)) { - assertThrows(UnsupportedOperationException.class, testCase.expr::createList); - assertThrows( - UnsupportedOperationException.class, () -> testCase.expr.toBuilder().createList()); + if (!testCase.expectedExprKind.equals(Kind.LIST)) { + assertThrows(UnsupportedOperationException.class, testCase.expr::list); + assertThrows(UnsupportedOperationException.class, () -> testCase.expr.toBuilder().list()); } - if (!testCase.expectedExprKind.equals(Kind.CREATE_STRUCT)) { - assertThrows(UnsupportedOperationException.class, testCase.expr::createStruct); - assertThrows( - UnsupportedOperationException.class, () -> testCase.expr.toBuilder().createStruct()); + if (!testCase.expectedExprKind.equals(Kind.STRUCT)) { + assertThrows(UnsupportedOperationException.class, testCase.expr::struct); + assertThrows(UnsupportedOperationException.class, () -> testCase.expr.toBuilder().struct()); } - if (!testCase.expectedExprKind.equals(Kind.CREATE_MAP)) { - assertThrows(UnsupportedOperationException.class, testCase.expr::createMap); - assertThrows( - UnsupportedOperationException.class, () -> testCase.expr.toBuilder().createMap()); + if (!testCase.expectedExprKind.equals(Kind.MAP)) { + assertThrows(UnsupportedOperationException.class, testCase.expr::map); + assertThrows(UnsupportedOperationException.class, () -> testCase.expr.toBuilder().map()); } if (!testCase.expectedExprKind.equals(Kind.COMPREHENSION)) { assertThrows(UnsupportedOperationException.class, testCase.expr::comprehension); @@ -425,15 +428,14 @@ public void getDefault_unmatchedKind_returnsDefaultInstance( if (!testCase.expectedExprKind.equals(Kind.CALL)) { assertThat(testCase.expr.callOrDefault()).isEqualTo(CelCall.newBuilder().build()); } - if (!testCase.expectedExprKind.equals(Kind.CREATE_LIST)) { - assertThat(testCase.expr.createListOrDefault()).isEqualTo(CelCreateList.newBuilder().build()); + if (!testCase.expectedExprKind.equals(Kind.LIST)) { + assertThat(testCase.expr.listOrDefault()).isEqualTo(CelList.newBuilder().build()); } - if (!testCase.expectedExprKind.equals(Kind.CREATE_STRUCT)) { - assertThat(testCase.expr.createStructOrDefault()) - .isEqualTo(CelCreateStruct.newBuilder().build()); + if (!testCase.expectedExprKind.equals(Kind.STRUCT)) { + assertThat(testCase.expr.structOrDefault()).isEqualTo(CelStruct.newBuilder().build()); } - if (!testCase.expectedExprKind.equals(Kind.CREATE_MAP)) { - assertThat(testCase.expr.createMapOrDefault()).isEqualTo(CelCreateMap.newBuilder().build()); + if (!testCase.expectedExprKind.equals(Kind.MAP)) { + assertThat(testCase.expr.mapOrDefault()).isEqualTo(CelMap.newBuilder().build()); } if (!testCase.expectedExprKind.equals(Kind.COMPREHENSION)) { assertThat(testCase.expr.comprehensionOrDefault()) @@ -460,14 +462,14 @@ public void getDefault_matchedKind_returnsUnderlyingExpression( case CALL: assertThat(testCase.expr.callOrDefault()).isEqualTo(testCase.expr.call()); break; - case CREATE_LIST: - assertThat(testCase.expr.createListOrDefault()).isEqualTo(testCase.expr.createList()); + case LIST: + assertThat(testCase.expr.listOrDefault()).isEqualTo(testCase.expr.list()); break; - case CREATE_STRUCT: - assertThat(testCase.expr.createStructOrDefault()).isEqualTo(testCase.expr.createStruct()); + case STRUCT: + assertThat(testCase.expr.structOrDefault()).isEqualTo(testCase.expr.struct()); break; - case CREATE_MAP: - assertThat(testCase.expr.createMapOrDefault()).isEqualTo(testCase.expr.createMap()); + case MAP: + assertThat(testCase.expr.mapOrDefault()).isEqualTo(testCase.expr.map()); break; case COMPREHENSION: assertThat(testCase.expr.comprehensionOrDefault()).isEqualTo(testCase.expr.comprehension()); @@ -476,46 +478,40 @@ public void getDefault_matchedKind_returnsUnderlyingExpression( } @Test - public void celCreateMapEntry_keyOrValueNotSet_throws() { - assertThrows(IllegalStateException.class, () -> CelCreateMap.Entry.newBuilder().build()); + public void celMapEntry_keyOrValueNotSet_throws() { + assertThrows(IllegalStateException.class, () -> CelMap.Entry.newBuilder().build()); assertThrows( IllegalStateException.class, - () -> CelCreateMap.Entry.newBuilder().setKey(CelExpr.ofNotSet(1)).build()); + () -> CelMap.Entry.newBuilder().setKey(CelExpr.ofNotSet(1)).build()); assertThrows( IllegalStateException.class, - () -> CelCreateMap.Entry.newBuilder().setValue(CelExpr.ofNotSet(1)).build()); + () -> CelMap.Entry.newBuilder().setValue(CelExpr.ofNotSet(1)).build()); } @Test - public void celCreateMapEntry_default() { - CelCreateMap.Entry entry = - CelCreateMap.Entry.newBuilder() - .setKey(CelExpr.ofNotSet(1)) - .setValue(CelExpr.ofNotSet(2)) - .build(); + public void celMapEntry_default() { + CelMap.Entry entry = + CelMap.Entry.newBuilder().setKey(CelExpr.ofNotSet(1)).setValue(CelExpr.ofNotSet(2)).build(); assertThat(entry.id()).isEqualTo(0); assertThat(entry.optionalEntry()).isFalse(); } @Test - public void celCreateStructEntry_fieldKeyOrValueNotSet_throws() { - assertThrows(IllegalStateException.class, () -> CelCreateStruct.Entry.newBuilder().build()); + public void celStructEntry_fieldKeyOrValueNotSet_throws() { + assertThrows(IllegalStateException.class, () -> CelStruct.Entry.newBuilder().build()); assertThrows( IllegalStateException.class, - () -> CelCreateStruct.Entry.newBuilder().setFieldKey("fieldKey").build()); + () -> CelStruct.Entry.newBuilder().setFieldKey("fieldKey").build()); assertThrows( IllegalStateException.class, - () -> CelCreateStruct.Entry.newBuilder().setValue(CelExpr.ofNotSet(1)).build()); + () -> CelStruct.Entry.newBuilder().setValue(CelExpr.ofNotSet(1)).build()); } @Test - public void celCreateStructEntry_default() { - CelCreateStruct.Entry entry = - CelCreateStruct.Entry.newBuilder() - .setFieldKey("fieldKey") - .setValue(CelExpr.ofNotSet(1)) - .build(); + public void celStructEntry_default() { + CelStruct.Entry entry = + CelStruct.Entry.newBuilder().setFieldKey("fieldKey").setValue(CelExpr.ofNotSet(1)).build(); assertThat(entry.id()).isEqualTo(0); assertThat(entry.optionalEntry()).isFalse(); diff --git a/common/src/test/java/dev/cel/common/ast/CelExprV1Alpha1ConverterTest.java b/common/src/test/java/dev/cel/common/ast/CelExprV1Alpha1ConverterTest.java index 283fecafa..193bfa2df 100644 --- a/common/src/test/java/dev/cel/common/ast/CelExprV1Alpha1ConverterTest.java +++ b/common/src/test/java/dev/cel/common/ast/CelExprV1Alpha1ConverterTest.java @@ -33,6 +33,7 @@ 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.values.CelByteString; import java.util.Optional; import org.junit.Test; import org.junit.runner.RunWith; @@ -43,50 +44,50 @@ public class CelExprV1Alpha1ConverterTest { private enum ConstantTestCase { NOT_SET( Expr.newBuilder().setId(1).setConstExpr(Constant.getDefaultInstance()).build(), - CelExpr.ofConstantExpr(1, CelConstant.ofNotSet())), + CelExpr.ofConstant(1, CelConstant.ofNotSet())), NULL( Expr.newBuilder() .setId(1) .setConstExpr(Constant.newBuilder().setNullValue(NullValue.NULL_VALUE).build()) .build(), - CelExpr.ofConstantExpr(1, CelConstant.ofValue(NullValue.NULL_VALUE))), + CelExpr.ofConstant(1, CelConstant.ofValue(dev.cel.common.values.NullValue.NULL_VALUE))), BOOLEAN( Expr.newBuilder() .setId(1) .setConstExpr(Constant.newBuilder().setBoolValue(true).build()) .build(), - CelExpr.ofConstantExpr(1, CelConstant.ofValue(true))), + CelExpr.ofConstant(1, CelConstant.ofValue(true))), INT64( Expr.newBuilder() .setId(1) .setConstExpr(Constant.newBuilder().setInt64Value(10).build()) .build(), - CelExpr.ofConstantExpr(1, CelConstant.ofValue(10))), + CelExpr.ofConstant(1, CelConstant.ofValue(10))), UINT64( Expr.newBuilder() .setId(1) .setConstExpr(Constant.newBuilder().setUint64Value(15).build()) .build(), - CelExpr.ofConstantExpr(1, CelConstant.ofValue(UnsignedLong.valueOf(15)))), + CelExpr.ofConstant(1, CelConstant.ofValue(UnsignedLong.valueOf(15)))), DOUBLE( Expr.newBuilder() .setId(1) .setConstExpr(Constant.newBuilder().setDoubleValue(1.5).build()) .build(), - CelExpr.ofConstantExpr(1, CelConstant.ofValue(1.5))), + CelExpr.ofConstant(1, CelConstant.ofValue(1.5))), STRING( Expr.newBuilder() .setId(1) .setConstExpr(Constant.newBuilder().setStringValue("Test").build()) .build(), - CelExpr.ofConstantExpr(1, CelConstant.ofValue("Test"))), + CelExpr.ofConstant(1, CelConstant.ofValue("Test"))), BYTES( Expr.newBuilder() .setId(1) .setConstExpr( Constant.newBuilder().setBytesValue(ByteString.copyFromUtf8("TEST")).build()) .build(), - CelExpr.ofConstantExpr(1, CelConstant.ofValue(ByteString.copyFromUtf8("TEST")))); + CelExpr.ofConstant(1, CelConstant.ofValue(CelByteString.copyFromUtf8("TEST")))); final Expr protoExpr; final CelExpr celExpr; @@ -122,7 +123,7 @@ public void convertExprIdent_toCelIdent() { CelExpr celExpr = CelExprV1Alpha1Converter.fromExpr(expr); - assertThat(celExpr).isEqualTo(CelExpr.ofIdentExpr(2, "Test")); + assertThat(celExpr).isEqualTo(CelExpr.ofIdent(2, "Test")); } @Test @@ -146,8 +147,8 @@ public void convertExprSelect_toCelSelect(boolean isTestOnly) { assertThat(celExpr) .isEqualTo( - CelExpr.ofSelectExpr( - 3, CelExpr.ofConstantExpr(4, CelConstant.ofValue(true)), "field", isTestOnly)); + CelExpr.ofSelect( + 3, CelExpr.ofConstant(4, CelConstant.ofValue(true)), "field", isTestOnly)); } @Test @@ -167,11 +168,11 @@ public void convertExprCall_toCelCall() { assertThat(celExpr) .isEqualTo( - CelExpr.ofCallExpr( + CelExpr.ofCall( 1, - Optional.of(CelExpr.ofConstantExpr(2, CelConstant.ofValue(10))), + Optional.of(CelExpr.ofConstant(2, CelConstant.ofValue(10))), "func", - ImmutableList.of(CelExpr.ofConstantExpr(3, CelConstant.ofValue(20))))); + ImmutableList.of(CelExpr.ofConstant(3, CelConstant.ofValue(20))))); } @Test @@ -191,11 +192,11 @@ public void convertExprList_toCelList() { assertThat(celExpr) .isEqualTo( - CelExpr.ofCreateListExpr( + CelExpr.ofList( 1, ImmutableList.of( - CelExpr.ofConstantExpr(2, CelConstant.ofValue(10)), - CelExpr.ofConstantExpr(3, CelConstant.ofValue(15))), + CelExpr.ofConstant(2, CelConstant.ofValue(10)), + CelExpr.ofConstant(3, CelConstant.ofValue(15))), ImmutableList.of(1))); } @@ -221,12 +222,12 @@ public void convertExprStructExpr_toCelStruct() { assertThat(celExpr) .isEqualTo( - CelExpr.ofCreateStructExpr( + CelExpr.ofStruct( 1, "messageName", ImmutableList.of( - CelExpr.ofCreateStructEntryExpr( - 2, "fieldKey", CelExpr.ofConstantExpr(3, CelConstant.ofValue(10)), true)))); + CelExpr.ofStructEntry( + 2, "fieldKey", CelExpr.ofConstant(3, CelConstant.ofValue(10)), true)))); } @Test @@ -299,13 +300,13 @@ public void convertExprStructExpr_toCelMap() { assertThat(celExpr) .isEqualTo( - CelExpr.ofCreateMapExpr( + CelExpr.ofMap( 1, ImmutableList.of( - CelExpr.ofCreateMapEntryExpr( + CelExpr.ofMapEntry( 2, - CelExpr.ofConstantExpr(3, CelConstant.ofValue(15)), - CelExpr.ofConstantExpr(4, CelConstant.ofValue(10)), + CelExpr.ofConstant(3, CelConstant.ofValue(15)), + CelExpr.ofConstant(4, CelConstant.ofValue(10)), true)))); } @@ -341,12 +342,12 @@ public void convertExprComprehensionExpr_toCelComprehension() { CelExpr.ofComprehension( 1, "iterVar", - CelExpr.ofConstantExpr(2, CelConstant.ofValue(10)), + CelExpr.ofConstant(2, CelConstant.ofValue(10)), "accuVar", - CelExpr.ofConstantExpr(3, CelConstant.ofValue(20)), - CelExpr.ofCallExpr(4, Optional.empty(), "testCondition", ImmutableList.of()), - CelExpr.ofCallExpr(5, Optional.empty(), "testStep", ImmutableList.of()), - CelExpr.ofConstantExpr(6, CelConstant.ofValue(30)))); + CelExpr.ofConstant(3, CelConstant.ofValue(20)), + CelExpr.ofCall(4, Optional.empty(), "testCondition", ImmutableList.of()), + CelExpr.ofCall(5, Optional.empty(), "testStep", ImmutableList.of()), + CelExpr.ofConstant(6, CelConstant.ofValue(30)))); } @Test @@ -390,7 +391,7 @@ public void convertCelNotSet_toExprNotSet() { @Test public void convertCelIdent_toExprIdent() { - CelExpr celExpr = CelExpr.ofIdentExpr(2, "Test"); + CelExpr celExpr = CelExpr.ofIdent(2, "Test"); Expr expr = CelExprV1Alpha1Converter.fromCelExpr(celExpr); @@ -407,8 +408,7 @@ public void convertCelIdent_toExprIdent() { @TestParameters("{isTestOnly: false}") public void convertCelSelect_toExprSelect(boolean isTestOnly) { CelExpr celExpr = - CelExpr.ofSelectExpr( - 3, CelExpr.ofConstantExpr(4, CelConstant.ofValue(true)), "field", isTestOnly); + CelExpr.ofSelect(3, CelExpr.ofConstant(4, CelConstant.ofValue(true)), "field", isTestOnly); Expr expr = CelExprV1Alpha1Converter.fromCelExpr(celExpr); @@ -430,11 +430,11 @@ public void convertCelSelect_toExprSelect(boolean isTestOnly) { @Test public void convertCelCall_toExprCall() { CelExpr celExpr = - CelExpr.ofCallExpr( + CelExpr.ofCall( 1, - Optional.of(CelExpr.ofConstantExpr(2, CelConstant.ofValue(10))), + Optional.of(CelExpr.ofConstant(2, CelConstant.ofValue(10))), "func", - ImmutableList.of(CelExpr.ofConstantExpr(3, CelConstant.ofValue(20)))); + ImmutableList.of(CelExpr.ofConstant(3, CelConstant.ofValue(20)))); Expr expr = CelExprV1Alpha1Converter.fromCelExpr(celExpr); @@ -454,11 +454,11 @@ public void convertCelCall_toExprCall() { @Test public void convertCelList_toExprList() { CelExpr celExpr = - CelExpr.ofCreateListExpr( + CelExpr.ofList( 1, ImmutableList.of( - CelExpr.ofConstantExpr(2, CelConstant.ofValue(10)), - CelExpr.ofConstantExpr(3, CelConstant.ofValue(15))), + CelExpr.ofConstant(2, CelConstant.ofValue(10)), + CelExpr.ofConstant(3, CelConstant.ofValue(15))), ImmutableList.of(1)); Expr expr = CelExprV1Alpha1Converter.fromCelExpr(celExpr); @@ -479,12 +479,12 @@ public void convertCelList_toExprList() { @Test public void convertCelStructExpr_toExprStruct_withFieldKey() { CelExpr celExpr = - CelExpr.ofCreateStructExpr( + CelExpr.ofStruct( 1, "messageName", ImmutableList.of( - CelExpr.ofCreateStructEntryExpr( - 2, "fieldKey", CelExpr.ofConstantExpr(3, CelConstant.ofValue(10)), true))); + CelExpr.ofStructEntry( + 2, "fieldKey", CelExpr.ofConstant(3, CelConstant.ofValue(10)), true))); Expr expr = CelExprV1Alpha1Converter.fromCelExpr(celExpr); @@ -509,13 +509,13 @@ public void convertCelStructExpr_toExprStruct_withFieldKey() { @Test public void convertCelMapExpr_toExprStruct() { CelExpr celExpr = - CelExpr.ofCreateMapExpr( + CelExpr.ofMap( 1, ImmutableList.of( - CelExpr.ofCreateMapEntryExpr( + CelExpr.ofMapEntry( 2, - CelExpr.ofConstantExpr(3, CelConstant.ofValue(15)), - CelExpr.ofConstantExpr(4, CelConstant.ofValue(10)), + CelExpr.ofConstant(3, CelConstant.ofValue(15)), + CelExpr.ofConstant(4, CelConstant.ofValue(10)), true))); Expr expr = CelExprV1Alpha1Converter.fromCelExpr(celExpr); @@ -543,12 +543,12 @@ public void convertCelComprehensionExpr_toExprComprehension() { CelExpr.ofComprehension( 1, "iterVar", - CelExpr.ofConstantExpr(2, CelConstant.ofValue(10)), + CelExpr.ofConstant(2, CelConstant.ofValue(10)), "accuVar", - CelExpr.ofConstantExpr(3, CelConstant.ofValue(20)), - CelExpr.ofCallExpr(4, Optional.empty(), "testCondition", ImmutableList.of()), - CelExpr.ofCallExpr(5, Optional.empty(), "testStep", ImmutableList.of()), - CelExpr.ofConstantExpr(6, CelConstant.ofValue(30))); + CelExpr.ofConstant(3, CelConstant.ofValue(20)), + CelExpr.ofCall(4, Optional.empty(), "testCondition", ImmutableList.of()), + CelExpr.ofCall(5, Optional.empty(), "testStep", ImmutableList.of()), + CelExpr.ofConstant(6, CelConstant.ofValue(30))); Expr expr = CelExprV1Alpha1Converter.fromCelExpr(celExpr); diff --git a/common/src/test/java/dev/cel/common/ast/CelExprVisitorTest.java b/common/src/test/java/dev/cel/common/ast/CelExprVisitorTest.java index 1926eb31e..97f1a4a1b 100644 --- a/common/src/test/java/dev/cel/common/ast/CelExprVisitorTest.java +++ b/common/src/test/java/dev/cel/common/ast/CelExprVisitorTest.java @@ -20,20 +20,22 @@ import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableList; import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelContainer; +import dev.cel.common.CelOptions; +import dev.cel.common.Operator; import dev.cel.common.ast.CelExpr.CelCall; import dev.cel.common.ast.CelExpr.CelComprehension; -import dev.cel.common.ast.CelExpr.CelCreateList; -import dev.cel.common.ast.CelExpr.CelCreateMap; -import dev.cel.common.ast.CelExpr.CelCreateStruct; -import dev.cel.common.ast.CelExpr.CelCreateStruct.Entry; import dev.cel.common.ast.CelExpr.CelIdent; +import dev.cel.common.ast.CelExpr.CelList; +import dev.cel.common.ast.CelExpr.CelMap; import dev.cel.common.ast.CelExpr.CelSelect; +import dev.cel.common.ast.CelExpr.CelStruct; +import dev.cel.common.ast.CelExpr.CelStruct.Entry; 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.parser.CelStandardMacro; -import dev.cel.parser.Operator; -import dev.cel.testing.testdata.proto3.TestAllTypesProto.TestAllTypes; import java.util.Optional; import org.junit.Before; import org.junit.Test; @@ -53,11 +55,11 @@ public abstract static class VisitedReference { public abstract Optional call(); - public abstract Optional createStruct(); + public abstract Optional struct(); - public abstract Optional createMap(); + public abstract Optional map(); - public abstract Optional createList(); + public abstract Optional list(); public abstract Optional comprehension(); @@ -73,11 +75,11 @@ public abstract static class Builder { public abstract Builder setCall(CelCall value); - public abstract Builder setCreateStruct(CelCreateStruct value); + public abstract Builder setStruct(CelStruct value); - public abstract Builder setCreateMap(CelCreateMap value); + public abstract Builder setMap(CelMap value); - public abstract Builder setCreateList(CelCreateList value); + public abstract Builder setList(CelList value); public abstract Builder setComprehension(CelComprehension value); @@ -124,21 +126,21 @@ protected void visit(CelExpr expr, CelCall call) { } @Override - protected void visit(CelExpr expr, CelCreateStruct createStruct) { - visitedReference.setCreateStruct(createStruct); - super.visit(expr, createStruct); + protected void visit(CelExpr expr, CelStruct struct) { + visitedReference.setStruct(struct); + super.visit(expr, struct); } @Override - protected void visit(CelExpr expr, CelCreateMap createMap) { - visitedReference.setCreateMap(createMap); - super.visit(expr, createMap); + protected void visit(CelExpr expr, CelMap map) { + visitedReference.setMap(map); + super.visit(expr, map); } @Override - protected void visit(CelExpr expr, CelCreateList createList) { - visitedReference.setCreateList(createList); - super.visit(expr, createList); + protected void visit(CelExpr expr, CelList list) { + visitedReference.setList(list); + super.visit(expr, list); } @Override @@ -194,7 +196,7 @@ public void visitSelect() throws Exception { CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder() .addMessageTypes(TestAllTypes.getDescriptor()) - .setContainer(TestAllTypes.getDescriptor().getFullName()) + .setContainer(CelContainer.ofName(TestAllTypes.getDescriptor().getFullName())) .build(); CelAbstractSyntaxTree ast = celCompiler.compile("TestAllTypes{}.single_int64").getAst(); @@ -204,16 +206,18 @@ public void visitSelect() throws Exception { assertThat(visited) .isEqualTo( VisitedReference.newBuilder() - .setCreateStruct( - CelCreateStruct.newBuilder().setMessageName("TestAllTypes").build()) + .setStruct( + CelStruct.newBuilder() + .setMessageName("cel.expr.conformance.proto3.TestAllTypes") + .build()) .setSelect( CelSelect.newBuilder() .setOperand( CelExpr.newBuilder() .setId(1) - .setCreateStruct( - CelCreateStruct.newBuilder() - .setMessageName("TestAllTypes") + .setStruct( + CelStruct.newBuilder() + .setMessageName("cel.expr.conformance.proto3.TestAllTypes") .build()) .build()) .setField("single_int64") @@ -237,19 +241,19 @@ public void visitCall() throws Exception { .setCall( CelCall.newBuilder() .setFunction("contains") - .setTarget(CelExpr.ofConstantExpr(1, CelConstant.ofValue("hi"))) - .addArgs(CelExpr.ofConstantExpr(3, stringVal)) + .setTarget(CelExpr.ofConstant(1, CelConstant.ofValue("hi"))) + .addArgs(CelExpr.ofConstant(3, stringVal)) .build()) - .addArguments(CelExpr.ofConstantExpr(3, stringVal)) + .addArguments(CelExpr.ofConstant(3, stringVal)) .build()); } @Test - public void visitCreateStruct_fieldkey() throws Exception { + public void visitStruct_fieldkey() throws Exception { CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder() .addMessageTypes(TestAllTypes.getDescriptor()) - .setContainer(TestAllTypes.getDescriptor().getFullName()) + .setContainer(CelContainer.ofName(TestAllTypes.getDescriptor().getFullName())) .build(); CelAbstractSyntaxTree ast = celCompiler.compile("TestAllTypes{single_int64: 1}").getAst(); @@ -261,21 +265,21 @@ public void visitCreateStruct_fieldkey() throws Exception { .isEqualTo( VisitedReference.newBuilder() .setConstant(longConstant) - .setCreateStruct( - CelCreateStruct.newBuilder() + .setStruct( + CelStruct.newBuilder() .addEntries( Entry.newBuilder() .setId(2) .setFieldKey("single_int64") - .setValue(CelExpr.ofConstantExpr(3, longConstant)) + .setValue(CelExpr.ofConstant(3, longConstant)) .build()) - .setMessageName("TestAllTypes") + .setMessageName("cel.expr.conformance.proto3.TestAllTypes") .build()) .build()); } @Test - public void visitCreateMap() throws Exception { + public void visitMap() throws Exception { CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder().build(); CelAbstractSyntaxTree ast = celCompiler.compile("{'a': 'b'}").getAst(); @@ -286,20 +290,20 @@ public void visitCreateMap() throws Exception { .isEqualTo( VisitedReference.newBuilder() .setConstant(CelConstant.ofValue("b")) - .setCreateMap( - CelCreateMap.newBuilder() + .setMap( + CelMap.newBuilder() .addEntries( - CelCreateMap.Entry.newBuilder() + CelMap.Entry.newBuilder() .setId(2) - .setKey(CelExpr.ofConstantExpr(3, CelConstant.ofValue("a"))) - .setValue(CelExpr.ofConstantExpr(4, CelConstant.ofValue("b"))) + .setKey(CelExpr.ofConstant(3, CelConstant.ofValue("a"))) + .setValue(CelExpr.ofConstant(4, CelConstant.ofValue("b"))) .build()) .build()) .build()); } @Test - public void visitCreateList() throws Exception { + public void visitList() throws Exception { CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder().build(); CelAbstractSyntaxTree ast = celCompiler.compile("[1, 1]").getAst(); @@ -311,8 +315,8 @@ public void visitCreateList() throws Exception { .isEqualTo( VisitedReference.newBuilder() .setConstant(integerVal) - .setCreateList( - CelCreateList.newBuilder() + .setList( + CelList.newBuilder() .addElements(CelExpr.newBuilder().setId(2).setConstant(integerVal).build()) .addElements(CelExpr.newBuilder().setId(3).setConstant(integerVal).build()) .build()) @@ -323,6 +327,7 @@ public void visitCreateList() throws Exception { public void visitComprehension() throws Exception { CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder() + .setOptions(CelOptions.current().enableHiddenAccumulatorVar(true).build()) .setStandardMacros(CelStandardMacro.ALL) .build(); CelAbstractSyntaxTree ast = celCompiler.compile("[1, 1].all(x, x == 1)").getAst(); @@ -333,20 +338,20 @@ public void visitComprehension() throws Exception { CelComprehension comprehension = visitedReference.comprehension().get(); ImmutableList iterRangeElements = ImmutableList.of( - CelExpr.ofConstantExpr(2, CelConstant.ofValue(1)), - CelExpr.ofConstantExpr(3, CelConstant.ofValue(1))); + CelExpr.ofConstant(2, CelConstant.ofValue(1)), + CelExpr.ofConstant(3, CelConstant.ofValue(1))); assertThat(comprehension.iterVar()).isEqualTo("x"); - assertThat(comprehension.iterRange().createList().elements()).isEqualTo(iterRangeElements); + assertThat(comprehension.iterRange().list().elements()).isEqualTo(iterRangeElements); assertThat(comprehension.accuInit().constant()).isEqualTo(CelConstant.ofValue(true)); assertThat(comprehension.loopCondition().call().function()) .isEqualTo(Operator.NOT_STRICTLY_FALSE.getFunction()); assertThat(comprehension.loopStep().call().function()) .isEqualTo(Operator.LOGICAL_AND.getFunction()); assertThat(comprehension.loopStep().call().args()).hasSize(2); - assertThat(visitedReference.createList().get().elements()).isEqualTo(iterRangeElements); + assertThat(visitedReference.list().get().elements()).isEqualTo(iterRangeElements); assertThat(visitedReference.identifier()) - .hasValue(CelIdent.newBuilder().setName("__result__").build()); + .hasValue(CelIdent.newBuilder().setName("@result").build()); assertThat(visitedReference.arguments()).hasSize(10); } diff --git a/common/src/test/java/dev/cel/common/ast/CelMutableAstTest.java b/common/src/test/java/dev/cel/common/ast/CelMutableAstTest.java new file mode 100644 index 000000000..6d2c07b17 --- /dev/null +++ b/common/src/test/java/dev/cel/common/ast/CelMutableAstTest.java @@ -0,0 +1,92 @@ +// 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. + +package dev.cel.common.ast; + +import static com.google.common.truth.Truth.assertThat; + +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelMutableAst; +import dev.cel.common.CelMutableSource; +import dev.cel.common.CelOptions; +import dev.cel.common.types.SimpleType; +import dev.cel.compiler.CelCompiler; +import dev.cel.compiler.CelCompilerFactory; +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 CelMutableAstTest { + + @Test + public void constructMutableAst() { + CelMutableExpr mutableExpr = CelMutableExpr.ofConstant(1L, CelConstant.ofValue("hello world")); + CelMutableSource mutableSource = CelMutableSource.newInstance(); + + CelMutableAst celMutableAst = CelMutableAst.of(mutableExpr, mutableSource); + + assertThat(celMutableAst.expr()).isEqualTo(mutableExpr); + assertThat(celMutableAst.source()).isSameInstanceAs(mutableSource); + } + + @Test + public void fromCelAst_mutableAst_containsMutableExpr() throws Exception { + CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder().build(); + CelAbstractSyntaxTree ast = celCompiler.compile("'hello world'").getAst(); + + CelMutableAst celMutableAst = CelMutableAst.fromCelAst(ast); + + assertThat(celMutableAst.expr()) + .isEqualTo(CelMutableExpr.ofConstant(1L, CelConstant.ofValue("hello world"))); + } + + @Test + public void getType_success() throws Exception { + CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder().build(); + CelAbstractSyntaxTree ast = celCompiler.compile("'hello world'").getAst(); + CelMutableAst celMutableAst = CelMutableAst.fromCelAst(ast); + + assertThat(celMutableAst.getType(1L)).hasValue(SimpleType.STRING); + } + + @Test + public void getReference_success() throws Exception { + CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder().build(); + CelAbstractSyntaxTree ast = celCompiler.compile("size('test')").getAst(); + CelMutableAst celMutableAst = CelMutableAst.fromCelAst(ast); + + assertThat(celMutableAst.getReference(1L)) + .hasValue(CelReference.newBuilder().addOverloadIds("size_string").build()); + } + + @Test + public void parsedAst_roundTrip() throws Exception { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .setOptions(CelOptions.current().populateMacroCalls(true).build()) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .build(); + CelAbstractSyntaxTree ast = celCompiler.parse("[1].exists(x, x > 0)").getAst(); + CelMutableAst celMutableAst = CelMutableAst.fromCelAst(ast); + + CelAbstractSyntaxTree roundTrippedAst = celMutableAst.toParsedAst(); + + assertThat(ast.getExpr()).isEqualTo(roundTrippedAst.getExpr()); + assertThat(roundTrippedAst.getSource().getMacroCalls()).hasSize(1); + assertThat(roundTrippedAst.getSource().getMacroCalls()) + .containsExactlyEntriesIn(ast.getSource().getMacroCalls()); + } +} diff --git a/common/src/test/java/dev/cel/common/ast/CelMutableExprConverterTest.java b/common/src/test/java/dev/cel/common/ast/CelMutableExprConverterTest.java new file mode 100644 index 000000000..76f9f2027 --- /dev/null +++ b/common/src/test/java/dev/cel/common/ast/CelMutableExprConverterTest.java @@ -0,0 +1,514 @@ +// 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. + +package dev.cel.common.ast; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableList; +import com.google.common.primitives.UnsignedLong; +import com.google.testing.junit.testparameterinjector.TestParameter; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.common.ast.CelExpr.CelCall; +import dev.cel.common.ast.CelExpr.CelList; +import dev.cel.common.ast.CelExpr.CelStruct; +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.CelMutableSelect; +import dev.cel.common.ast.CelMutableExpr.CelMutableStruct; +import dev.cel.common.values.CelByteString; +import dev.cel.common.values.NullValue; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public class CelMutableExprConverterTest { + @SuppressWarnings("Immutable") // Mutable by design + private enum ConstantTestCase { + NOT_SET( + CelMutableExpr.ofConstant(1, CelConstant.ofNotSet()), + CelExpr.ofConstant(1, CelConstant.ofNotSet())), + NULL( + CelMutableExpr.ofConstant(1, CelConstant.ofValue(NullValue.NULL_VALUE)), + CelExpr.ofConstant(1, CelConstant.ofValue(NullValue.NULL_VALUE))), + BOOLEAN( + CelMutableExpr.ofConstant(1, CelConstant.ofValue(true)), + CelExpr.ofConstant(1, CelConstant.ofValue(true))), + INT64( + CelMutableExpr.ofConstant(1, CelConstant.ofValue(10)), + CelExpr.ofConstant(1, CelConstant.ofValue(10))), + UINT64( + CelMutableExpr.ofConstant(1, CelConstant.ofValue(UnsignedLong.valueOf(15))), + CelExpr.ofConstant(1, CelConstant.ofValue(UnsignedLong.valueOf(15)))), + DOUBLE( + CelMutableExpr.ofConstant(1, CelConstant.ofValue(1.5)), + CelExpr.ofConstant(1, CelConstant.ofValue(1.5))), + STRING( + CelMutableExpr.ofConstant(1, CelConstant.ofValue("Test")), + CelExpr.ofConstant(1, CelConstant.ofValue("Test"))), + BYTES( + CelMutableExpr.ofConstant(1, CelConstant.ofValue(CelByteString.copyFromUtf8("TEST"))), + CelExpr.ofConstant(1, CelConstant.ofValue(CelByteString.copyFromUtf8("TEST")))); + + final CelMutableExpr mutableExpr; + final CelExpr celExpr; + + ConstantTestCase(CelMutableExpr mutableExpr, CelExpr celExpr) { + this.mutableExpr = mutableExpr; + this.celExpr = celExpr; + } + } + + @Test + public void convertConstant_bidirectional(@TestParameter ConstantTestCase constantTestCase) { + CelExpr convertedCelExpr = + CelMutableExprConverter.fromMutableExpr(constantTestCase.mutableExpr); + CelMutableExpr convertedMutableExpr = + CelMutableExprConverter.fromCelExpr(constantTestCase.celExpr); + + assertThat(convertedCelExpr).isEqualTo(constantTestCase.celExpr); + assertThat(convertedMutableExpr).isEqualTo(constantTestCase.mutableExpr); + } + + @Test + public void convertMutableNotSet_toCelNotSet() { + CelMutableExpr mutableExpr = CelMutableExpr.ofNotSet(1L); + + CelExpr celExpr = CelMutableExprConverter.fromMutableExpr(mutableExpr); + + assertThat(celExpr).isEqualTo(CelExpr.ofNotSet(1L)); + } + + @Test + public void convertCelNotSet_toMutableNotSet() { + CelExpr celExpr = CelExpr.ofNotSet(1L); + + CelMutableExpr mutableExpr = CelMutableExprConverter.fromCelExpr(celExpr); + + assertThat(mutableExpr).isEqualTo(CelMutableExpr.ofNotSet(1L)); + } + + @Test + public void convertMutableIdent_toCelIdent() { + CelMutableExpr mutableExpr = CelMutableExpr.ofIdent(1L, "x"); + + CelExpr celExpr = CelMutableExprConverter.fromMutableExpr(mutableExpr); + + assertThat(celExpr).isEqualTo(CelExpr.ofIdent(1L, "x")); + } + + @Test + public void convertCelIdent_toMutableIdent() { + CelExpr celExpr = CelExpr.ofIdent(1L, "x"); + + CelMutableExpr mutableExpr = CelMutableExprConverter.fromCelExpr(celExpr); + + assertThat(mutableExpr).isEqualTo(CelMutableExpr.ofIdent(1L, "x")); + } + + @Test + public void convertMutableSelect_toCelSelect() { + CelMutableExpr mutableExpr = + CelMutableExpr.ofSelect( + 1L, + CelMutableSelect.create( + CelMutableExpr.ofIdent(2L, "x"), "field", /* testOnly= */ true)); + + CelExpr celExpr = CelMutableExprConverter.fromMutableExpr(mutableExpr); + + assertThat(celExpr) + .isEqualTo(CelExpr.ofSelect(1L, CelExpr.ofIdent(2L, "x"), "field", /* isTestOnly= */ true)); + } + + @Test + public void convertCelSelect_toMutableSelect() { + CelExpr celExpr = + CelExpr.ofSelect(1L, CelExpr.ofIdent(2L, "x"), "field", /* isTestOnly= */ true); + + CelMutableExpr mutableExpr = CelMutableExprConverter.fromCelExpr(celExpr); + + assertThat(mutableExpr) + .isEqualTo( + CelMutableExpr.ofSelect( + 1L, + CelMutableSelect.create( + CelMutableExpr.ofIdent(2L, "x"), "field", /* testOnly= */ true))); + } + + @Test + public void convertMutableCall_toCelCall() { + CelMutableExpr mutableExpr = + CelMutableExpr.ofCall( + 1L, + CelMutableCall.create( + CelMutableExpr.ofConstant(2L, CelConstant.ofValue("target")), + "function", + CelMutableExpr.ofConstant(3L, CelConstant.ofValue("arg")))); + + CelExpr celExpr = CelMutableExprConverter.fromMutableExpr(mutableExpr); + + assertThat(celExpr) + .isEqualTo( + CelExpr.newBuilder() + .setId(1L) + .setCall( + CelCall.newBuilder() + .setFunction("function") + .setTarget(CelExpr.ofConstant(2L, CelConstant.ofValue("target"))) + .addArgs(CelExpr.ofConstant(3L, CelConstant.ofValue("arg"))) + .build()) + .build()); + } + + @Test + public void convertCelCall_toMutableCall() { + CelExpr celExpr = + CelExpr.newBuilder() + .setId(1L) + .setCall( + CelCall.newBuilder() + .setFunction("function") + .setTarget(CelExpr.ofConstant(2L, CelConstant.ofValue("target"))) + .addArgs(CelExpr.ofConstant(3L, CelConstant.ofValue("arg"))) + .build()) + .build(); + + CelMutableExpr mutableExpr = CelMutableExprConverter.fromCelExpr(celExpr); + + assertThat(mutableExpr) + .isEqualTo( + CelMutableExpr.ofCall( + 1L, + CelMutableCall.create( + CelMutableExpr.ofConstant(2L, CelConstant.ofValue("target")), + "function", + CelMutableExpr.ofConstant(3L, CelConstant.ofValue("arg"))))); + } + + @Test + public void convertMutableList_toCelList() { + CelMutableExpr mutableExpr = + CelMutableExpr.ofList( + 1L, + CelMutableList.create( + ImmutableList.of( + CelMutableExpr.ofConstant(2L, CelConstant.ofValue("element1")), + CelMutableExpr.ofConstant(3L, CelConstant.ofValue("element2"))), + ImmutableList.of(0, 1))); + + CelExpr celExpr = CelMutableExprConverter.fromMutableExpr(mutableExpr); + + assertThat(celExpr) + .isEqualTo( + CelExpr.ofList( + 1L, + ImmutableList.of( + CelExpr.ofConstant(2L, CelConstant.ofValue("element1")), + CelExpr.ofConstant(3L, CelConstant.ofValue("element2"))), + ImmutableList.of(0, 1))); + } + + @Test + public void convertCelList_toMutableList() { + CelExpr celExpr = + CelExpr.ofList( + 1L, + ImmutableList.of( + CelExpr.ofConstant(2L, CelConstant.ofValue("element1")), + CelExpr.ofConstant(3L, CelConstant.ofValue("element2"))), + ImmutableList.of(0, 1)); + + CelMutableExpr mutableExpr = CelMutableExprConverter.fromCelExpr(celExpr); + + assertThat(mutableExpr) + .isEqualTo( + CelMutableExpr.ofList( + 1L, + CelMutableList.create( + ImmutableList.of( + CelMutableExpr.ofConstant(2L, CelConstant.ofValue("element1")), + CelMutableExpr.ofConstant(3L, CelConstant.ofValue("element2"))), + ImmutableList.of(0, 1)))); + } + + @Test + public void convertMutableStruct_toCelStruct() { + CelMutableExpr mutableExpr = + CelMutableExpr.ofStruct( + 8L, + CelMutableStruct.create( + "message", + ImmutableList.of( + CelMutableStruct.Entry.create( + 9L, + "field", + CelMutableExpr.ofConstant(10L, CelConstant.ofValue("value")), + /* optionalEntry= */ true)))); + + CelExpr celExpr = CelMutableExprConverter.fromMutableExpr(mutableExpr); + + assertThat(celExpr) + .isEqualTo( + CelExpr.ofStruct( + 8L, + "message", + ImmutableList.of( + CelStruct.Entry.newBuilder() + .setId(9L) + .setFieldKey("field") + .setValue(CelExpr.ofConstant(10L, CelConstant.ofValue("value"))) + .setOptionalEntry(true) + .build()))); + } + + @Test + public void convertCelStruct_toMutableStruct() { + CelExpr celExpr = + CelExpr.ofStruct( + 8L, + "message", + ImmutableList.of( + CelStruct.Entry.newBuilder() + .setId(9L) + .setFieldKey("field") + .setValue(CelExpr.ofConstant(10L, CelConstant.ofValue("value"))) + .setOptionalEntry(true) + .build())); + + CelMutableExpr mutableExpr = CelMutableExprConverter.fromCelExpr(celExpr); + + assertThat(mutableExpr) + .isEqualTo( + CelMutableExpr.ofStruct( + 8L, + CelMutableStruct.create( + "message", + ImmutableList.of( + CelMutableStruct.Entry.create( + 9L, + "field", + CelMutableExpr.ofConstant(10L, CelConstant.ofValue("value")), + /* optionalEntry= */ true))))); + } + + @Test + public void convertMutableMap_toCelMap() { + CelMutableExpr mutableExpr = + CelMutableExpr.ofMap( + 9L, + CelMutableMap.create( + ImmutableList.of( + CelMutableMap.Entry.create( + 10L, + CelMutableExpr.ofConstant(11L, CelConstant.ofValue("key")), + CelMutableExpr.ofConstant(12L, CelConstant.ofValue("value")), + /* optionalEntry= */ true)))); + + CelExpr celExpr = CelMutableExprConverter.fromMutableExpr(mutableExpr); + + assertThat(celExpr) + .isEqualTo( + CelExpr.ofMap( + 9L, + ImmutableList.of( + CelExpr.ofMapEntry( + 10L, + CelExpr.ofConstant(11L, CelConstant.ofValue("key")), + CelExpr.ofConstant(12L, CelConstant.ofValue("value")), + true)))); + } + + @Test + public void convertCelMap_toMutableMap() { + CelExpr celExpr = + CelExpr.ofMap( + 9L, + ImmutableList.of( + CelExpr.ofMapEntry( + 10L, + CelExpr.ofConstant(11L, CelConstant.ofValue("key")), + CelExpr.ofConstant(12L, CelConstant.ofValue("value")), + true))); + + CelMutableExpr mutableExpr = CelMutableExprConverter.fromCelExpr(celExpr); + + assertThat(mutableExpr) + .isEqualTo( + CelMutableExpr.ofMap( + 9L, + CelMutableMap.create( + ImmutableList.of( + CelMutableMap.Entry.create( + 10L, + CelMutableExpr.ofConstant(11L, CelConstant.ofValue("key")), + CelMutableExpr.ofConstant(12L, CelConstant.ofValue("value")), + /* optionalEntry= */ true))))); + } + + @Test + public void convertMutableComprehension_toCelComprehension() { + CelMutableExpr mutableExpr = + CelMutableExpr.ofComprehension( + 1L, + CelMutableComprehension.create( + "iterVar", + CelMutableExpr.ofList( + 2L, + CelMutableList.create( + CelMutableExpr.ofConstant(3L, CelConstant.ofValue(true)))), + "accuVar", + CelMutableExpr.ofConstant(4L, CelConstant.ofValue(true)), + CelMutableExpr.ofConstant(5L, CelConstant.ofValue(true)), + CelMutableExpr.ofConstant(6L, CelConstant.ofValue(true)), + CelMutableExpr.ofIdent(7L, "__result__"))); + + CelExpr celExpr = CelMutableExprConverter.fromMutableExpr(mutableExpr); + + assertThat(celExpr) + .isEqualTo( + CelExpr.ofComprehension( + 1L, + "iterVar", + CelExpr.newBuilder() + .setId(2L) + .setList( + CelList.newBuilder() + .addElements(CelExpr.ofConstant(3L, CelConstant.ofValue(true))) + .build()) + .build(), + "accuVar", + CelExpr.ofConstant(4L, CelConstant.ofValue(true)), + CelExpr.ofConstant(5L, CelConstant.ofValue(true)), + CelExpr.ofConstant(6L, CelConstant.ofValue(true)), + CelExpr.ofIdent(7L, "__result__"))); + } + + @Test + public void convertCelComprehension_toMutableComprehension() { + CelExpr celExpr = + CelExpr.ofComprehension( + 1L, + "iterVar", + CelExpr.newBuilder() + .setId(2L) + .setList( + CelList.newBuilder() + .addElements(CelExpr.ofConstant(3L, CelConstant.ofValue(true))) + .build()) + .build(), + "accuVar", + CelExpr.ofConstant(4L, CelConstant.ofValue(true)), + CelExpr.ofConstant(5L, CelConstant.ofValue(true)), + CelExpr.ofConstant(6L, CelConstant.ofValue(true)), + CelExpr.ofIdent(7L, "__result__")); + + CelMutableExpr mutableExpr = CelMutableExprConverter.fromCelExpr(celExpr); + assertThat(mutableExpr) + .isEqualTo( + CelMutableExpr.ofComprehension( + 1L, + CelMutableComprehension.create( + "iterVar", + CelMutableExpr.ofList( + 2L, + CelMutableList.create( + CelMutableExpr.ofConstant(3L, CelConstant.ofValue(true)))), + "accuVar", + CelMutableExpr.ofConstant(4L, CelConstant.ofValue(true)), + CelMutableExpr.ofConstant(5L, CelConstant.ofValue(true)), + CelMutableExpr.ofConstant(6L, CelConstant.ofValue(true)), + CelMutableExpr.ofIdent(7L, "__result__")))); + } + + @Test + public void convertMutableComprehension_withTwoIterVars_toCelComprehension() { + CelMutableExpr mutableExpr = + CelMutableExpr.ofComprehension( + 1L, + CelMutableComprehension.create( + "iterVar", + "iterVar2", + CelMutableExpr.ofList( + 2L, + CelMutableList.create( + CelMutableExpr.ofConstant(3L, CelConstant.ofValue(true)))), + "accuVar", + CelMutableExpr.ofConstant(4L, CelConstant.ofValue(true)), + CelMutableExpr.ofConstant(5L, CelConstant.ofValue(true)), + CelMutableExpr.ofConstant(6L, CelConstant.ofValue(true)), + CelMutableExpr.ofIdent(7L, "__result__"))); + + CelExpr celExpr = CelMutableExprConverter.fromMutableExpr(mutableExpr); + + assertThat(celExpr) + .isEqualTo( + CelExpr.ofComprehension( + 1L, + "iterVar", + "iterVar2", + CelExpr.newBuilder() + .setId(2L) + .setList( + CelList.newBuilder() + .addElements(CelExpr.ofConstant(3L, CelConstant.ofValue(true))) + .build()) + .build(), + "accuVar", + CelExpr.ofConstant(4L, CelConstant.ofValue(true)), + CelExpr.ofConstant(5L, CelConstant.ofValue(true)), + CelExpr.ofConstant(6L, CelConstant.ofValue(true)), + CelExpr.ofIdent(7L, "__result__"))); + } + + @Test + public void convertCelComprehension_withTwoIterVars_toMutableComprehension() { + CelExpr celExpr = + CelExpr.ofComprehension( + 1L, + "iterVar", + "iterVar2", + CelExpr.newBuilder() + .setId(2L) + .setList( + CelList.newBuilder() + .addElements(CelExpr.ofConstant(3L, CelConstant.ofValue(true))) + .build()) + .build(), + "accuVar", + CelExpr.ofConstant(4L, CelConstant.ofValue(true)), + CelExpr.ofConstant(5L, CelConstant.ofValue(true)), + CelExpr.ofConstant(6L, CelConstant.ofValue(true)), + CelExpr.ofIdent(7L, "__result__")); + + CelMutableExpr mutableExpr = CelMutableExprConverter.fromCelExpr(celExpr); + assertThat(mutableExpr) + .isEqualTo( + CelMutableExpr.ofComprehension( + 1L, + CelMutableComprehension.create( + "iterVar", + "iterVar2", + CelMutableExpr.ofList( + 2L, + CelMutableList.create( + CelMutableExpr.ofConstant(3L, CelConstant.ofValue(true)))), + "accuVar", + CelMutableExpr.ofConstant(4L, CelConstant.ofValue(true)), + CelMutableExpr.ofConstant(5L, CelConstant.ofValue(true)), + CelMutableExpr.ofConstant(6L, CelConstant.ofValue(true)), + CelMutableExpr.ofIdent(7L, "__result__")))); + } +} diff --git a/common/src/test/java/dev/cel/common/ast/CelMutableExprTest.java b/common/src/test/java/dev/cel/common/ast/CelMutableExprTest.java new file mode 100644 index 000000000..a44c30bb1 --- /dev/null +++ b/common/src/test/java/dev/cel/common/ast/CelMutableExprTest.java @@ -0,0 +1,1052 @@ +// 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. + +package dev.cel.common.ast; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableList; +import com.google.common.testing.EqualsTester; +import com.google.common.truth.Correspondence; +import com.google.testing.junit.testparameterinjector.TestParameter; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.common.ast.CelExpr.ExprKind.Kind; +import dev.cel.common.ast.CelMutableExpr.CelMutableCall; +import dev.cel.common.ast.CelMutableExpr.CelMutableComprehension; +import dev.cel.common.ast.CelMutableExpr.CelMutableIdent; +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.ast.CelMutableExpr.CelMutableStruct; +import java.util.ArrayList; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public class CelMutableExprTest { + + @Test + public void ofNotSet() { + CelMutableExpr mutableExpr = CelMutableExpr.ofNotSet(); + + assertThat(mutableExpr.id()).isEqualTo(0L); + assertThat(mutableExpr.notSet()).isNotNull(); + } + + @Test + public void ofNotSet_withId() { + CelMutableExpr mutableExpr = CelMutableExpr.ofNotSet(1L); + + assertThat(mutableExpr.id()).isEqualTo(1L); + assertThat(mutableExpr.notSet()).isNotNull(); + } + + @Test + public void ofConstant() { + CelMutableExpr mutableExpr = CelMutableExpr.ofConstant(CelConstant.ofValue(5L)); + + assertThat(mutableExpr.id()).isEqualTo(0L); + assertThat(mutableExpr.constant()).isEqualTo(CelConstant.ofValue(5L)); + } + + @Test + public void ofConstant_withId() { + CelMutableExpr mutableExpr = CelMutableExpr.ofConstant(1L, CelConstant.ofValue(5L)); + + assertThat(mutableExpr.id()).isEqualTo(1L); + assertThat(mutableExpr.constant()).isEqualTo(CelConstant.ofValue(5L)); + } + + @Test + public void mutableConstant_deepCopy() { + CelMutableExpr mutableExpr = CelMutableExpr.ofConstant(1L, CelConstant.ofValue(5L)); + + CelMutableExpr deepCopiedExpr = CelMutableExpr.newInstance(mutableExpr); + + assertThat(mutableExpr).isEqualTo(deepCopiedExpr); + assertThat(mutableExpr.constant()).isEqualTo(deepCopiedExpr.constant()); + assertThat(mutableExpr).isNotSameInstanceAs(deepCopiedExpr); + // The stored constant itself is immutable, thus remain referentially equal when copied. + assertThat(mutableExpr.constant()).isSameInstanceAs(deepCopiedExpr.constant()); + } + + @Test + public void ofIdent() { + CelMutableExpr mutableExpr = CelMutableExpr.ofIdent("x"); + + assertThat(mutableExpr.id()).isEqualTo(0L); + assertThat(mutableExpr.ident().name()).isEqualTo("x"); + } + + @Test + public void ofIdent_withId() { + CelMutableExpr mutableExpr = CelMutableExpr.ofIdent(1L, "x"); + + assertThat(mutableExpr.id()).isEqualTo(1L); + assertThat(mutableExpr.ident().name()).isEqualTo("x"); + } + + @Test + public void mutableIdent_setName() { + CelMutableIdent ident = CelMutableIdent.create("x"); + + ident.setName("y"); + + assertThat(ident.name()).isEqualTo("y"); + } + + @Test + public void mutableIdent_deepCopy() { + CelMutableExpr mutableExpr = CelMutableExpr.ofIdent(1L, "x"); + + CelMutableExpr deepCopiedExpr = CelMutableExpr.newInstance(mutableExpr); + + assertThat(mutableExpr).isEqualTo(deepCopiedExpr); + assertThat(mutableExpr.ident()).isEqualTo(deepCopiedExpr.ident()); + assertThat(mutableExpr).isNotSameInstanceAs(deepCopiedExpr); + assertThat(mutableExpr.ident()).isNotSameInstanceAs(deepCopiedExpr.ident()); + } + + @Test + public void ofSelect() { + CelMutableExpr mutableExpr = + CelMutableExpr.ofSelect(CelMutableSelect.create(CelMutableExpr.ofIdent("x"), "field")); + + assertThat(mutableExpr.id()).isEqualTo(0L); + assertThat(mutableExpr.select().testOnly()).isFalse(); + assertThat(mutableExpr.select().field()).isEqualTo("field"); + assertThat(mutableExpr.select().operand()).isEqualTo(CelMutableExpr.ofIdent("x")); + } + + @Test + public void ofSelect_withId() { + CelMutableExpr mutableExpr = + CelMutableExpr.ofSelect( + 1L, + CelMutableSelect.create(CelMutableExpr.ofIdent("x"), "field", /* testOnly= */ true)); + + assertThat(mutableExpr.id()).isEqualTo(1L); + assertThat(mutableExpr.select().testOnly()).isTrue(); + assertThat(mutableExpr.select().field()).isEqualTo("field"); + assertThat(mutableExpr.select().operand()).isEqualTo(CelMutableExpr.ofIdent("x")); + } + + @Test + public void mutableSelect_setters() { + CelMutableSelect select = + CelMutableSelect.create(CelMutableExpr.ofIdent("x"), "field", /* testOnly= */ true); + + select.setOperand(CelMutableExpr.ofConstant(CelConstant.ofValue(1L))); + select.setField("field2"); + select.setTestOnly(false); + + assertThat(select.operand()).isEqualTo(CelMutableExpr.ofConstant(CelConstant.ofValue(1L))); + assertThat(select.field()).isEqualTo("field2"); + assertThat(select.testOnly()).isFalse(); + } + + @Test + public void mutableSelect_deepCopy() { + CelMutableExpr mutableExpr = + CelMutableExpr.ofSelect( + 1L, + CelMutableSelect.create(CelMutableExpr.ofIdent("x"), "field", /* testOnly= */ true)); + + CelMutableExpr deepCopiedExpr = CelMutableExpr.newInstance(mutableExpr); + + assertThat(mutableExpr).isEqualTo(deepCopiedExpr); + assertThat(mutableExpr.select()).isEqualTo(deepCopiedExpr.select()); + assertThat(mutableExpr).isNotSameInstanceAs(deepCopiedExpr); + assertThat(mutableExpr.select()).isNotSameInstanceAs(deepCopiedExpr.select()); + } + + @Test + public void ofCall() { + CelMutableExpr mutableExpr = + CelMutableExpr.ofCall( + CelMutableCall.create( + CelMutableExpr.ofConstant(CelConstant.ofValue("target")), + "function", + CelMutableExpr.ofConstant(CelConstant.ofValue("arg")))); + + assertThat(mutableExpr.id()).isEqualTo(0L); + assertThat(mutableExpr.call().target()) + .hasValue(CelMutableExpr.ofConstant(CelConstant.ofValue("target"))); + assertThat(mutableExpr.call().function()).isEqualTo("function"); + assertThat(mutableExpr.call().args()) + .containsExactly(CelMutableExpr.ofConstant(CelConstant.ofValue("arg"))); + } + + @Test + public void ofCall_withId() { + CelMutableExpr mutableExpr = + CelMutableExpr.ofCall( + 1L, + CelMutableCall.create( + CelMutableExpr.ofConstant(CelConstant.ofValue("target")), + "function", + CelMutableExpr.ofConstant(CelConstant.ofValue("arg")))); + + assertThat(mutableExpr.id()).isEqualTo(1L); + assertThat(mutableExpr.call().target()) + .hasValue(CelMutableExpr.ofConstant(CelConstant.ofValue("target"))); + assertThat(mutableExpr.call().function()).isEqualTo("function"); + assertThat(mutableExpr.call().args()) + .containsExactly(CelMutableExpr.ofConstant(CelConstant.ofValue("arg"))); + } + + @Test + public void setId_success() { + CelMutableExpr mutableExpr = CelMutableExpr.ofConstant(CelConstant.ofValue(5L)); + + mutableExpr.setId(2L); + + assertThat(mutableExpr.id()).isEqualTo(2L); + } + + @Test + public void mutableCall_setArgumentAtIndex() { + CelMutableCall call = + CelMutableCall.create("function", CelMutableExpr.ofConstant(CelConstant.ofValue(1L))); + + call.setArg(0, CelMutableExpr.ofConstant(CelConstant.ofValue("hello"))); + + assertThat(call.args()) + .containsExactly(CelMutableExpr.ofConstant(CelConstant.ofValue("hello"))); + assertThat(call.args()).isInstanceOf(ArrayList.class); + } + + @Test + public void mutableCall_setArguments() { + CelMutableCall call = + CelMutableCall.create("function", CelMutableExpr.ofConstant(CelConstant.ofValue(1L))); + + call.setArgs( + ImmutableList.of( + CelMutableExpr.ofConstant(CelConstant.ofValue(2)), + CelMutableExpr.ofConstant(CelConstant.ofValue(3)))); + + assertThat(call.args()) + .containsExactly( + CelMutableExpr.ofConstant(CelConstant.ofValue(2)), + CelMutableExpr.ofConstant(CelConstant.ofValue(3))) + .inOrder(); + assertThat(call.args()).isInstanceOf(ArrayList.class); + } + + @Test + public void mutableCall_addArguments() { + CelMutableCall call = + CelMutableCall.create("function", CelMutableExpr.ofConstant(CelConstant.ofValue(1L))); + + call.addArgs( + CelMutableExpr.ofConstant(CelConstant.ofValue(2)), + CelMutableExpr.ofConstant(CelConstant.ofValue(3))); + + assertThat(call.args()) + .containsExactly( + CelMutableExpr.ofConstant(CelConstant.ofValue(1)), + CelMutableExpr.ofConstant(CelConstant.ofValue(2)), + CelMutableExpr.ofConstant(CelConstant.ofValue(3))) + .inOrder(); + assertThat(call.args()).isInstanceOf(ArrayList.class); + } + + @Test + public void mutableCall_clearArguments() { + CelMutableCall call = + CelMutableCall.create( + "function", + CelMutableExpr.ofConstant(CelConstant.ofValue(1L)), + CelMutableExpr.ofConstant(CelConstant.ofValue(2L))); + + call.clearArgs(); + + assertThat(call.args()).isEmpty(); + } + + @Test + public void mutableCall_setTarget() { + CelMutableCall call = CelMutableCall.create("function"); + + call.setTarget(CelMutableExpr.ofConstant(CelConstant.ofValue("hello"))); + + assertThat(call.target()).hasValue(CelMutableExpr.ofConstant(CelConstant.ofValue("hello"))); + } + + @Test + public void mutableCall_deepCopy() { + CelMutableExpr mutableExpr = + CelMutableExpr.ofCall( + 1L, + CelMutableCall.create( + CelMutableExpr.ofConstant(CelConstant.ofValue("target")), + "function", + CelMutableExpr.ofConstant(CelConstant.ofValue("arg")))); + + CelMutableExpr deepCopiedExpr = CelMutableExpr.newInstance(mutableExpr); + + assertThat(mutableExpr).isEqualTo(deepCopiedExpr); + assertThat(mutableExpr.call()).isEqualTo(deepCopiedExpr.call()); + assertThat(mutableExpr).isNotSameInstanceAs(deepCopiedExpr); + assertThat(mutableExpr.call()).isNotSameInstanceAs(deepCopiedExpr.call()); + } + + @Test + public void mutableCall_setFunction() { + CelMutableCall call = CelMutableCall.create("function"); + + call.setFunction("function2"); + + assertThat(call.function()).isEqualTo("function2"); + } + + @Test + public void ofList() { + CelMutableExpr mutableExpr = + CelMutableExpr.ofList( + CelMutableList.create( + CelMutableExpr.ofConstant(CelConstant.ofValue("element1")), + CelMutableExpr.ofConstant(CelConstant.ofValue("element2")))); + + assertThat(mutableExpr.id()).isEqualTo(0L); + assertThat(mutableExpr.list().elements()) + .containsExactly( + CelMutableExpr.ofConstant(CelConstant.ofValue("element1")), + CelMutableExpr.ofConstant(CelConstant.ofValue("element2"))) + .inOrder(); + assertThat(mutableExpr.list().optionalIndices()).isEmpty(); + } + + @Test + public void ofList_withId() { + CelMutableExpr mutableExpr = + CelMutableExpr.ofList( + 1L, + CelMutableList.create( + ImmutableList.of( + CelMutableExpr.ofConstant(CelConstant.ofValue("element1")), + CelMutableExpr.ofConstant(CelConstant.ofValue("element2"))), + ImmutableList.of(0, 1))); + + assertThat(mutableExpr.id()).isEqualTo(1L); + assertThat(mutableExpr.list().elements()) + .containsExactly( + CelMutableExpr.ofConstant(CelConstant.ofValue("element1")), + CelMutableExpr.ofConstant(CelConstant.ofValue("element2"))) + .inOrder(); + assertThat(mutableExpr.list().optionalIndices()).containsExactly(0, 1).inOrder(); + } + + @Test + public void mutableList_setElementAtIndex() { + CelMutableList list = + CelMutableList.create(CelMutableExpr.ofConstant(CelConstant.ofValue("element1"))); + + list.setElement(0, CelMutableExpr.ofConstant(CelConstant.ofValue("hello"))); + + assertThat(list.elements()) + .containsExactly(CelMutableExpr.ofConstant(CelConstant.ofValue("hello"))); + assertThat(list.elements()).isInstanceOf(ArrayList.class); + } + + @Test + @SuppressWarnings("ReferenceEquality") // test only on iterating through elements + public void mutableList_deepCopy() { + CelMutableExpr mutableExpr = + CelMutableExpr.ofList( + CelMutableList.create( + CelMutableExpr.ofConstant(CelConstant.ofValue("element1")), + CelMutableExpr.ofConstant(CelConstant.ofValue("element2")))); + + CelMutableExpr deepCopiedExpr = CelMutableExpr.newInstance(mutableExpr); + + assertThat(mutableExpr).isEqualTo(deepCopiedExpr); + assertThat(mutableExpr.list()).isEqualTo(deepCopiedExpr.list()); + assertThat(mutableExpr).isNotSameInstanceAs(deepCopiedExpr); + assertThat(mutableExpr.list()).isNotSameInstanceAs(deepCopiedExpr.list()); + assertThat(mutableExpr.list().elements()) + .comparingElementsUsing( + Correspondence.from( + (e1, e2) -> e1 != e2 && e1.equals(e2), + "are only value equal and not referentially equal")) + .containsExactlyElementsIn(deepCopiedExpr.list().elements()); + } + + @Test + public void ofStruct() { + CelMutableExpr mutableExpr = + CelMutableExpr.ofStruct(CelMutableStruct.create("message", ImmutableList.of())); + + assertThat(mutableExpr.id()).isEqualTo(0L); + assertThat(mutableExpr.struct().messageName()).isEqualTo("message"); + assertThat(mutableExpr.struct().entries()).isEmpty(); + } + + @Test + public void ofStruct_withId() { + CelMutableExpr mutableExpr = + CelMutableExpr.ofStruct( + 8L, + CelMutableStruct.create( + "message", + ImmutableList.of( + CelMutableStruct.Entry.create( + 9L, + "field", + CelMutableExpr.ofConstant(CelConstant.ofValue("value")), + /* optionalEntry= */ true)))); + + assertThat(mutableExpr.id()).isEqualTo(8L); + assertThat(mutableExpr.struct().messageName()).isEqualTo("message"); + assertThat(mutableExpr.struct().entries()) + .containsExactly( + CelMutableStruct.Entry.create( + 9L, + "field", + CelMutableExpr.ofConstant(CelConstant.ofValue("value")), + /* optionalEntry= */ true)); + } + + @Test + public void mutableStruct_setEntryAtIndex() { + CelMutableStruct struct = + CelMutableStruct.create( + "message", + ImmutableList.of( + CelMutableStruct.Entry.create( + 1L, "field", CelMutableExpr.ofConstant(CelConstant.ofValue("value"))))); + CelMutableStruct.Entry newEntry = + CelMutableStruct.Entry.create( + 2L, + "field2", + CelMutableExpr.ofConstant(CelConstant.ofValue("value2")), + /* optionalEntry= */ true); + + struct.setEntry(0, newEntry); + + assertThat(struct.entries()).containsExactly(newEntry); + } + + @Test + public void mutableStructEntry_setters() { + CelMutableStruct.Entry structEntry = + CelMutableStruct.Entry.create( + 1L, "field", CelMutableExpr.ofConstant(CelConstant.ofValue("value"))); + + structEntry.setId(2L); + structEntry.setFieldKey("field2"); + structEntry.setValue(CelMutableExpr.ofConstant(CelConstant.ofValue("value2"))); + structEntry.setOptionalEntry(true); + + assertThat(structEntry) + .isEqualTo( + CelMutableStruct.Entry.create( + 2L, "field2", CelMutableExpr.ofConstant(CelConstant.ofValue("value2")), true)); + } + + @Test + @SuppressWarnings("ReferenceEquality") // test only on iterating through elements + public void mutableStruct_deepCopy() { + CelMutableExpr mutableExpr = + CelMutableExpr.ofStruct( + 8L, + CelMutableStruct.create( + "message", + ImmutableList.of( + CelMutableStruct.Entry.create( + 8L, + "field", + CelMutableExpr.ofConstant(CelConstant.ofValue("value")), + /* optionalEntry= */ true)))); + + CelMutableExpr deepCopiedExpr = CelMutableExpr.newInstance(mutableExpr); + + assertThat(mutableExpr).isEqualTo(deepCopiedExpr); + assertThat(mutableExpr.struct()).isEqualTo(deepCopiedExpr.struct()); + assertThat(mutableExpr).isNotSameInstanceAs(deepCopiedExpr); + assertThat(mutableExpr.struct()).isNotSameInstanceAs(deepCopiedExpr.struct()); + assertThat(mutableExpr.struct().entries()) + .isNotSameInstanceAs(deepCopiedExpr.struct().entries()); + assertThat(mutableExpr.struct().entries()) + .comparingElementsUsing( + Correspondence.from( + (e1, e2) -> + e1 != e2 + && e1.equals(e2) + && e1.value() != e2.value() + && e1.value().equals(e2.value()), + "are only value equal and not referentially equal")) + .containsExactlyElementsIn(deepCopiedExpr.struct().entries()); + } + + @Test + public void ofMap() { + CelMutableExpr mutableExpr = CelMutableExpr.ofMap(CelMutableMap.create(ImmutableList.of())); + + assertThat(mutableExpr.id()).isEqualTo(0L); + assertThat(mutableExpr.map().entries()).isEmpty(); + } + + @Test + public void ofMap_withId() { + CelMutableExpr mutableExpr = + CelMutableExpr.ofMap( + 9L, + CelMutableMap.create( + ImmutableList.of( + CelMutableMap.Entry.create( + 10L, + CelMutableExpr.ofConstant(CelConstant.ofValue("key")), + CelMutableExpr.ofConstant(CelConstant.ofValue("value")), + /* optionalEntry= */ true)))); + + assertThat(mutableExpr.id()).isEqualTo(9L); + assertThat(mutableExpr.map().entries()) + .containsExactly( + CelMutableMap.Entry.create( + 10L, + CelMutableExpr.ofConstant(CelConstant.ofValue("key")), + CelMutableExpr.ofConstant(CelConstant.ofValue("value")), + /* optionalEntry= */ true)); + } + + @Test + public void mutableMap_setEntryAtIndex() { + CelMutableMap map = + CelMutableMap.create( + ImmutableList.of( + CelMutableMap.Entry.create( + 10L, + CelMutableExpr.ofConstant(CelConstant.ofValue("key")), + CelMutableExpr.ofConstant(CelConstant.ofValue("value"))))); + CelMutableMap.Entry newEntry = + CelMutableMap.Entry.create( + 2L, + CelMutableExpr.ofConstant(CelConstant.ofValue("key2")), + CelMutableExpr.ofConstant(CelConstant.ofValue("value2")), + /* optionalEntry= */ true); + + map.setEntry(0, newEntry); + + assertThat(map.entries()).containsExactly(newEntry); + } + + @Test + public void mutableMapEntry_setters() { + CelMutableMap.Entry mapEntry = + CelMutableMap.Entry.create( + 1L, + CelMutableExpr.ofConstant(CelConstant.ofValue("key")), + CelMutableExpr.ofConstant(CelConstant.ofValue("value"))); + + mapEntry.setId(2L); + mapEntry.setKey(CelMutableExpr.ofConstant(CelConstant.ofValue("key2"))); + mapEntry.setValue(CelMutableExpr.ofConstant(CelConstant.ofValue("value2"))); + mapEntry.setOptionalEntry(true); + + assertThat(mapEntry) + .isEqualTo( + CelMutableMap.Entry.create( + 2L, + CelMutableExpr.ofConstant(CelConstant.ofValue("key2")), + CelMutableExpr.ofConstant(CelConstant.ofValue("value2")), + true)); + } + + @Test + @SuppressWarnings("ReferenceEquality") // test only on iterating through elements + public void mutableMap_deepCopy() { + CelMutableExpr mutableExpr = + CelMutableExpr.ofMap( + 9L, + CelMutableMap.create( + ImmutableList.of( + CelMutableMap.Entry.create( + 10L, + CelMutableExpr.ofConstant(CelConstant.ofValue("key")), + CelMutableExpr.ofConstant(CelConstant.ofValue("value")), + /* optionalEntry= */ true)))); + + CelMutableExpr deepCopiedExpr = CelMutableExpr.newInstance(mutableExpr); + + assertThat(mutableExpr).isEqualTo(deepCopiedExpr); + assertThat(mutableExpr.map()).isEqualTo(deepCopiedExpr.map()); + assertThat(mutableExpr).isNotSameInstanceAs(deepCopiedExpr); + assertThat(mutableExpr.map()).isNotSameInstanceAs(deepCopiedExpr.map()); + assertThat(mutableExpr.map().entries()) + .comparingElementsUsing( + Correspondence.from( + (e1, e2) -> + e1 != e2 + && e1.equals(e2) + && e1.key() != e2.key() + && e1.key().equals(e2.key()) + && e1.value() != e2.value() + && e1.value().equals(e2.value()), + "are only value equal and not referentially equal")) + .containsExactlyElementsIn(deepCopiedExpr.map().entries()); + } + + @Test + public void ofComprehension() { + CelMutableExpr mutableExpr = + CelMutableExpr.ofComprehension( + CelMutableComprehension.create( + "iterVar", + CelMutableExpr.ofList( + CelMutableList.create(CelMutableExpr.ofConstant(CelConstant.ofValue(true)))), + "accuVar", + CelMutableExpr.ofConstant(CelConstant.ofValue(true)), + CelMutableExpr.ofConstant(CelConstant.ofValue(true)), + CelMutableExpr.ofConstant(CelConstant.ofValue(true)), + CelMutableExpr.ofIdent("__result__"))); + + assertThat(mutableExpr.id()).isEqualTo(0L); + assertThat(mutableExpr.comprehension()) + .isEqualTo( + CelMutableComprehension.create( + "iterVar", + "", + CelMutableExpr.ofList( + CelMutableList.create(CelMutableExpr.ofConstant(CelConstant.ofValue(true)))), + "accuVar", + CelMutableExpr.ofConstant(CelConstant.ofValue(true)), + CelMutableExpr.ofConstant(CelConstant.ofValue(true)), + CelMutableExpr.ofConstant(CelConstant.ofValue(true)), + CelMutableExpr.ofIdent("__result__"))); + } + + @Test + public void ofComprehension_withId() { + CelMutableExpr mutableExpr = + CelMutableExpr.ofComprehension( + 10L, + CelMutableComprehension.create( + "iterVar", + CelMutableExpr.ofList( + CelMutableList.create(CelMutableExpr.ofConstant(CelConstant.ofValue(true)))), + "accuVar", + CelMutableExpr.ofConstant(CelConstant.ofValue(true)), + CelMutableExpr.ofConstant(CelConstant.ofValue(true)), + CelMutableExpr.ofConstant(CelConstant.ofValue(true)), + CelMutableExpr.ofIdent("__result__"))); + + assertThat(mutableExpr.id()).isEqualTo(10L); + assertThat(mutableExpr.comprehension()) + .isEqualTo( + CelMutableComprehension.create( + "iterVar", + "", + CelMutableExpr.ofList( + CelMutableList.create(CelMutableExpr.ofConstant(CelConstant.ofValue(true)))), + "accuVar", + CelMutableExpr.ofConstant(CelConstant.ofValue(true)), + CelMutableExpr.ofConstant(CelConstant.ofValue(true)), + CelMutableExpr.ofConstant(CelConstant.ofValue(true)), + CelMutableExpr.ofIdent("__result__"))); + } + + @Test + public void mutableComprehension_setters() { + CelMutableComprehension mutableComprehension = + CelMutableComprehension.create( + "iterVar", + CelMutableExpr.ofNotSet(), + "accuVar", + CelMutableExpr.ofNotSet(), + CelMutableExpr.ofNotSet(), + CelMutableExpr.ofNotSet(), + CelMutableExpr.ofNotSet()); + + mutableComprehension.setIterVar("iterVar2"); + mutableComprehension.setAccuVar("accuVar2"); + mutableComprehension.setIterRange( + CelMutableExpr.ofList( + CelMutableList.create(CelMutableExpr.ofConstant(CelConstant.ofValue(true))))); + mutableComprehension.setAccuInit(CelMutableExpr.ofConstant(CelConstant.ofValue(true))); + mutableComprehension.setLoopCondition(CelMutableExpr.ofConstant(CelConstant.ofValue(true))); + mutableComprehension.setLoopStep(CelMutableExpr.ofConstant(CelConstant.ofValue(true))); + mutableComprehension.setResult(CelMutableExpr.ofIdent("__result__")); + + assertThat(mutableComprehension) + .isEqualTo( + CelMutableComprehension.create( + "iterVar2", + CelMutableExpr.ofList( + CelMutableList.create(CelMutableExpr.ofConstant(CelConstant.ofValue(true)))), + "accuVar2", + CelMutableExpr.ofConstant(CelConstant.ofValue(true)), + CelMutableExpr.ofConstant(CelConstant.ofValue(true)), + CelMutableExpr.ofConstant(CelConstant.ofValue(true)), + CelMutableExpr.ofIdent("__result__"))); + } + + @Test + public void mutableComprehension_deepCopy() { + CelMutableExpr mutableExpr = + CelMutableExpr.ofComprehension( + 10L, + CelMutableComprehension.create( + "iterVar", + CelMutableExpr.ofList( + CelMutableList.create(CelMutableExpr.ofConstant(CelConstant.ofValue(true)))), + "accuVar", + CelMutableExpr.ofConstant(CelConstant.ofValue(true)), + CelMutableExpr.ofConstant(CelConstant.ofValue(true)), + CelMutableExpr.ofConstant(CelConstant.ofValue(true)), + CelMutableExpr.ofIdent("__result__"))); + + CelMutableExpr deepCopiedExpr = CelMutableExpr.newInstance(mutableExpr); + + assertThat(mutableExpr).isEqualTo(deepCopiedExpr); + assertThat(mutableExpr).isNotSameInstanceAs(deepCopiedExpr); + assertThat(mutableExpr.comprehension()).isEqualTo(deepCopiedExpr.comprehension()); + assertThat(mutableExpr.comprehension()).isNotSameInstanceAs(deepCopiedExpr.comprehension()); + assertThat(mutableExpr.comprehension().accuInit()) + .isEqualTo(deepCopiedExpr.comprehension().accuInit()); + assertThat(mutableExpr.comprehension().accuInit()) + .isNotSameInstanceAs(deepCopiedExpr.comprehension().accuInit()); + assertThat(mutableExpr.comprehension().iterRange()) + .isEqualTo(deepCopiedExpr.comprehension().iterRange()); + assertThat(mutableExpr.comprehension().iterRange()) + .isNotSameInstanceAs(deepCopiedExpr.comprehension().iterRange()); + assertThat(mutableExpr.comprehension().loopCondition()) + .isEqualTo(deepCopiedExpr.comprehension().loopCondition()); + assertThat(mutableExpr.comprehension().loopCondition()) + .isNotSameInstanceAs(deepCopiedExpr.comprehension().loopCondition()); + assertThat(mutableExpr.comprehension().loopStep()) + .isEqualTo(deepCopiedExpr.comprehension().loopStep()); + assertThat(mutableExpr.comprehension().loopStep()) + .isNotSameInstanceAs(deepCopiedExpr.comprehension().loopStep()); + assertThat(mutableExpr.comprehension().result()) + .isEqualTo(deepCopiedExpr.comprehension().result()); + assertThat(mutableExpr.comprehension().result()) + .isNotSameInstanceAs(deepCopiedExpr.comprehension().result()); + } + + @Test + public void equalityTest() { + new EqualsTester() + .addEqualityGroup(CelMutableExpr.ofNotSet()) + .addEqualityGroup(CelMutableExpr.ofNotSet(1L), CelMutableExpr.ofNotSet(1L)) + .addEqualityGroup(CelMutableExpr.ofConstant(1L, CelConstant.ofValue(2L))) + .addEqualityGroup( + CelMutableExpr.ofConstant(5L, CelConstant.ofValue("hello")), + CelMutableExpr.ofConstant(5L, CelConstant.ofValue("hello"))) + .addEqualityGroup(CelMutableExpr.ofIdent("x")) + .addEqualityGroup(CelMutableExpr.ofIdent(2L, "y"), CelMutableExpr.ofIdent(2L, "y")) + .addEqualityGroup( + CelMutableExpr.ofSelect(CelMutableSelect.create(CelMutableExpr.ofIdent("y"), "field"))) + .addEqualityGroup( + CelMutableExpr.ofSelect( + 4L, CelMutableSelect.create(CelMutableExpr.ofIdent("x"), "test")), + CelMutableExpr.ofSelect( + 4L, CelMutableSelect.create(CelMutableExpr.ofIdent("x"), "test"))) + .addEqualityGroup(CelMutableExpr.ofCall(CelMutableCall.create("function"))) + .addEqualityGroup( + CelMutableExpr.ofCall( + 5L, + CelMutableCall.create( + CelMutableExpr.ofConstant(CelConstant.ofValue("target")), + "function", + CelMutableExpr.ofConstant(CelConstant.ofValue("arg")))), + CelMutableExpr.ofCall( + 5L, + CelMutableCall.create( + CelMutableExpr.ofConstant(CelConstant.ofValue("target")), + "function", + CelMutableExpr.ofConstant(CelConstant.ofValue("arg"))))) + .addEqualityGroup(CelMutableExpr.ofList(CelMutableList.create())) + .addEqualityGroup( + CelMutableExpr.ofList( + 6L, + CelMutableList.create( + CelMutableExpr.ofConstant(CelConstant.ofValue("element1")), + CelMutableExpr.ofConstant(CelConstant.ofValue("element2")))), + CelMutableExpr.ofList( + 6L, + CelMutableList.create( + CelMutableExpr.ofConstant(CelConstant.ofValue("element1")), + CelMutableExpr.ofConstant(CelConstant.ofValue("element2"))))) + .addEqualityGroup( + CelMutableExpr.ofStruct(CelMutableStruct.create("message", ImmutableList.of()))) + .addEqualityGroup( + CelMutableExpr.ofStruct( + 7L, + CelMutableStruct.create( + "message", + ImmutableList.of( + CelMutableStruct.Entry.create( + 8L, + "field", + CelMutableExpr.ofConstant(CelConstant.ofValue("value")), + /* optionalEntry= */ true)))), + CelMutableExpr.ofStruct( + 7L, + CelMutableStruct.create( + "message", + ImmutableList.of( + CelMutableStruct.Entry.create( + 8L, + "field", + CelMutableExpr.ofConstant(CelConstant.ofValue("value")), + /* optionalEntry= */ true))))) + .addEqualityGroup(CelMutableExpr.ofMap(CelMutableMap.create(ImmutableList.of()))) + .addEqualityGroup( + CelMutableMap.create( + ImmutableList.of( + CelMutableMap.Entry.create( + 9L, + CelMutableExpr.ofConstant(CelConstant.ofValue("key")), + CelMutableExpr.ofConstant(CelConstant.ofValue("value")), + /* optionalEntry= */ true))), + CelMutableMap.create( + ImmutableList.of( + CelMutableMap.Entry.create( + 9L, + CelMutableExpr.ofConstant(CelConstant.ofValue("key")), + CelMutableExpr.ofConstant(CelConstant.ofValue("value")), + /* optionalEntry= */ true)))) + .addEqualityGroup( + CelMutableExpr.ofComprehension( + 10L, + CelMutableComprehension.create( + "iterVar", + CelMutableExpr.ofNotSet(), + "accuVar", + CelMutableExpr.ofNotSet(), + CelMutableExpr.ofNotSet(), + CelMutableExpr.ofNotSet(), + CelMutableExpr.ofNotSet()))) + .addEqualityGroup( + CelMutableExpr.ofComprehension( + 11L, + CelMutableComprehension.create( + "iterVar", + CelMutableExpr.ofList( + CelMutableList.create( + CelMutableExpr.ofConstant(CelConstant.ofValue(true)))), + "accuVar", + CelMutableExpr.ofConstant(CelConstant.ofValue(true)), + CelMutableExpr.ofConstant(CelConstant.ofValue(true)), + CelMutableExpr.ofConstant(CelConstant.ofValue(true)), + CelMutableExpr.ofIdent("__result__"))), + CelMutableExpr.ofComprehension( + 11L, + CelMutableComprehension.create( + "iterVar", + CelMutableExpr.ofList( + CelMutableList.create( + CelMutableExpr.ofConstant(CelConstant.ofValue(true)))), + "accuVar", + CelMutableExpr.ofConstant(CelConstant.ofValue(true)), + CelMutableExpr.ofConstant(CelConstant.ofValue(true)), + CelMutableExpr.ofConstant(CelConstant.ofValue(true)), + CelMutableExpr.ofIdent("__result__")))) + .addEqualityGroup( + CelMutableExpr.ofComprehension( + 12L, + CelMutableComprehension.create( + "iterVar", + "iterVar2", + CelMutableExpr.ofList( + CelMutableList.create( + CelMutableExpr.ofConstant(CelConstant.ofValue(true)))), + "accuVar", + CelMutableExpr.ofConstant(CelConstant.ofValue(true)), + CelMutableExpr.ofConstant(CelConstant.ofValue(true)), + CelMutableExpr.ofConstant(CelConstant.ofValue(true)), + CelMutableExpr.ofIdent("__result__"))), + CelMutableExpr.ofComprehension( + 12L, + CelMutableComprehension.create( + "iterVar", + "iterVar2", + CelMutableExpr.ofList( + CelMutableList.create( + CelMutableExpr.ofConstant(CelConstant.ofValue(true)))), + "accuVar", + CelMutableExpr.ofConstant(CelConstant.ofValue(true)), + CelMutableExpr.ofConstant(CelConstant.ofValue(true)), + CelMutableExpr.ofConstant(CelConstant.ofValue(true)), + CelMutableExpr.ofIdent("__result__")))) + .testEquals(); + } + + @SuppressWarnings("Immutable") // Mutable by design + private enum MutableExprKindTestCase { + NOT_SET(CelMutableExpr.ofNotSet(1L)), + CONSTANT(CelMutableExpr.ofConstant(CelConstant.ofValue(2L))), + IDENT(CelMutableExpr.ofIdent("test")), + SELECT(CelMutableExpr.ofSelect(CelMutableSelect.create(CelMutableExpr.ofNotSet(), "field"))), + CALL(CelMutableExpr.ofCall(CelMutableCall.create("call"))), + LIST(CelMutableExpr.ofList(CelMutableList.create())), + STRUCT(CelMutableExpr.ofStruct(CelMutableStruct.create("message", ImmutableList.of()))), + MAP(CelMutableExpr.ofMap(CelMutableMap.create(ImmutableList.of()))), + COMPREHENSION( + CelMutableExpr.ofComprehension( + 10L, + CelMutableComprehension.create( + "iterVar", + CelMutableExpr.ofNotSet(), + "accuVar", + CelMutableExpr.ofNotSet(), + CelMutableExpr.ofNotSet(), + CelMutableExpr.ofNotSet(), + CelMutableExpr.ofNotSet()))), + ; + + private final CelMutableExpr mutableExpr; + + MutableExprKindTestCase(CelMutableExpr mutableExpr) { + this.mutableExpr = mutableExpr; + } + } + + @Test + public void getExprValue_invalidKind_throws(@TestParameter MutableExprKindTestCase testCase) { + Kind testCaseKind = testCase.mutableExpr.getKind(); + if (!testCaseKind.equals(Kind.NOT_SET)) { + assertThrows(IllegalArgumentException.class, testCase.mutableExpr::notSet); + } + if (!testCaseKind.equals(Kind.CONSTANT)) { + assertThrows(IllegalArgumentException.class, testCase.mutableExpr::constant); + } + if (!testCaseKind.equals(Kind.IDENT)) { + assertThrows(IllegalArgumentException.class, testCase.mutableExpr::ident); + } + if (!testCaseKind.equals(Kind.SELECT)) { + assertThrows(IllegalArgumentException.class, testCase.mutableExpr::select); + } + if (!testCaseKind.equals(Kind.CALL)) { + assertThrows(IllegalArgumentException.class, testCase.mutableExpr::call); + } + if (!testCaseKind.equals(Kind.LIST)) { + assertThrows(IllegalArgumentException.class, testCase.mutableExpr::list); + } + if (!testCaseKind.equals(Kind.STRUCT)) { + assertThrows(IllegalArgumentException.class, testCase.mutableExpr::struct); + } + if (!testCaseKind.equals(Kind.MAP)) { + assertThrows(IllegalArgumentException.class, testCase.mutableExpr::map); + } + if (!testCaseKind.equals(Kind.COMPREHENSION)) { + assertThrows(IllegalArgumentException.class, testCase.mutableExpr::comprehension); + } + } + + @SuppressWarnings("Immutable") // Mutable by design + private enum HashCodeTestCase { + NOT_SET(CelMutableExpr.ofNotSet(1L), -722379961), + CONSTANT(CelMutableExpr.ofConstant(2L, CelConstant.ofValue("test")), -724279919), + IDENT(CelMutableExpr.ofIdent("x"), -721379855), + SELECT( + CelMutableExpr.ofSelect( + 4L, + CelMutableSelect.create(CelMutableExpr.ofIdent("y"), "field", /* testOnly= */ true)), + 1458249843), + CALL( + CelMutableExpr.ofCall( + 5L, + CelMutableCall.create( + CelMutableExpr.ofConstant(CelConstant.ofValue("target")), + "function", + CelMutableExpr.ofConstant(CelConstant.ofValue("arg")))), + -1735261193), + LIST( + CelMutableExpr.ofList( + 6L, + CelMutableList.create( + CelMutableExpr.ofConstant(CelConstant.ofValue("element1")), + CelMutableExpr.ofConstant(CelConstant.ofValue("element2")))), + 165341403), + STRUCT( + CelMutableExpr.ofStruct( + 7L, + CelMutableStruct.create( + "message", + ImmutableList.of( + CelMutableStruct.Entry.create( + 8L, + "field", + CelMutableExpr.ofConstant(CelConstant.ofValue("value")), + /* optionalEntry= */ true)))), + 2064611987), + MAP( + CelMutableExpr.ofMap( + 8L, + CelMutableMap.create( + ImmutableList.of( + CelMutableMap.Entry.create( + 9L, + CelMutableExpr.ofConstant(CelConstant.ofValue("key")), + CelMutableExpr.ofConstant(CelConstant.ofValue("value")), + /* optionalEntry= */ true)))), + 1260717292), + COMPREHENSION( + CelMutableExpr.ofComprehension( + 10L, + CelMutableComprehension.create( + "iterVar", + CelMutableExpr.ofList( + CelMutableList.create(CelMutableExpr.ofConstant(CelConstant.ofValue(true)))), + "accuVar", + CelMutableExpr.ofConstant(CelConstant.ofValue(true)), + CelMutableExpr.ofConstant(CelConstant.ofValue(true)), + CelMutableExpr.ofConstant(CelConstant.ofValue(true)), + CelMutableExpr.ofIdent("__result__"))), + -707426392), + COMPREHENSIONV2( + CelMutableExpr.ofComprehension( + 10L, + CelMutableComprehension.create( + "iterVar", + "iterVar2", + CelMutableExpr.ofList( + CelMutableList.create(CelMutableExpr.ofConstant(CelConstant.ofValue(true)))), + "accuVar", + CelMutableExpr.ofConstant(CelConstant.ofValue(true)), + CelMutableExpr.ofConstant(CelConstant.ofValue(true)), + CelMutableExpr.ofConstant(CelConstant.ofValue(true)), + CelMutableExpr.ofIdent("__result__"))), + 1063550879); + + private final CelMutableExpr mutableExpr; + private final int expectedHashCode; + + HashCodeTestCase(CelMutableExpr mutableExpr, int expectedHashCode) { + this.mutableExpr = mutableExpr; + this.expectedHashCode = expectedHashCode; + } + } + + @Test + public void hashCodeTest(@TestParameter HashCodeTestCase testCase) { + assertThat(testCase.mutableExpr.hashCode()).isEqualTo(testCase.expectedHashCode); + // Run it twice to ensure cached value is stable + assertThat(testCase.mutableExpr.hashCode()).isEqualTo(testCase.expectedHashCode); + } + + @Test + public void propertyMutated_hashCodeChanged() { + CelMutableExpr mutableExpr = CelMutableExpr.ofIdent("x"); + int originalHash = mutableExpr.hashCode(); + + mutableExpr.ident().setName("y"); + + assertThat(originalHash).isNotEqualTo(mutableExpr.hashCode()); + } +} 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 41d98f6c8..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,8 +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", @@ -10,8 +13,10 @@ java_library( srcs = glob(["*.java"]), resources = ["//common/src/test/resources"], deps = [ + "//:auto_value", "//:java_truth", - "//common", + "//common:cel_descriptor_util", + "//common:cel_descriptors", "//common:options", "//common/ast", "//common/internal", @@ -19,25 +24,31 @@ java_library( "//common/internal:comparison_functions", "//common/internal:converter", "//common/internal:default_instance_message_factory", + "//common/internal:default_lite_descriptor_pool", "//common/internal:default_message_factory", "//common/internal:dynamic_proto", "//common/internal:errors", "//common/internal:proto_equality", "//common/internal:proto_message_factory", - "//common/resources/testdata/proto2:messages_extensions_proto2_java_proto", - "//common/resources/testdata/proto2:messages_proto2_java_proto", - "//common/resources/testdata/proto2:test_all_types_java_proto", - "//common/resources/testdata/proto3:test_all_types_java_proto", + "//common/internal:proto_time_utils", + "//common/internal:well_known_proto", "//common/src/test/resources:default_instance_message_test_protos_java_proto", - "//common/src/test/resources:multi_file_java_proto", "//common/src/test/resources:service_conflicting_name_java_proto", - "//common/src/test/resources:single_file_java_proto", "//common/testing", - "@maven//:com_google_api_grpc_proto_google_common_protos", + "//common/values", + "//common/values:cel_byte_string", + "//protobuf:cel_lite_descriptor", + "//testing/protos:multi_file_java_proto", + "//testing/protos:single_file_java_proto", + "//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", + "@com_google_googleapis//google/type:type_java_proto", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", "@maven//:com_google_testparameterinjector_test_parameter_injector", "@maven//:junit_junit", + "@maven_android//:com_google_protobuf_protobuf_javalite", ], ) diff --git a/common/src/test/java/dev/cel/common/internal/CelCodePointArrayTest.java b/common/src/test/java/dev/cel/common/internal/CelCodePointArrayTest.java new file mode 100644 index 000000000..7cb02c5a8 --- /dev/null +++ b/common/src/test/java/dev/cel/common/internal/CelCodePointArrayTest.java @@ -0,0 +1,77 @@ +// 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. + +package dev.cel.common.internal; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import com.google.testing.junit.testparameterinjector.TestParameter; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import com.google.testing.junit.testparameterinjector.TestParameterValuesProvider; +import java.util.Arrays; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public final class CelCodePointArrayTest { + + @Test + public void computeLineOffset( + @TestParameter(valuesProvider = LineOffsetDataProvider.class) LineOffsetTestCase testCase) { + CelCodePointArray codePointArray = CelCodePointArray.fromString(testCase.text()); + + assertThat(codePointArray.lineOffsets()) + .containsExactlyElementsIn(testCase.offsets()) + .inOrder(); + } + + @AutoValue + abstract static class LineOffsetTestCase { + abstract String text(); + + abstract ImmutableList offsets(); + + static LineOffsetTestCase of(String text, Integer... offsets) { + return of(text, Arrays.asList(offsets)); + } + + static LineOffsetTestCase of(String text, List offsets) { + return new AutoValue_CelCodePointArrayTest_LineOffsetTestCase( + text, ImmutableList.copyOf(offsets)); + } + } + + private static final class LineOffsetDataProvider extends TestParameterValuesProvider { + + @Override + protected List provideValues(Context context) { + return Arrays.asList( + // Empty + LineOffsetTestCase.of("", 1), + // ISO-8859-1 + LineOffsetTestCase.of("hello world", 12), + LineOffsetTestCase.of("hello\nworld", 6, 12), + LineOffsetTestCase.of("hello\nworld\n\nfoo\n", 6, 12, 13, 17, 18), + // BMP + LineOffsetTestCase.of("abc 가나다", 8), + LineOffsetTestCase.of("abc\n가나다\n我b很好\n", 4, 8, 13, 14), + // SMP + LineOffsetTestCase.of(" text 가나다 😦😁😑 ", 15), + LineOffsetTestCase.of(" text\n가나다 \n😦😁😑\n\n", 6, 11, 15, 16, 17)); + } + } +} diff --git a/common/src/test/java/dev/cel/common/internal/CombinedDescriptorPoolTest.java b/common/src/test/java/dev/cel/common/internal/CombinedDescriptorPoolTest.java index a56bb2ed5..877d6a976 100644 --- a/common/src/test/java/dev/cel/common/internal/CombinedDescriptorPoolTest.java +++ b/common/src/test/java/dev/cel/common/internal/CombinedDescriptorPoolTest.java @@ -21,9 +21,8 @@ import com.google.protobuf.ExtensionRegistry; import com.google.protobuf.Value; import dev.cel.common.CelDescriptorUtil; -import dev.cel.testing.testdata.proto2.MessagesProto2Extensions; -import dev.cel.testing.testdata.proto2.Proto2Message; -import dev.cel.testing.testdata.proto2.TestAllTypesProto.TestAllTypes; +import dev.cel.expr.conformance.proto2.TestAllTypes; +import dev.cel.expr.conformance.proto2.TestAllTypesExtensions; import java.util.Optional; import org.junit.Test; import org.junit.runner.RunWith; @@ -67,19 +66,19 @@ public void findExtensionDescriptor_success() { CelDescriptorPool dynamicDescriptorPool = DefaultDescriptorPool.create( CelDescriptorUtil.getAllDescriptorsFromFileDescriptor( - MessagesProto2Extensions.getDescriptor().getFile())); + TestAllTypesExtensions.getDescriptor().getFile())); CombinedDescriptorPool combinedDescriptorPool = CombinedDescriptorPool.create( ImmutableList.of(DefaultDescriptorPool.INSTANCE, dynamicDescriptorPool)); Optional fieldDescriptor = combinedDescriptorPool.findExtensionDescriptor( - Proto2Message.getDescriptor(), "dev.cel.testing.testdata.proto2.test_all_types_ext"); + TestAllTypes.getDescriptor(), "cel.expr.conformance.proto2.test_all_types_ext"); assertThat(fieldDescriptor).isPresent(); assertThat(fieldDescriptor.get().isExtension()).isTrue(); assertThat(fieldDescriptor.get().getFullName()) - .isEqualTo("dev.cel.testing.testdata.proto2.test_all_types_ext"); + .isEqualTo("cel.expr.conformance.proto2.test_all_types_ext"); } @Test @@ -87,7 +86,7 @@ public void findExtensionDescriptor_returnsEmpty() { CelDescriptorPool dynamicDescriptorPool = DefaultDescriptorPool.create( CelDescriptorUtil.getAllDescriptorsFromFileDescriptor( - MessagesProto2Extensions.getDescriptor().getFile())); + TestAllTypesExtensions.getDescriptor().getFile())); CombinedDescriptorPool combinedDescriptorPool = CombinedDescriptorPool.create( ImmutableList.of(DefaultDescriptorPool.INSTANCE, dynamicDescriptorPool)); diff --git a/common/src/test/java/dev/cel/common/internal/ComparisonFunctionsTest.java b/common/src/test/java/dev/cel/common/internal/ComparisonFunctionsTest.java index f218583e8..7508d2ed8 100644 --- a/common/src/test/java/dev/cel/common/internal/ComparisonFunctionsTest.java +++ b/common/src/test/java/dev/cel/common/internal/ComparisonFunctionsTest.java @@ -64,4 +64,33 @@ public void compareUintIntEdgeCases() { assertThat(ComparisonFunctions.compareUintInt(ux, 1)).isEqualTo(1); assertThat(ComparisonFunctions.compareIntUint(1, ux)).isEqualTo(-1); } + + @Test + @TestParameters("{x: 1, y: 1, expect: 0}") + @TestParameters("{x: 1, y: 2, expect: -1}") + @TestParameters("{x: 2, y: -1, expect: 1}") + public void numericCompareDoubleInt(double x, long y, int expect) { + assertThat(ComparisonFunctions.numericCompare(x, y)).isEqualTo(expect); + assertThat(ComparisonFunctions.numericCompare(y, x)).isEqualTo(-1 * expect); + } + + @Test + @TestParameters("{x: 1, y: 1, expect: 0}") + @TestParameters("{x: 1, y: 2, expect: -1}") + @TestParameters("{x: 2, y: 1, expect: 1}") + public void numericCompareDoubleUint(double x, long y, int expect) { + UnsignedLong uy = UnsignedLong.valueOf(y); + assertThat(ComparisonFunctions.numericCompare(x, uy)).isEqualTo(expect); + assertThat(ComparisonFunctions.numericCompare(uy, x)).isEqualTo(-1 * expect); + } + + @Test + @TestParameters("{x: 1, y: 1, expect: 0}") + @TestParameters("{x: 1, y: 2, expect: -1}") + @TestParameters("{x: 2, y: -1, expect: 1}") + public void numericCompareUintInt(long x, long y, int expect) { + UnsignedLong ux = UnsignedLong.valueOf(x); + assertThat(ComparisonFunctions.numericCompare(ux, y)).isEqualTo(expect); + assertThat(ComparisonFunctions.numericCompare(y, ux)).isEqualTo(-1 * expect); + } } diff --git a/common/src/test/java/dev/cel/common/internal/ConstantsTest.java b/common/src/test/java/dev/cel/common/internal/ConstantsTest.java index d222f9df5..f4a9fc32e 100644 --- a/common/src/test/java/dev/cel/common/internal/ConstantsTest.java +++ b/common/src/test/java/dev/cel/common/internal/ConstantsTest.java @@ -18,11 +18,11 @@ import static org.junit.Assert.assertThrows; import com.google.common.primitives.UnsignedLong; -import com.google.protobuf.ByteString; import com.google.testing.junit.testparameterinjector.TestParameterInjector; import com.google.testing.junit.testparameterinjector.TestParameters; import dev.cel.common.ast.CelConstant; import dev.cel.common.ast.CelConstant.Kind; +import dev.cel.common.values.CelByteString; import java.text.ParseException; import org.junit.Test; import org.junit.runner.RunWith; @@ -336,7 +336,7 @@ public void parseBytes_multibyteCodePoints() throws Exception { private static void testBytes(String actual, String expected) throws Exception { CelConstant constant = Constants.parseBytes(actual); assertThat(constant.getKind()).isEqualTo(Kind.BYTES_VALUE); - assertThat(constant.bytesValue()).isEqualTo(ByteString.copyFromUtf8(expected)); + assertThat(constant.bytesValue()).isEqualTo(CelByteString.copyFromUtf8(expected)); } private static void testQuotedBytes(String actual, String expected) throws Exception { @@ -380,7 +380,7 @@ public void parseRawBytes_escapeSequence() throws Exception { private static void testRawBytes(String actual, String expected) throws Exception { CelConstant constant = Constants.parseBytes(actual); assertThat(constant.getKind()).isEqualTo(Kind.BYTES_VALUE); - assertThat(constant.bytesValue()).isEqualTo(ByteString.copyFromUtf8(expected)); + assertThat(constant.bytesValue()).isEqualTo(CelByteString.copyFromUtf8(expected)); } private static void testRawQuotedBytes(String actual, String expected) throws Exception { diff --git a/common/src/test/java/dev/cel/common/internal/DefaultInstanceMessageFactoryTest.java b/common/src/test/java/dev/cel/common/internal/DefaultInstanceMessageFactoryTest.java index a167190f9..d6749a391 100644 --- a/common/src/test/java/dev/cel/common/internal/DefaultInstanceMessageFactoryTest.java +++ b/common/src/test/java/dev/cel/common/internal/DefaultInstanceMessageFactoryTest.java @@ -31,7 +31,7 @@ import com.google.testing.junit.testparameterinjector.TestParameterInjector; import dev.cel.common.testdata.ProtoJavaApiVersion1.Proto2JavaVersion1Message; import dev.cel.common.testing.RepeatedTestProvider; -import dev.cel.testing.testdata.proto3.TestAllTypesProto.TestAllTypes; +import dev.cel.expr.conformance.proto3.TestAllTypes; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -44,12 +44,12 @@ import org.junit.runner.RunWith; @RunWith(TestParameterInjector.class) -public final class DefaultInstanceMessageFactoryTest { +public class DefaultInstanceMessageFactoryTest { @Before public void setUp() { // Reset the statically initialized descriptor map to get clean test runs. - DefaultInstanceMessageFactory.getInstance().resetDescriptorMapForTesting(); + DefaultInstanceMessageLiteFactory.getInstance().resetTypeMap(); } private enum PrototypeDescriptorTestCase { @@ -112,7 +112,7 @@ public void getPrototype_cached_success(@TestParameter PrototypeDescriptorTestCa @Test public void getPrototype_concurrentAccess_doesNotThrow( - @TestParameter(valuesProvider = RepeatedTestProvider.class) int testRunIndex) + @TestParameter(valuesProvider = RepeatedTestProvider.class) int unusedTestRunIndex) throws Exception { // Arrange int threadCount = 10; diff --git a/common/src/test/java/dev/cel/common/internal/DefaultLiteDescriptorPoolTest.java b/common/src/test/java/dev/cel/common/internal/DefaultLiteDescriptorPoolTest.java new file mode 100644 index 000000000..198cfde41 --- /dev/null +++ b/common/src/test/java/dev/cel/common/internal/DefaultLiteDescriptorPoolTest.java @@ -0,0 +1,121 @@ +// 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 static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableSet; +import com.google.common.truth.Expect; +import com.google.protobuf.Descriptors.Descriptor; +import com.google.protobuf.Descriptors.FieldDescriptor; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.expr.conformance.proto3.TestAllTypesCelDescriptor; +import dev.cel.protobuf.CelLiteDescriptor.FieldLiteDescriptor; +import dev.cel.protobuf.CelLiteDescriptor.FieldLiteDescriptor.EncodingType; +import dev.cel.protobuf.CelLiteDescriptor.MessageLiteDescriptor; +import java.util.NoSuchElementException; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public class DefaultLiteDescriptorPoolTest { + @Rule public final Expect expect = Expect.create(); + + @Test + public void containsAllWellKnownProtos() { + DefaultLiteDescriptorPool descriptorPool = + DefaultLiteDescriptorPool.newInstance(ImmutableSet.of()); + + for (WellKnownProto wellKnownProto : WellKnownProto.values()) { + MessageLiteDescriptor liteDescriptor = + descriptorPool.getDescriptorOrThrow(wellKnownProto.typeName()); + expect.that(liteDescriptor.getProtoTypeName()).isEqualTo(wellKnownProto.typeName()); + } + } + + @Test + public void wellKnownProto_compareAgainstFullDescriptors_allFieldPropertiesAreEqual() { + DefaultLiteDescriptorPool descriptorPool = + DefaultLiteDescriptorPool.newInstance(ImmutableSet.of()); + + for (WellKnownProto wellKnownProto : WellKnownProto.values()) { + Descriptor fullDescriptor = + DefaultDescriptorPool.INSTANCE.findDescriptor(wellKnownProto.typeName()).get(); + MessageLiteDescriptor liteDescriptor = + descriptorPool.getDescriptorOrThrow(wellKnownProto.typeName()); + + for (FieldDescriptor fullFieldDescriptor : fullDescriptor.getFields()) { + String expectMessageTitle = + wellKnownProto.typeName() + ", field number: " + fullFieldDescriptor.getNumber(); + FieldLiteDescriptor fieldLiteDescriptor = + liteDescriptor.getByFieldNumberOrThrow(fullFieldDescriptor.getNumber()); + + expect + .withMessage(expectMessageTitle) + .that(fieldLiteDescriptor.getFieldName()) + .isEqualTo(fullFieldDescriptor.getName()); + expect + .withMessage(expectMessageTitle) + .that(fieldLiteDescriptor.getIsPacked()) + .isEqualTo(fullFieldDescriptor.isPacked()); + expect + .withMessage(expectMessageTitle) + .that(fieldLiteDescriptor.getEncodingType()) + .isEqualTo( + fullFieldDescriptor.isMapField() + ? EncodingType.MAP + : fullFieldDescriptor.isRepeated() ? EncodingType.LIST : EncodingType.SINGULAR); + // Note: enums such as JavaType are semantically equal, but their instances differ. + expect + .withMessage(expectMessageTitle) + .that(fieldLiteDescriptor.getJavaType().toString()) + .isEqualTo(fullFieldDescriptor.getJavaType().toString()); + expect + .withMessage(expectMessageTitle) + .that(fieldLiteDescriptor.getProtoFieldType().toString()) + .isEqualTo(fullFieldDescriptor.getType().toString()); + if (fullFieldDescriptor.getType().equals(FieldDescriptor.Type.MESSAGE)) { + expect + .withMessage(expectMessageTitle) + .that(fieldLiteDescriptor.getFieldProtoTypeName()) + .isEqualTo(fullFieldDescriptor.getMessageType().getFullName()); + } + } + } + } + + @Test + public void findDescriptor_success() { + DefaultLiteDescriptorPool descriptorPool = + DefaultLiteDescriptorPool.newInstance( + ImmutableSet.of(TestAllTypesCelDescriptor.getDescriptor())); + + MessageLiteDescriptor liteDescriptor = + descriptorPool.getDescriptorOrThrow("cel.expr.conformance.proto3.TestAllTypes"); + + assertThat(liteDescriptor.getProtoTypeName()) + .isEqualTo("cel.expr.conformance.proto3.TestAllTypes"); + } + + @Test + public void findDescriptor_throws() { + DefaultLiteDescriptorPool descriptorPool = + DefaultLiteDescriptorPool.newInstance(ImmutableSet.of()); + + assertThrows(NoSuchElementException.class, () -> descriptorPool.getDescriptorOrThrow("foo")); + } +} diff --git a/common/src/test/java/dev/cel/common/internal/DefaultMessageFactoryTest.java b/common/src/test/java/dev/cel/common/internal/DefaultMessageFactoryTest.java index aef6140bf..b26197d04 100644 --- a/common/src/test/java/dev/cel/common/internal/DefaultMessageFactoryTest.java +++ b/common/src/test/java/dev/cel/common/internal/DefaultMessageFactoryTest.java @@ -29,7 +29,7 @@ import dev.cel.common.CelDescriptorUtil; import dev.cel.common.CelDescriptors; import dev.cel.common.internal.ProtoMessageFactory.CombinedMessageFactory; -import dev.cel.testing.testdata.proto2.TestAllTypesProto.TestAllTypes; +import dev.cel.expr.conformance.proto3.TestAllTypes; import java.util.Optional; import org.junit.Test; import org.junit.runner.RunWith; @@ -59,7 +59,7 @@ public void newBuilder_withDescriptor_producesNewMessageBuilder() { TestAllTypes.Builder builder = (TestAllTypes.Builder) - messageFactory.newBuilder("dev.cel.testing.testdata.proto2.TestAllTypes").get(); + messageFactory.newBuilder("cel.expr.conformance.proto3.TestAllTypes").get(); assertThat(builder.setSingleInt64(5L).build()) .isEqualTo(TestAllTypes.newBuilder().setSingleInt64(5L).build()); 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 151f79f49..91e0e22db 100644 --- a/common/src/test/java/dev/cel/common/internal/ProtoAdapterTest.java +++ b/common/src/test/java/dev/cel/common/internal/ProtoAdapterTest.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.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -25,6 +26,8 @@ import com.google.protobuf.BytesValue; import com.google.protobuf.DoubleValue; import com.google.protobuf.Duration; +import com.google.protobuf.Empty; +import com.google.protobuf.FieldMask; import com.google.protobuf.FloatValue; import com.google.protobuf.Int64Value; import com.google.protobuf.ListValue; @@ -37,6 +40,8 @@ import com.google.protobuf.Value; import com.google.type.Expr; import dev.cel.common.CelOptions; +import dev.cel.common.values.CelByteString; +import java.time.Instant; import java.util.Arrays; import java.util.List; import java.util.Optional; @@ -51,10 +56,6 @@ @RunWith(Enclosed.class) public final class ProtoAdapterTest { - private static final CelOptions LEGACY = CelOptions.DEFAULT; - private static final CelOptions CURRENT = - CelOptions.newBuilder().enableUnsignedLongs(true).build(); - private static final DynamicProto DYNAMIC_PROTO = DynamicProto.create(DefaultMessageFactory.INSTANCE); @@ -66,50 +67,40 @@ public static class BidirectionalConversionTest { @Parameter(1) public Message proto; - @Parameter(2) - public CelOptions options; - @Parameters public static List data() { return Arrays.asList( new Object[][] { { - NullValue.NULL_VALUE, + dev.cel.common.values.NullValue.NULL_VALUE, Any.pack(Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build()), - LEGACY }, - {true, BoolValue.of(true), LEGACY}, - {true, Any.pack(BoolValue.of(true)), LEGACY}, - {true, Value.newBuilder().setBoolValue(true).build(), LEGACY}, + {true, BoolValue.of(true)}, + {true, Any.pack(BoolValue.of(true))}, + {true, Value.newBuilder().setBoolValue(true).build()}, { - ByteString.copyFromUtf8("hello"), - BytesValue.of(ByteString.copyFromUtf8("hello")), - LEGACY + CelByteString.copyFromUtf8("hello"), BytesValue.of(ByteString.copyFromUtf8("hello")), }, { - ByteString.copyFromUtf8("hello"), + CelByteString.copyFromUtf8("hello"), Any.pack(BytesValue.of(ByteString.copyFromUtf8("hello"))), - LEGACY }, - {1.5D, DoubleValue.of(1.5D), LEGACY}, - {1.5D, Any.pack(DoubleValue.of(1.5D)), LEGACY}, - {1.5D, Value.newBuilder().setNumberValue(1.5D).build(), LEGACY}, + {1.5D, DoubleValue.of(1.5D)}, + {1.5D, Any.pack(DoubleValue.of(1.5D))}, + {1.5D, Value.newBuilder().setNumberValue(1.5D).build()}, { - Duration.newBuilder().setSeconds(123).build(), - Duration.newBuilder().setSeconds(123).build(), - LEGACY + java.time.Duration.ofSeconds(123), Duration.newBuilder().setSeconds(123).build(), }, { - Duration.newBuilder().setSeconds(123).build(), + java.time.Duration.ofSeconds(123), Any.pack(Duration.newBuilder().setSeconds(123).build()), - LEGACY }, - {1L, Int64Value.of(1L), LEGACY}, - {1L, Any.pack(Int64Value.of(1L)), LEGACY}, - {1L, UInt64Value.of(1L), LEGACY}, - {"hello", StringValue.of("hello"), LEGACY}, - {"hello", Any.pack(StringValue.of("hello")), LEGACY}, - {"hello", Value.newBuilder().setStringValue("hello").build(), LEGACY}, + {1L, Int64Value.of(1L)}, + {1L, Any.pack(Int64Value.of(1L))}, + {UnsignedLong.valueOf(1L), UInt64Value.of(1L)}, + {"hello", StringValue.of("hello")}, + {"hello", Any.pack(StringValue.of("hello"))}, + {"hello", Value.newBuilder().setStringValue("hello").build()}, { Arrays.asList("hello", "world"), Any.pack( @@ -117,7 +108,6 @@ public static List data() { .addValues(Value.newBuilder().setStringValue("hello")) .addValues(Value.newBuilder().setStringValue("world")) .build()), - LEGACY }, { ImmutableMap.of("hello", "world"), @@ -125,10 +115,11 @@ public static List data() { Struct.newBuilder() .putFields("hello", Value.newBuilder().setStringValue("world").build()) .build()), - LEGACY }, { - ImmutableMap.of("list_value", ImmutableList.of(false, NullValue.NULL_VALUE)), + ImmutableMap.of( + "list_value", + ImmutableList.of(false, dev.cel.common.values.NullValue.NULL_VALUE)), Struct.newBuilder() .putFields( "list_value", @@ -139,30 +130,29 @@ public static List data() { .addValues(Value.newBuilder().setNullValue(NullValue.NULL_VALUE))) .build()) .build(), - LEGACY }, { - Timestamp.newBuilder().setSeconds(123).build(), - Timestamp.newBuilder().setSeconds(123).build(), - LEGACY + Instant.ofEpochSecond(123), Timestamp.newBuilder().setSeconds(123).build(), }, { - Timestamp.newBuilder().setSeconds(123).build(), - Any.pack(Timestamp.newBuilder().setSeconds(123).build()), - LEGACY + Instant.ofEpochSecond(123), Any.pack(Timestamp.newBuilder().setSeconds(123).build()), }, - // Adaption support for the most current CelOptions. - {UnsignedLong.valueOf(1L), UInt64Value.of(1L), CURRENT}, - {UnsignedLong.valueOf(1L), Any.pack(UInt64Value.of(1L)), CURRENT}, + {UnsignedLong.valueOf(1L), UInt64Value.of(1L)}, + {UnsignedLong.valueOf(1L), Any.pack(UInt64Value.of(1L))}, + {Empty.getDefaultInstance(), Empty.getDefaultInstance()}, + { + FieldMask.newBuilder().addPaths("foo").build(), + FieldMask.newBuilder().addPaths("foo").build() + } }); } @Test public void adaptValueToProto_bidirectionalConversion() { DynamicProto dynamicProto = DynamicProto.create(DefaultMessageFactory.INSTANCE); - ProtoAdapter protoAdapter = new ProtoAdapter(dynamicProto, options.enableUnsignedLongs()); + ProtoAdapter protoAdapter = new ProtoAdapter(dynamicProto, CelOptions.current().build()); assertThat(protoAdapter.adaptValueToProto(value, proto.getDescriptorForType().getFullName())) - .hasValue(proto); + .isEqualTo(proto); assertThat(protoAdapter.adaptProtoToValue(proto)).isEqualTo(value); } } @@ -179,94 +169,101 @@ public void adaptAnyValue_hermeticTypes_bidirectionalConversion() { typeName.equals(Expr.getDescriptor().getFullName()) ? Optional.of(Expr.newBuilder()) : Optional.empty()), - LEGACY.enableUnsignedLongs()); + CelOptions.DEFAULT); assertThat(protoAdapter.adaptValueToProto(expr, Any.getDescriptor().getFullName())) - .hasValue(Any.pack(expr)); + .isEqualTo(Any.pack(expr)); assertThat(protoAdapter.adaptProtoToValue(Any.pack(expr))).isEqualTo(expr); } } @RunWith(JUnit4.class) public static class AsymmetricConversionTest { + @Test - public void adaptValueToProto_asymmetricNullConversion() { - ProtoAdapter protoAdapter = new ProtoAdapter(DYNAMIC_PROTO, LEGACY.enableUnsignedLongs()); - assertThat(protoAdapter.adaptValueToProto(null, Any.getDescriptor().getFullName())) - .hasValue(Any.pack(Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build())); - assertThat( - protoAdapter.adaptProtoToValue( - Any.pack(Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build()))) - .isEqualTo(NullValue.NULL_VALUE); + 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, LEGACY.enableUnsignedLongs()); + ProtoAdapter protoAdapter = new ProtoAdapter(DYNAMIC_PROTO, CelOptions.DEFAULT); assertThat(protoAdapter.adaptValueToProto(1.5F, Any.getDescriptor().getFullName())) - .hasValue(Any.pack(FloatValue.of(1.5F))); + .isEqualTo(Any.pack(FloatValue.of(1.5F))); assertThat(protoAdapter.adaptProtoToValue(Any.pack(FloatValue.of(1.5F)))).isEqualTo(1.5D); } @Test public void adaptValueToProto_asymmetricDoubleFloatConversion() { - ProtoAdapter protoAdapter = new ProtoAdapter(DYNAMIC_PROTO, LEGACY.enableUnsignedLongs()); + ProtoAdapter protoAdapter = new ProtoAdapter(DYNAMIC_PROTO, CelOptions.DEFAULT); assertThat(protoAdapter.adaptValueToProto(1.5D, FloatValue.getDescriptor().getFullName())) - .hasValue(FloatValue.of(1.5F)); + .isEqualTo(FloatValue.of(1.5F)); assertThat(protoAdapter.adaptProtoToValue(FloatValue.of(1.5F))).isEqualTo(1.5D); } @Test public void adaptValueToProto_asymmetricFloatDoubleConversion() { - ProtoAdapter protoAdapter = new ProtoAdapter(DYNAMIC_PROTO, LEGACY.enableUnsignedLongs()); + ProtoAdapter protoAdapter = new ProtoAdapter(DYNAMIC_PROTO, CelOptions.DEFAULT); assertThat(protoAdapter.adaptValueToProto(1.5F, DoubleValue.getDescriptor().getFullName())) - .hasValue(DoubleValue.of(1.5D)); + .isEqualTo(DoubleValue.of(1.5D)); } @Test public void adaptValueToProto_asymmetricJsonConversion() { - ProtoAdapter protoAdapter = new ProtoAdapter(DYNAMIC_PROTO, CURRENT.enableUnsignedLongs()); + ProtoAdapter protoAdapter = new ProtoAdapter(DYNAMIC_PROTO, CelOptions.DEFAULT); assertThat( protoAdapter.adaptValueToProto( UnsignedLong.valueOf(1L), Value.getDescriptor().getFullName())) - .hasValue(Value.newBuilder().setNumberValue(1).build()); + .isEqualTo(Value.newBuilder().setNumberValue(1).build()); assertThat( protoAdapter.adaptValueToProto( UnsignedLong.fromLongBits(-1L), Value.getDescriptor().getFullName())) - .hasValue(Value.newBuilder().setStringValue(Long.toUnsignedString(-1L)).build()); + .isEqualTo(Value.newBuilder().setStringValue(Long.toUnsignedString(-1L)).build()); assertThat(protoAdapter.adaptValueToProto(1L, Value.getDescriptor().getFullName())) - .hasValue(Value.newBuilder().setNumberValue(1).build()); + .isEqualTo(Value.newBuilder().setNumberValue(1).build()); assertThat( protoAdapter.adaptValueToProto(Long.MAX_VALUE, Value.getDescriptor().getFullName())) - .hasValue(Value.newBuilder().setStringValue(Long.toString(Long.MAX_VALUE)).build()); + .isEqualTo(Value.newBuilder().setStringValue(Long.toString(Long.MAX_VALUE)).build()); assertThat( protoAdapter.adaptValueToProto( - ByteString.copyFromUtf8("foo"), Value.getDescriptor().getFullName())) - .hasValue(Value.newBuilder().setStringValue("Zm9v").build()); + CelByteString.copyFromUtf8("foo"), Value.getDescriptor().getFullName())) + .isEqualTo(Value.newBuilder().setStringValue("Zm9v").build()); } @Test public void adaptValueToProto_unsupportedJsonConversion() { - ProtoAdapter protoAdapter = new ProtoAdapter(DYNAMIC_PROTO, LEGACY.enableUnsignedLongs()); - assertThat( + ProtoAdapter protoAdapter = new ProtoAdapter(DYNAMIC_PROTO, CelOptions.DEFAULT); + + assertThrows( + ClassCastException.class, + () -> protoAdapter.adaptValueToProto( - ImmutableMap.of(1, 1), Any.getDescriptor().getFullName())) - .isEmpty(); + ImmutableMap.of(1, 1), Any.getDescriptor().getFullName())); } @Test public void adaptValueToProto_unsupportedJsonListConversion() { - ProtoAdapter protoAdapter = new ProtoAdapter(DYNAMIC_PROTO, LEGACY.enableUnsignedLongs()); - assertThat( + ProtoAdapter protoAdapter = new ProtoAdapter(DYNAMIC_PROTO, CelOptions.DEFAULT); + + assertThrows( + ClassCastException.class, + () -> protoAdapter.adaptValueToProto( - ImmutableMap.of(1, 1), ListValue.getDescriptor().getFullName())) - .isEmpty(); + ImmutableMap.of(1, 1), ListValue.getDescriptor().getFullName())); } @Test public void adaptValueToProto_unsupportedConversion() { - ProtoAdapter protoAdapter = new ProtoAdapter(DYNAMIC_PROTO, LEGACY.enableUnsignedLongs()); - assertThat(protoAdapter.adaptValueToProto("Hello", Expr.getDescriptor().getFullName())) - .isEmpty(); + ProtoAdapter protoAdapter = new ProtoAdapter(DYNAMIC_PROTO, CelOptions.DEFAULT); + + assertThrows( + IllegalStateException.class, + () -> protoAdapter.adaptValueToProto("Hello", Expr.getDescriptor().getFullName())); } @Test diff --git a/common/src/test/java/dev/cel/common/internal/ProtoEqualityTest.java b/common/src/test/java/dev/cel/common/internal/ProtoEqualityTest.java index 7cbabf9bc..d35947b9a 100644 --- a/common/src/test/java/dev/cel/common/internal/ProtoEqualityTest.java +++ b/common/src/test/java/dev/cel/common/internal/ProtoEqualityTest.java @@ -21,9 +21,9 @@ import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.Struct; import com.google.protobuf.Value; -import dev.cel.testing.testdata.proto3.TestAllTypesProto.TestAllTypes; -import dev.cel.testing.testdata.proto3.TestAllTypesProto.TestAllTypes.NestedEnum; -import dev.cel.testing.testdata.proto3.TestAllTypesProto.TestAllTypes.NestedMessage; +import dev.cel.expr.conformance.proto3.TestAllTypes; +import dev.cel.expr.conformance.proto3.TestAllTypes.NestedEnum; +import dev.cel.expr.conformance.proto3.TestAllTypes.NestedMessage; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/common/src/test/java/dev/cel/common/internal/ProtoTimeUtilsTest.java b/common/src/test/java/dev/cel/common/internal/ProtoTimeUtilsTest.java new file mode 100644 index 000000000..31ee9a9eb --- /dev/null +++ b/common/src/test/java/dev/cel/common/internal/ProtoTimeUtilsTest.java @@ -0,0 +1,72 @@ +// 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 static com.google.common.truth.Truth.assertThat; +import static dev.cel.common.internal.ProtoTimeUtils.DURATION_SECONDS_MAX; +import static dev.cel.common.internal.ProtoTimeUtils.DURATION_SECONDS_MIN; + +import com.google.protobuf.Duration; +import com.google.protobuf.Timestamp; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import java.time.Instant; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public class ProtoTimeUtilsTest { + + @Test + public void toJavaInstant_overRfc3339Range() { + Timestamp ts = + Timestamp.newBuilder().setSeconds(ProtoTimeUtils.TIMESTAMP_SECONDS_MAX + 1).build(); + + Instant instant = ProtoTimeUtils.toJavaInstant(ts); + + assertThat(instant).isEqualTo(Instant.ofEpochSecond(ProtoTimeUtils.TIMESTAMP_SECONDS_MAX + 1)); + } + + @Test + public void toJavaInstant_underRfc3339Range() { + Timestamp ts = + Timestamp.newBuilder().setSeconds(ProtoTimeUtils.TIMESTAMP_SECONDS_MIN - 1).build(); + + Instant instant = ProtoTimeUtils.toJavaInstant(ts); + + assertThat(instant).isEqualTo(Instant.ofEpochSecond(ProtoTimeUtils.TIMESTAMP_SECONDS_MIN - 1)); + } + + @Test + public void toJavaDuration_overRfc3339Range() { + Duration d = Duration.newBuilder() + .setSeconds(DURATION_SECONDS_MAX + 1) + .build(); + + java.time.Duration duration = ProtoTimeUtils.toJavaDuration(d); + + assertThat(duration).isEqualTo(java.time.Duration.ofSeconds(DURATION_SECONDS_MAX + 1)); + } + + @Test + public void toJavaDuration_underRfc3339Range() { + Duration d = Duration.newBuilder() + .setSeconds(DURATION_SECONDS_MIN - 1) + .build(); + + java.time.Duration duration = ProtoTimeUtils.toJavaDuration(d); + + assertThat(duration).isEqualTo(java.time.Duration.ofSeconds(DURATION_SECONDS_MIN - 1)); + } +} diff --git a/common/src/test/java/dev/cel/common/internal/WellKnownProtoTest.java b/common/src/test/java/dev/cel/common/internal/WellKnownProtoTest.java new file mode 100644 index 000000000..c12411d95 --- /dev/null +++ b/common/src/test/java/dev/cel/common/internal/WellKnownProtoTest.java @@ -0,0 +1,146 @@ +// 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 static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableList; +import com.google.protobuf.Any; +import com.google.protobuf.BoolValue; +import com.google.protobuf.BytesValue; +import com.google.protobuf.DoubleValue; +import com.google.protobuf.Duration; +import com.google.protobuf.Empty; +import com.google.protobuf.FieldMask; +import com.google.protobuf.FloatValue; +import com.google.protobuf.Int32Value; +import com.google.protobuf.Int64Value; +import com.google.protobuf.ListValue; +import com.google.protobuf.StringValue; +import com.google.protobuf.Struct; +import com.google.protobuf.Timestamp; +import com.google.protobuf.UInt32Value; +import com.google.protobuf.UInt64Value; +import com.google.protobuf.Value; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import com.google.testing.junit.testparameterinjector.TestParameters; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public class WellKnownProtoTest { + + @Test + @TestParameters("{typeName: 'google.protobuf.FloatValue'}") + @TestParameters("{typeName: 'google.protobuf.Int32Value'}") + @TestParameters("{typeName: 'google.protobuf.Int64Value'}") + @TestParameters("{typeName: 'google.protobuf.StringValue'}") + @TestParameters("{typeName: 'google.protobuf.BoolValue'}") + @TestParameters("{typeName: 'google.protobuf.BytesValue'}") + @TestParameters("{typeName: 'google.protobuf.DoubleValue'}") + @TestParameters("{typeName: 'google.protobuf.UInt32Value'}") + @TestParameters("{typeName: 'google.protobuf.UInt64Value'}") + @TestParameters("{typeName: 'google.protobuf.Empty'}") + @TestParameters("{typeName: 'google.protobuf.FieldMask'}") + public void isWrapperType_withTypeName_true(String typeName) { + assertThat(WellKnownProto.isWrapperType(typeName)).isTrue(); + } + + @Test + @TestParameters("{typeName: 'not.wellknown.type'}") + @TestParameters("{typeName: 'google.protobuf.Any'}") + @TestParameters("{typeName: 'google.protobuf.Duration'}") + @TestParameters("{typeName: 'google.protobuf.ListValue'}") + @TestParameters("{typeName: 'google.protobuf.Struct'}") + @TestParameters("{typeName: 'google.protobuf.Value'}") + @TestParameters("{typeName: 'google.protobuf.Timestamp'}") + public void isWrapperType_withTypeName_false(String typeName) { + assertThat(WellKnownProto.isWrapperType(typeName)).isFalse(); + } + + @Test + public void getByClass_success() { + assertThat(WellKnownProto.getByClass(FloatValue.class)).hasValue(WellKnownProto.FLOAT_VALUE); + } + + @Test + public void getByClass_unknownClass_returnsEmpty() { + assertThat(WellKnownProto.getByClass(List.class)).isEmpty(); + } + + @Test + public void getByPathName_singular() { + assertThat(WellKnownProto.getByPathName(Any.getDescriptor().getFile().getName())) + .containsExactly(WellKnownProto.ANY_VALUE); + assertThat(WellKnownProto.getByPathName(Duration.getDescriptor().getFile().getName())) + .containsExactly(WellKnownProto.DURATION); + assertThat(WellKnownProto.getByPathName(Timestamp.getDescriptor().getFile().getName())) + .containsExactly(WellKnownProto.TIMESTAMP); + assertThat(WellKnownProto.getByPathName(Empty.getDescriptor().getFile().getName())) + .containsExactly(WellKnownProto.EMPTY); + assertThat(WellKnownProto.getByPathName(FieldMask.getDescriptor().getFile().getName())) + .containsExactly(WellKnownProto.FIELD_MASK); + } + + @Test + public void getByPathName_json() { + ImmutableList expectedWellKnownProtos = + ImmutableList.of( + WellKnownProto.JSON_STRUCT_VALUE, + WellKnownProto.JSON_VALUE, + WellKnownProto.JSON_LIST_VALUE); + assertThat(WellKnownProto.getByPathName(Struct.getDescriptor().getFile().getName())) + .containsExactlyElementsIn(expectedWellKnownProtos); + assertThat(WellKnownProto.getByPathName(Value.getDescriptor().getFile().getName())) + .containsExactlyElementsIn(expectedWellKnownProtos); + assertThat(WellKnownProto.getByPathName(ListValue.getDescriptor().getFile().getName())) + .containsExactlyElementsIn(expectedWellKnownProtos); + } + + @Test + public void getByPathName_wrappers() { + ImmutableList expectedWellKnownProtos = + ImmutableList.of( + WellKnownProto.FLOAT_VALUE, + WellKnownProto.DOUBLE_VALUE, + WellKnownProto.INT32_VALUE, + WellKnownProto.INT64_VALUE, + WellKnownProto.UINT32_VALUE, + WellKnownProto.UINT64_VALUE, + WellKnownProto.BOOL_VALUE, + WellKnownProto.STRING_VALUE, + WellKnownProto.BYTES_VALUE); + + assertThat(WellKnownProto.getByPathName(FloatValue.getDescriptor().getFile().getName())) + .containsExactlyElementsIn(expectedWellKnownProtos); + assertThat(WellKnownProto.getByPathName(DoubleValue.getDescriptor().getFile().getName())) + .containsExactlyElementsIn(expectedWellKnownProtos); + assertThat(WellKnownProto.getByPathName(Int32Value.getDescriptor().getFile().getName())) + .containsExactlyElementsIn(expectedWellKnownProtos); + assertThat(WellKnownProto.getByPathName(Int64Value.getDescriptor().getFile().getName())) + .containsExactlyElementsIn(expectedWellKnownProtos); + assertThat(WellKnownProto.getByPathName(UInt32Value.getDescriptor().getFile().getName())) + .containsExactlyElementsIn(expectedWellKnownProtos); + assertThat(WellKnownProto.getByPathName(UInt64Value.getDescriptor().getFile().getName())) + .containsExactlyElementsIn(expectedWellKnownProtos); + assertThat(WellKnownProto.getByPathName(BoolValue.getDescriptor().getFile().getName())) + .containsExactlyElementsIn(expectedWellKnownProtos); + assertThat(WellKnownProto.getByPathName(StringValue.getDescriptor().getFile().getName())) + .containsExactlyElementsIn(expectedWellKnownProtos); + assertThat(WellKnownProto.getByPathName(BytesValue.getDescriptor().getFile().getName())) + .containsExactlyElementsIn(expectedWellKnownProtos); + } +} 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 490819e77..f8b2b988b 100644 --- a/common/src/test/java/dev/cel/common/navigation/BUILD.bazel +++ b/common/src/test/java/dev/cel/common/navigation/BUILD.bazel @@ -1,6 +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", @@ -8,17 +11,22 @@ java_library( srcs = glob(["*.java"]), deps = [ "//:java_truth", - "//common", + "//common:cel_ast", "//common:compiler_common", + "//common:container", + "//common:mutable_ast", + "//common:operator", "//common:options", "//common/ast", + "//common/ast:mutable_expr", "//common/navigation", - "//common/resources/testdata/proto3:test_all_types_java_proto", + "//common/navigation:common", + "//common/navigation:mutable_navigation", "//common/types", "//compiler", "//compiler:compiler_builder", "//parser:macro", - "//parser:operator", + "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", "@maven//:com_google_guava_guava", "@maven//:com_google_testparameterinjector_test_parameter_injector", "@maven//:junit_junit", diff --git a/common/src/test/java/dev/cel/common/navigation/CelNavigableAstTest.java b/common/src/test/java/dev/cel/common/navigation/CelNavigableAstTest.java index 40f772810..6fa4cd203 100644 --- a/common/src/test/java/dev/cel/common/navigation/CelNavigableAstTest.java +++ b/common/src/test/java/dev/cel/common/navigation/CelNavigableAstTest.java @@ -37,7 +37,7 @@ public void construct_success() throws Exception { assertThat(navigableAst.getAst()).isEqualTo(ast); assertThat(navigableAst.getRoot().expr()) - .isEqualTo(CelExpr.ofConstantExpr(1, CelConstant.ofValue("Hello World"))); + .isEqualTo(CelExpr.ofConstant(1, CelConstant.ofValue("Hello World"))); assertThat(navigableAst.getRoot().parent()).isEmpty(); assertThat(navigableAst.getRoot().depth()).isEqualTo(0); } diff --git a/common/src/test/java/dev/cel/common/navigation/CelNavigableExprTest.java b/common/src/test/java/dev/cel/common/navigation/CelNavigableExprTest.java index 261524df0..69b244708 100644 --- a/common/src/test/java/dev/cel/common/navigation/CelNavigableExprTest.java +++ b/common/src/test/java/dev/cel/common/navigation/CelNavigableExprTest.java @@ -27,7 +27,7 @@ public class CelNavigableExprTest { @Test public void construct_withoutParent_success() { - CelExpr constExpr = CelExpr.ofConstantExpr(1, CelConstant.ofValue("test")); + CelExpr constExpr = CelExpr.ofConstant(1, CelConstant.ofValue("test")); CelNavigableExpr navigableExpr = CelNavigableExpr.builder().setExpr(constExpr).setDepth(2).build(); @@ -38,8 +38,8 @@ public void construct_withoutParent_success() { @Test public void construct_withParent_success() { - CelExpr constExpr = CelExpr.ofConstantExpr(1, CelConstant.ofValue("test")); - CelExpr identExpr = CelExpr.ofIdentExpr(2, "a"); + CelExpr constExpr = CelExpr.ofConstant(1, CelConstant.ofValue("test")); + CelExpr identExpr = CelExpr.ofIdent(2, "a"); CelNavigableExpr parentExpr = CelNavigableExpr.builder().setExpr(identExpr).setDepth(1).build(); CelNavigableExpr navigableExpr = CelNavigableExpr.builder().setExpr(constExpr).setDepth(2).setParent(parentExpr).build(); diff --git a/common/src/test/java/dev/cel/common/navigation/CelNavigableExprVisitorTest.java b/common/src/test/java/dev/cel/common/navigation/CelNavigableExprVisitorTest.java index 710267e55..15c6ac620 100644 --- a/common/src/test/java/dev/cel/common/navigation/CelNavigableExprVisitorTest.java +++ b/common/src/test/java/dev/cel/common/navigation/CelNavigableExprVisitorTest.java @@ -26,20 +26,20 @@ import com.google.testing.junit.testparameterinjector.TestParameterInjector; import com.google.testing.junit.testparameterinjector.TestParameters; import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelContainer; import dev.cel.common.CelOptions; +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.ast.CelExpr.ExprKind.Kind; -import dev.cel.common.navigation.CelNavigableExpr.TraversalOrder; import dev.cel.common.types.ListType; 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.CelStandardMacro; -import dev.cel.parser.Operator; -import dev.cel.testing.testdata.proto3.TestAllTypesProto.TestAllTypes; import java.util.Optional; import org.junit.Test; import org.junit.runner.RunWith; @@ -86,26 +86,26 @@ public void add_allNodes_allNodesReturned() throws Exception { navigableAst.getRoot().allNodes().map(CelNavigableExpr::expr).collect(toImmutableList()); CelExpr childAddCall = - CelExpr.ofCallExpr( + CelExpr.ofCall( 2, Optional.empty(), Operator.ADD.getFunction(), ImmutableList.of( - CelExpr.ofConstantExpr(1, CelConstant.ofValue(1)), // 1 + a - CelExpr.ofIdentExpr(3, "a"))); + CelExpr.ofConstant(1, CelConstant.ofValue(1)), // 1 + a + CelExpr.ofIdent(3, "a"))); CelExpr rootAddCall = - CelExpr.ofCallExpr( + CelExpr.ofCall( 4, Optional.empty(), Operator.ADD.getFunction(), - ImmutableList.of(childAddCall, CelExpr.ofConstantExpr(5, CelConstant.ofValue(2)))); + ImmutableList.of(childAddCall, CelExpr.ofConstant(5, CelConstant.ofValue(2)))); assertThat(allNodes) .containsExactly( rootAddCall, childAddCall, - CelExpr.ofConstantExpr(1, CelConstant.ofValue(1)), - CelExpr.ofIdentExpr(3, "a"), - CelExpr.ofConstantExpr(5, CelConstant.ofValue(2))); + CelExpr.ofConstant(1, CelConstant.ofValue(1)), + CelExpr.ofIdent(3, "a"), + CelExpr.ofConstant(5, CelConstant.ofValue(2))); } @Test @@ -128,6 +128,26 @@ public void add_preOrder_heightSet() throws Exception { assertThat(allNodeHeights).containsExactly(2, 1, 0, 0, 0).inOrder(); // +, +, 1, a, 2 } + @Test + public void add_preOrder_maxIdsSet() throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder().addVar("a", SimpleType.INT).build(); + // Tree shape (brackets are IDs): + // + [4] + // + [2] 2 [5] + // 1 [1] a [3] + CelAbstractSyntaxTree ast = compiler.compile("1 + a + 2").getAst(); + CelNavigableAst navigableAst = CelNavigableAst.fromAst(ast); + + ImmutableList allNodeMaxIds = + navigableAst + .getRoot() + .allNodes(TraversalOrder.PRE_ORDER) + .map(CelNavigableExpr::maxId) + .collect(toImmutableList()); + assertThat(allNodeMaxIds).containsExactly(5L, 3L, 1L, 3L, 5L).inOrder(); // +, +, 1, a, 2 + } + @Test public void add_postOrder_heightSet() throws Exception { CelCompiler compiler = @@ -148,6 +168,26 @@ public void add_postOrder_heightSet() throws Exception { assertThat(allNodeHeights).containsExactly(0, 0, 1, 0, 2).inOrder(); // 1, a, +, 2, + } + @Test + public void add_postOrder_maxIdsSet() throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder().addVar("a", SimpleType.INT).build(); + // Tree shape (brackets are IDs): + // + [4] + // + [2] 2 [5] + // 1 [1] a [3] + CelAbstractSyntaxTree ast = compiler.compile("1 + a + 2").getAst(); + CelNavigableAst navigableAst = CelNavigableAst.fromAst(ast); + + ImmutableList allNodeMaxIds = + navigableAst + .getRoot() + .allNodes(TraversalOrder.POST_ORDER) + .map(CelNavigableExpr::maxId) + .collect(toImmutableList()); + assertThat(allNodeMaxIds).containsExactly(1L, 3L, 3L, 5L, 5L).inOrder(); // 1, a, +, 2, + + } + @Test public void add_fromLeaf_heightSetForParents() throws Exception { CelCompiler compiler = @@ -177,6 +217,35 @@ public void add_fromLeaf_heightSetForParents() throws Exception { assertThat(heights.build()).containsExactly(3, 2, 1, 0); } + @Test + public void add_fromLeaf_maxIdsSetForParents() throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder().addVar("a", SimpleType.INT).build(); + // Tree shape: + // + + // + 3 + // + 2 + // a 1 + CelAbstractSyntaxTree ast = compiler.compile("1 + a + 2 + 3").getAst(); + CelNavigableAst navigableAst = CelNavigableAst.fromAst(ast); + + ImmutableList.Builder heights = ImmutableList.builder(); + CelNavigableExpr navigableExpr = + navigableAst + .getRoot() + .allNodes() + .filter(node -> node.expr().identOrDefault().name().equals("a")) + .findAny() + .get(); + heights.add(navigableExpr.maxId()); + while (navigableExpr.parent().isPresent()) { + navigableExpr = navigableExpr.parent().get(); + heights.add(navigableExpr.maxId()); + } + + assertThat(heights.build()).containsExactly(3L, 3L, 5L, 7L).inOrder(); + } + @Test public void add_children_heightSet(@TestParameter TraversalOrder traversalOrder) throws Exception { @@ -199,6 +268,30 @@ public void add_children_heightSet(@TestParameter TraversalOrder traversalOrder) assertThat(allNodeHeights).containsExactly(2, 0).inOrder(); // + (2), 2 (0) regardless of order } + @Test + public void add_children_maxIdsSet(@TestParameter TraversalOrder traversalOrder) + throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder().addVar("a", SimpleType.INT).build(); + // Tree shape: + // + + // + 3 + // + 2 + // a 1 + CelAbstractSyntaxTree ast = compiler.compile("1 + a + 2 + 3").getAst(); + CelNavigableAst navigableAst = CelNavigableAst.fromAst(ast); + + ImmutableList allNodeHeights = + navigableAst + .getRoot() + .children(traversalOrder) + .map(CelNavigableExpr::maxId) + .collect(toImmutableList()); + assertThat(allNodeHeights) + .containsExactly(5L, 7L) + .inOrder(); // + (5), 3 (7) regardless of order + } + @Test public void add_filterConstants_allNodesReturned() throws Exception { CelCompiler compiler = @@ -218,10 +311,8 @@ public void add_filterConstants_allNodesReturned() throws Exception { .collect(toImmutableList()); assertThat(allConstants).hasSize(2); - assertThat(allConstants.get(0).expr()) - .isEqualTo(CelExpr.ofConstantExpr(1, CelConstant.ofValue(1))); - assertThat(allConstants.get(1).expr()) - .isEqualTo(CelExpr.ofConstantExpr(5, CelConstant.ofValue(2))); + assertThat(allConstants.get(0).expr()).isEqualTo(CelExpr.ofConstant(1, CelConstant.ofValue(1))); + assertThat(allConstants.get(1).expr()).isEqualTo(CelExpr.ofConstant(5, CelConstant.ofValue(2))); } @Test @@ -247,20 +338,19 @@ public void add_filterConstants_parentsPopulated() throws Exception { CelExpr rootAddCall = allConstants.get(1).parent().get().expr(); // childAddCall + 2 assertThat(childAddCall) .isEqualTo( - CelExpr.ofCallExpr( + CelExpr.ofCall( 2, Optional.empty(), Operator.ADD.getFunction(), ImmutableList.of( - CelExpr.ofConstantExpr(1, CelConstant.ofValue(1)), - CelExpr.ofIdentExpr(3, "a")))); + CelExpr.ofConstant(1, CelConstant.ofValue(1)), CelExpr.ofIdent(3, "a")))); assertThat(rootAddCall) .isEqualTo( - CelExpr.ofCallExpr( + CelExpr.ofCall( 4, Optional.empty(), Operator.ADD.getFunction(), - ImmutableList.of(childAddCall, CelExpr.ofConstantExpr(5, CelConstant.ofValue(2))))); + ImmutableList.of(childAddCall, CelExpr.ofConstant(5, CelConstant.ofValue(2))))); } @Test @@ -282,8 +372,7 @@ public void add_filterConstants_singleChildReturned() throws Exception { .collect(toImmutableList()); assertThat(allConstants).hasSize(1); - assertThat(allConstants.get(0).expr()) - .isEqualTo(CelExpr.ofConstantExpr(5, CelConstant.ofValue(2))); + assertThat(allConstants.get(0).expr()).isEqualTo(CelExpr.ofConstant(5, CelConstant.ofValue(2))); } @Test @@ -335,8 +424,8 @@ public void add_childrenOfMiddleBranch_success() throws Exception { // Assert that the children of add call in the middle branch are const(1) and ident("a") assertThat(children).hasSize(2); - assertThat(children.get(0).expr()).isEqualTo(CelExpr.ofConstantExpr(1, CelConstant.ofValue(1))); - assertThat(children.get(1)).isEqualTo(ident); + assertThat(children.get(0).expr()).isEqualTo(CelExpr.ofConstant(1, CelConstant.ofValue(1))); + assertThat(children.get(1).expr()).isEqualTo(ident.expr()); } @Test @@ -358,12 +447,12 @@ public void stringFormatCall_filterList_success() throws Exception { navigableAst .getRoot() .allNodes() - .filter(x -> x.getKind().equals(Kind.CREATE_LIST)) + .filter(x -> x.getKind().equals(Kind.LIST)) .collect(toImmutableList()); assertThat(allConstants).hasSize(1); CelNavigableExpr listExpr = allConstants.get(0); - assertThat(listExpr.getKind()).isEqualTo(Kind.CREATE_LIST); + assertThat(listExpr.getKind()).isEqualTo(Kind.LIST); assertThat(listExpr.parent()).isPresent(); CelNavigableExpr stringFormatExpr = listExpr.parent().get(); assertThat(stringFormatExpr.getKind()).isEqualTo(Kind.CALL); @@ -406,9 +495,9 @@ public void message_allNodesReturned() throws Exception { ImmutableList allNodes = navigableAst.getRoot().allNodes().map(CelNavigableExpr::expr).collect(toImmutableList()); - CelExpr operand = CelExpr.ofIdentExpr(1, "msg"); + CelExpr operand = CelExpr.ofIdent(1, "msg"); assertThat(allNodes) - .containsExactly(operand, CelExpr.ofSelectExpr(2, operand, "single_int64", false)); + .containsExactly(operand, CelExpr.ofSelect(2, operand, "single_int64", false)); } @Test @@ -430,9 +519,9 @@ public void nestedMessage_filterSelect_allNodesReturned() throws Exception { .collect(toImmutableList()); CelExpr innerSelect = - CelExpr.ofSelectExpr( - 2, CelExpr.ofIdentExpr(1, "msg"), "standalone_message", false); // msg.standalone - CelExpr outerSelect = CelExpr.ofSelectExpr(3, innerSelect, "bb", false); // innerSelect.bb + CelExpr.ofSelect( + 2, CelExpr.ofIdent(1, "msg"), "standalone_message", false); // msg.standalone + CelExpr outerSelect = CelExpr.ofSelect(3, innerSelect, "bb", false); // innerSelect.bb assertThat(allSelects).containsExactly(innerSelect, outerSelect); } @@ -460,9 +549,9 @@ public void nestedMessage_filterSelect_singleChildReturned() throws Exception { assertThat(allSelects).hasSize(1); CelExpr innerSelect = - CelExpr.ofSelectExpr( - 4, CelExpr.ofIdentExpr(3, "msg"), "standalone_message", false); // msg.standalone - CelExpr outerSelect = CelExpr.ofSelectExpr(5, innerSelect, "bb", false); // innerSelect.bb + CelExpr.ofSelect( + 4, CelExpr.ofIdent(3, "msg"), "standalone_message", false); // msg.standalone + CelExpr outerSelect = CelExpr.ofSelect(5, innerSelect, "bb", false); // innerSelect.bb assertThat(allSelects.get(0).expr()).isEqualTo(outerSelect); } @@ -483,8 +572,8 @@ public void presenceTest_allNodesReturned() throws Exception { assertThat(allNodes).hasSize(2); assertThat(allNodes) .containsExactly( - CelExpr.ofIdentExpr(2, "msg"), - CelExpr.ofSelectExpr(4, CelExpr.ofIdentExpr(2, "msg"), "standalone_message", true)); + CelExpr.ofIdent(2, "msg"), + CelExpr.ofSelect(4, CelExpr.ofIdent(2, "msg"), "standalone_message", true)); } @Test @@ -492,7 +581,7 @@ public void messageConstruction_allNodesReturned() throws Exception { CelCompiler compiler = CelCompilerFactory.standardCelCompilerBuilder() .addMessageTypes(TestAllTypes.getDescriptor()) - .setContainer("dev.cel.testing.testdata.proto3") + .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) .build(); CelAbstractSyntaxTree ast = compiler.compile("TestAllTypes{single_int64: 1}").getAst(); CelNavigableAst navigableAst = CelNavigableAst.fromAst(ast); @@ -500,23 +589,22 @@ public void messageConstruction_allNodesReturned() throws Exception { ImmutableList allNodes = navigableAst.getRoot().allNodes().map(CelNavigableExpr::expr).collect(toImmutableList()); - CelExpr constExpr = CelExpr.ofConstantExpr(3, CelConstant.ofValue(1)); + CelExpr constExpr = CelExpr.ofConstant(3, CelConstant.ofValue(1)); assertThat(allNodes) .containsExactly( constExpr, - CelExpr.ofCreateStructExpr( + CelExpr.ofStruct( 1, - "TestAllTypes", - ImmutableList.of( - CelExpr.ofCreateStructEntryExpr(2, "single_int64", constExpr, false)))); + "cel.expr.conformance.proto3.TestAllTypes", + ImmutableList.of(CelExpr.ofStructEntry(2, "single_int64", constExpr, false)))); } @Test - public void messageConstruction_filterCreateStruct_allNodesReturned() throws Exception { + public void messageConstruction_filterStruct_allNodesReturned() throws Exception { CelCompiler compiler = CelCompilerFactory.standardCelCompilerBuilder() .addMessageTypes(TestAllTypes.getDescriptor()) - .setContainer("dev.cel.testing.testdata.proto3") + .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) .build(); CelAbstractSyntaxTree ast = compiler.compile("TestAllTypes{single_int64: 1}").getAst(); CelNavigableAst navigableAst = CelNavigableAst.fromAst(ast); @@ -525,21 +613,18 @@ public void messageConstruction_filterCreateStruct_allNodesReturned() throws Exc navigableAst .getRoot() .allNodes() - .filter(x -> x.getKind().equals(Kind.CREATE_STRUCT)) + .filter(x -> x.getKind().equals(Kind.STRUCT)) .collect(toImmutableList()); assertThat(allNodes).hasSize(1); assertThat(allNodes.get(0).expr()) .isEqualTo( - CelExpr.ofCreateStructExpr( + CelExpr.ofStruct( 1, - "TestAllTypes", + "cel.expr.conformance.proto3.TestAllTypes", ImmutableList.of( - CelExpr.ofCreateStructEntryExpr( - 2, - "single_int64", - CelExpr.ofConstantExpr(3, CelConstant.ofValue(1)), - false)))); + CelExpr.ofStructEntry( + 2, "single_int64", CelExpr.ofConstant(3, CelConstant.ofValue(1)), false)))); } @Test @@ -547,7 +632,7 @@ public void messageConstruction_preOrder_heightSet() throws Exception { CelCompiler compiler = CelCompilerFactory.standardCelCompilerBuilder() .addMessageTypes(TestAllTypes.getDescriptor()) - .setContainer("dev.cel.testing.testdata.proto3") + .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) .build(); CelAbstractSyntaxTree ast = compiler.compile("TestAllTypes{single_int64: 1}").getAst(); CelNavigableAst navigableAst = CelNavigableAst.fromAst(ast); @@ -567,7 +652,7 @@ public void messageConstruction_postOrder_heightSet() throws Exception { CelCompiler compiler = CelCompilerFactory.standardCelCompilerBuilder() .addMessageTypes(TestAllTypes.getDescriptor()) - .setContainer("dev.cel.testing.testdata.proto3") + .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) .build(); CelAbstractSyntaxTree ast = compiler.compile("TestAllTypes{single_int64: 1}").getAst(); CelNavigableAst navigableAst = CelNavigableAst.fromAst(ast); @@ -582,6 +667,27 @@ public void messageConstruction_postOrder_heightSet() throws Exception { assertThat(allNodes).containsExactly(0, 1).inOrder(); } + @Test + public void messageConstruction_maxIdsSet(@TestParameter TraversalOrder traversalOrder) + throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addMessageTypes(TestAllTypes.getDescriptor()) + .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) + .build(); + CelAbstractSyntaxTree ast = compiler.compile("TestAllTypes{single_int64: 1}").getAst(); + CelNavigableAst navigableAst = CelNavigableAst.fromAst(ast); + + ImmutableList allNodes = + navigableAst + .getRoot() + .allNodes(traversalOrder) + .map(CelNavigableExpr::maxId) + .collect(toImmutableList()); + + assertThat(allNodes).containsExactly(3L, 3L).inOrder(); + } + @Test public void mapConstruction_allNodesReturned() throws Exception { CelCompiler compiler = CelCompilerFactory.standardCelCompilerBuilder().build(); @@ -592,20 +698,18 @@ public void mapConstruction_allNodesReturned() throws Exception { navigableAst.getRoot().allNodes().map(CelNavigableExpr::expr).collect(toImmutableList()); assertThat(allNodes).hasSize(3); - CelExpr mapKeyExpr = CelExpr.ofConstantExpr(3, CelConstant.ofValue("key")); - CelExpr mapValueExpr = CelExpr.ofConstantExpr(4, CelConstant.ofValue(2)); + CelExpr mapKeyExpr = CelExpr.ofConstant(3, CelConstant.ofValue("key")); + CelExpr mapValueExpr = CelExpr.ofConstant(4, CelConstant.ofValue(2)); assertThat(allNodes) .containsExactly( mapKeyExpr, mapValueExpr, - CelExpr.ofCreateMapExpr( - 1, - ImmutableList.of( - CelExpr.ofCreateMapEntryExpr(2, mapKeyExpr, mapValueExpr, false)))); + CelExpr.ofMap( + 1, ImmutableList.of(CelExpr.ofMapEntry(2, mapKeyExpr, mapValueExpr, false)))); } @Test - public void mapConstruction_filterCreateMap_allNodesReturned() throws Exception { + public void mapConstruction_filterMap_allNodesReturned() throws Exception { CelCompiler compiler = CelCompilerFactory.standardCelCompilerBuilder().build(); CelAbstractSyntaxTree ast = compiler.compile("{'key': 2}").getAst(); CelNavigableAst navigableAst = CelNavigableAst.fromAst(ast); @@ -614,18 +718,16 @@ public void mapConstruction_filterCreateMap_allNodesReturned() throws Exception navigableAst .getRoot() .allNodes() - .filter(x -> x.getKind().equals(Kind.CREATE_MAP)) + .filter(x -> x.getKind().equals(Kind.MAP)) .collect(toImmutableList()); assertThat(allNodes).hasSize(1); - CelExpr mapKeyExpr = CelExpr.ofConstantExpr(3, CelConstant.ofValue("key")); - CelExpr mapValueExpr = CelExpr.ofConstantExpr(4, CelConstant.ofValue(2)); + CelExpr mapKeyExpr = CelExpr.ofConstant(3, CelConstant.ofValue("key")); + CelExpr mapValueExpr = CelExpr.ofConstant(4, CelConstant.ofValue(2)); assertThat(allNodes.get(0).expr()) .isEqualTo( - CelExpr.ofCreateMapExpr( - 1, - ImmutableList.of( - CelExpr.ofCreateMapEntryExpr(2, mapKeyExpr, mapValueExpr, false)))); + CelExpr.ofMap( + 1, ImmutableList.of(CelExpr.ofMapEntry(2, mapKeyExpr, mapValueExpr, false)))); } @Test @@ -660,6 +762,38 @@ public void mapConstruction_postOrder_heightSet() throws Exception { assertThat(allNodes).containsExactly(0, 0, 1).inOrder(); } + @Test + public void mapConstruction_preOrder_maxIdsSet() throws Exception { + CelCompiler compiler = CelCompilerFactory.standardCelCompilerBuilder().build(); + CelAbstractSyntaxTree ast = compiler.compile("{'key': 2}").getAst(); + CelNavigableAst navigableAst = CelNavigableAst.fromAst(ast); + + ImmutableList allNodes = + navigableAst + .getRoot() + .allNodes(TraversalOrder.PRE_ORDER) + .map(CelNavigableExpr::maxId) + .collect(toImmutableList()); + + assertThat(allNodes).containsExactly(4L, 3L, 4L).inOrder(); + } + + @Test + public void mapConstruction_postOrder_maxIdsSet() throws Exception { + CelCompiler compiler = CelCompilerFactory.standardCelCompilerBuilder().build(); + CelAbstractSyntaxTree ast = compiler.compile("{'key': 2}").getAst(); + CelNavigableAst navigableAst = CelNavigableAst.fromAst(ast); + + ImmutableList allNodes = + navigableAst + .getRoot() + .allNodes(TraversalOrder.POST_ORDER) + .map(CelNavigableExpr::maxId) + .collect(toImmutableList()); + + assertThat(allNodes).containsExactly(3L, 4L, 4L).inOrder(); + } + @Test public void emptyMapConstruction_allNodesReturned() throws Exception { CelCompiler compiler = CelCompilerFactory.standardCelCompilerBuilder().build(); @@ -670,13 +804,14 @@ public void emptyMapConstruction_allNodesReturned() throws Exception { navigableAst.getRoot().allNodes().collect(toImmutableList()); assertThat(allNodes).hasSize(1); - assertThat(allNodes.get(0).expr()).isEqualTo(CelExpr.ofCreateMapExpr(1, ImmutableList.of())); + assertThat(allNodes.get(0).expr()).isEqualTo(CelExpr.ofMap(1, ImmutableList.of())); } @Test public void comprehension_preOrder_allNodesReturned() throws Exception { CelCompiler compiler = CelCompilerFactory.standardCelCompilerBuilder() + .setOptions(CelOptions.current().enableHiddenAccumulatorVar(true).build()) .setStandardMacros(CelStandardMacro.EXISTS) .build(); CelAbstractSyntaxTree ast = compiler.compile("[true].exists(i, i)").getAst(); @@ -689,35 +824,34 @@ public void comprehension_preOrder_allNodesReturned() throws Exception { .map(CelNavigableExpr::expr) .collect(toImmutableList()); - CelExpr iterRangeConstExpr = CelExpr.ofConstantExpr(2, CelConstant.ofValue(true)); - CelExpr iterRange = - CelExpr.ofCreateListExpr(1, ImmutableList.of(iterRangeConstExpr), ImmutableList.of()); - CelExpr accuInit = CelExpr.ofConstantExpr(6, CelConstant.ofValue(false)); - CelExpr loopConditionIdentExpr = CelExpr.ofIdentExpr(7, "__result__"); + CelExpr iterRangeConstExpr = CelExpr.ofConstant(2, CelConstant.ofValue(true)); + CelExpr iterRange = CelExpr.ofList(1, ImmutableList.of(iterRangeConstExpr), ImmutableList.of()); + CelExpr accuInit = CelExpr.ofConstant(6, CelConstant.ofValue(false)); + CelExpr loopConditionIdentExpr = CelExpr.ofIdent(7, "@result"); CelExpr loopConditionCallExpr = - CelExpr.ofCallExpr( + CelExpr.ofCall( 8, Optional.empty(), Operator.LOGICAL_NOT.getFunction(), ImmutableList.of(loopConditionIdentExpr)); CelExpr loopCondition = - CelExpr.ofCallExpr( + CelExpr.ofCall( 9, Optional.empty(), Operator.NOT_STRICTLY_FALSE.getFunction(), ImmutableList.of(loopConditionCallExpr)); - CelExpr loopStepResultExpr = CelExpr.ofIdentExpr(10, "__result__"); - CelExpr loopStepVarExpr = CelExpr.ofIdentExpr(5, "i"); + CelExpr loopStepResultExpr = CelExpr.ofIdent(10, "@result"); + CelExpr loopStepVarExpr = CelExpr.ofIdent(5, "i"); CelExpr loopStep = - CelExpr.ofCallExpr( + CelExpr.ofCall( 11, Optional.empty(), Operator.LOGICAL_OR.getFunction(), ImmutableList.of(loopStepResultExpr, loopStepVarExpr)); - CelExpr result = CelExpr.ofIdentExpr(12, "__result__"); + CelExpr result = CelExpr.ofIdent(12, "@result"); CelExpr comprehension = CelExpr.ofComprehension( - 13, "i", iterRange, "__result__", accuInit, loopCondition, loopStep, result); + 13, "i", iterRange, "@result", accuInit, loopCondition, loopStep, result); assertThat(allNodes).hasSize(11); assertThat(allNodes) .containsExactly( @@ -739,6 +873,7 @@ public void comprehension_preOrder_allNodesReturned() throws Exception { public void comprehension_postOrder_allNodesReturned() throws Exception { CelCompiler compiler = CelCompilerFactory.standardCelCompilerBuilder() + .setOptions(CelOptions.current().enableHiddenAccumulatorVar(true).build()) .setStandardMacros(CelStandardMacro.EXISTS) .build(); CelAbstractSyntaxTree ast = compiler.compile("[true].exists(i, i)").getAst(); @@ -751,35 +886,34 @@ public void comprehension_postOrder_allNodesReturned() throws Exception { .map(CelNavigableExpr::expr) .collect(toImmutableList()); - CelExpr iterRangeConstExpr = CelExpr.ofConstantExpr(2, CelConstant.ofValue(true)); - CelExpr iterRange = - CelExpr.ofCreateListExpr(1, ImmutableList.of(iterRangeConstExpr), ImmutableList.of()); - CelExpr accuInit = CelExpr.ofConstantExpr(6, CelConstant.ofValue(false)); - CelExpr loopConditionIdentExpr = CelExpr.ofIdentExpr(7, "__result__"); + CelExpr iterRangeConstExpr = CelExpr.ofConstant(2, CelConstant.ofValue(true)); + CelExpr iterRange = CelExpr.ofList(1, ImmutableList.of(iterRangeConstExpr), ImmutableList.of()); + CelExpr accuInit = CelExpr.ofConstant(6, CelConstant.ofValue(false)); + CelExpr loopConditionIdentExpr = CelExpr.ofIdent(7, "@result"); CelExpr loopConditionCallExpr = - CelExpr.ofCallExpr( + CelExpr.ofCall( 8, Optional.empty(), Operator.LOGICAL_NOT.getFunction(), ImmutableList.of(loopConditionIdentExpr)); CelExpr loopCondition = - CelExpr.ofCallExpr( + CelExpr.ofCall( 9, Optional.empty(), Operator.NOT_STRICTLY_FALSE.getFunction(), ImmutableList.of(loopConditionCallExpr)); - CelExpr loopStepResultExpr = CelExpr.ofIdentExpr(10, "__result__"); - CelExpr loopStepVarExpr = CelExpr.ofIdentExpr(5, "i"); + CelExpr loopStepResultExpr = CelExpr.ofIdent(10, "@result"); + CelExpr loopStepVarExpr = CelExpr.ofIdent(5, "i"); CelExpr loopStep = - CelExpr.ofCallExpr( + CelExpr.ofCall( 11, Optional.empty(), Operator.LOGICAL_OR.getFunction(), ImmutableList.of(loopStepResultExpr, loopStepVarExpr)); - CelExpr result = CelExpr.ofIdentExpr(12, "__result__"); + CelExpr result = CelExpr.ofIdent(12, "@result"); CelExpr comprehension = CelExpr.ofComprehension( - 13, "i", iterRange, "__result__", accuInit, loopCondition, loopStep, result); + 13, "i", iterRange, "@result", accuInit, loopCondition, loopStep, result); assertThat(allNodes).hasSize(11); assertThat(allNodes) .containsExactly( @@ -835,10 +969,49 @@ public void comprehension_postOrder_heightSet() throws Exception { assertThat(allNodes).containsExactly(0, 1, 0, 0, 1, 2, 0, 0, 1, 0, 3).inOrder(); } + @Test + public void comprehension_preOrder_maxIdsSet() throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder() + .setStandardMacros(CelStandardMacro.EXISTS) + .build(); + CelAbstractSyntaxTree ast = compiler.compile("[true].exists(i, i)").getAst(); + CelNavigableAst navigableAst = CelNavigableAst.fromAst(ast); + + ImmutableList allNodes = + navigableAst + .getRoot() + .allNodes(TraversalOrder.PRE_ORDER) + .map(CelNavigableExpr::maxId) + .collect(toImmutableList()); + + assertThat(allNodes).containsExactly(13L, 2L, 2L, 6L, 9L, 8L, 7L, 11L, 10L, 5L, 12L).inOrder(); + } + + @Test + public void comprehension_postOrder_maxIdsSet() throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder() + .setStandardMacros(CelStandardMacro.EXISTS) + .build(); + CelAbstractSyntaxTree ast = compiler.compile("[true].exists(i, i)").getAst(); + CelNavigableAst navigableAst = CelNavigableAst.fromAst(ast); + + ImmutableList allNodes = + navigableAst + .getRoot() + .allNodes(TraversalOrder.POST_ORDER) + .map(CelNavigableExpr::maxId) + .collect(toImmutableList()); + + assertThat(allNodes).containsExactly(2L, 2L, 6L, 7L, 8L, 9L, 10L, 5L, 11L, 12L, 13L).inOrder(); + } + @Test public void comprehension_allNodes_parentsPopulated() throws Exception { CelCompiler compiler = CelCompilerFactory.standardCelCompilerBuilder() + .setOptions(CelOptions.current().enableHiddenAccumulatorVar(true).build()) .setStandardMacros(CelStandardMacro.EXISTS) .build(); CelAbstractSyntaxTree ast = compiler.compile("[true].exists(i, i)").getAst(); @@ -846,35 +1019,34 @@ public void comprehension_allNodes_parentsPopulated() throws Exception { ImmutableList allNodes = navigableAst.getRoot().allNodes(TraversalOrder.PRE_ORDER).collect(toImmutableList()); - CelExpr iterRangeConstExpr = CelExpr.ofConstantExpr(2, CelConstant.ofValue(true)); - CelExpr iterRange = - CelExpr.ofCreateListExpr(1, ImmutableList.of(iterRangeConstExpr), ImmutableList.of()); - CelExpr accuInit = CelExpr.ofConstantExpr(6, CelConstant.ofValue(false)); - CelExpr loopConditionIdentExpr = CelExpr.ofIdentExpr(7, "__result__"); + CelExpr iterRangeConstExpr = CelExpr.ofConstant(2, CelConstant.ofValue(true)); + CelExpr iterRange = CelExpr.ofList(1, ImmutableList.of(iterRangeConstExpr), ImmutableList.of()); + CelExpr accuInit = CelExpr.ofConstant(6, CelConstant.ofValue(false)); + CelExpr loopConditionIdentExpr = CelExpr.ofIdent(7, "@result"); CelExpr loopConditionCallExpr = - CelExpr.ofCallExpr( + CelExpr.ofCall( 8, Optional.empty(), Operator.LOGICAL_NOT.getFunction(), ImmutableList.of(loopConditionIdentExpr)); CelExpr loopCondition = - CelExpr.ofCallExpr( + CelExpr.ofCall( 9, Optional.empty(), Operator.NOT_STRICTLY_FALSE.getFunction(), ImmutableList.of(loopConditionCallExpr)); - CelExpr loopStepResultExpr = CelExpr.ofIdentExpr(10, "__result__"); - CelExpr loopStepVarExpr = CelExpr.ofIdentExpr(5, "i"); + CelExpr loopStepResultExpr = CelExpr.ofIdent(10, "@result"); + CelExpr loopStepVarExpr = CelExpr.ofIdent(5, "i"); CelExpr loopStep = - CelExpr.ofCallExpr( + CelExpr.ofCall( 11, Optional.empty(), Operator.LOGICAL_OR.getFunction(), ImmutableList.of(loopStepResultExpr, loopStepVarExpr)); - CelExpr result = CelExpr.ofIdentExpr(12, "__result__"); + CelExpr result = CelExpr.ofIdent(12, "@result"); CelExpr comprehension = CelExpr.ofComprehension( - 13, "i", iterRange, "__result__", accuInit, loopCondition, loopStep, result); + 13, "i", iterRange, "@result", accuInit, loopCondition, loopStep, result); assertThat(allNodes).hasSize(11); assertThat(allNodes.get(0).parent()).isEmpty(); // comprehension assertThat(allNodes.get(1).parent().get().expr()).isEqualTo(comprehension); // iter_range @@ -898,6 +1070,7 @@ public void comprehension_allNodes_parentsPopulated() throws Exception { public void comprehension_filterComprehension_allNodesReturned() throws Exception { CelCompiler compiler = CelCompilerFactory.standardCelCompilerBuilder() + .setOptions(CelOptions.current().enableHiddenAccumulatorVar(true).build()) .setStandardMacros(CelStandardMacro.EXISTS) .build(); CelAbstractSyntaxTree ast = compiler.compile("[true].exists(i, i)").getAst(); @@ -910,35 +1083,34 @@ public void comprehension_filterComprehension_allNodesReturned() throws Exceptio .filter(x -> x.getKind().equals(Kind.COMPREHENSION)) .collect(toImmutableList()); - CelExpr iterRangeConstExpr = CelExpr.ofConstantExpr(2, CelConstant.ofValue(true)); - CelExpr iterRange = - CelExpr.ofCreateListExpr(1, ImmutableList.of(iterRangeConstExpr), ImmutableList.of()); - CelExpr accuInit = CelExpr.ofConstantExpr(6, CelConstant.ofValue(false)); - CelExpr loopConditionIdentExpr = CelExpr.ofIdentExpr(7, "__result__"); + CelExpr iterRangeConstExpr = CelExpr.ofConstant(2, CelConstant.ofValue(true)); + CelExpr iterRange = CelExpr.ofList(1, ImmutableList.of(iterRangeConstExpr), ImmutableList.of()); + CelExpr accuInit = CelExpr.ofConstant(6, CelConstant.ofValue(false)); + CelExpr loopConditionIdentExpr = CelExpr.ofIdent(7, "@result"); CelExpr loopConditionCallExpr = - CelExpr.ofCallExpr( + CelExpr.ofCall( 8, Optional.empty(), Operator.LOGICAL_NOT.getFunction(), ImmutableList.of(loopConditionIdentExpr)); CelExpr loopCondition = - CelExpr.ofCallExpr( + CelExpr.ofCall( 9, Optional.empty(), Operator.NOT_STRICTLY_FALSE.getFunction(), ImmutableList.of(loopConditionCallExpr)); - CelExpr loopStepResultExpr = CelExpr.ofIdentExpr(10, "__result__"); - CelExpr loopStepVarExpr = CelExpr.ofIdentExpr(5, "i"); + CelExpr loopStepResultExpr = CelExpr.ofIdent(10, "@result"); + CelExpr loopStepVarExpr = CelExpr.ofIdent(5, "i"); CelExpr loopStep = - CelExpr.ofCallExpr( + CelExpr.ofCall( 11, Optional.empty(), Operator.LOGICAL_OR.getFunction(), ImmutableList.of(loopStepResultExpr, loopStepVarExpr)); - CelExpr result = CelExpr.ofIdentExpr(12, "__result__"); + CelExpr result = CelExpr.ofIdent(12, "@result"); CelExpr comprehension = CelExpr.ofComprehension( - 13, "i", iterRange, "__result__", accuInit, loopCondition, loopStep, result); + 13, "i", iterRange, "@result", accuInit, loopCondition, loopStep, result); assertThat(allNodes).hasSize(1); assertThat(allNodes.get(0).expr()).isEqualTo(comprehension); } @@ -967,12 +1139,12 @@ public void callExpr_preOrder() throws Exception { .map(CelNavigableExpr::expr) .collect(toImmutableList()); - CelExpr targetExpr = CelExpr.ofConstantExpr(1, CelConstant.ofValue("hello")); - CelExpr intArgExpr = CelExpr.ofConstantExpr(3, CelConstant.ofValue(5)); - CelExpr uintArgExpr = CelExpr.ofConstantExpr(4, CelConstant.ofValue(UnsignedLong.valueOf(6))); + CelExpr targetExpr = CelExpr.ofConstant(1, CelConstant.ofValue("hello")); + CelExpr intArgExpr = CelExpr.ofConstant(3, CelConstant.ofValue(5)); + CelExpr uintArgExpr = CelExpr.ofConstant(4, CelConstant.ofValue(UnsignedLong.valueOf(6))); assertThat(allNodes) .containsExactly( - CelExpr.ofCallExpr( + CelExpr.ofCall( 2, Optional.of(targetExpr), "test", ImmutableList.of(intArgExpr, uintArgExpr)), targetExpr, intArgExpr, @@ -1004,15 +1176,15 @@ public void callExpr_postOrder() throws Exception { .map(CelNavigableExpr::expr) .collect(toImmutableList()); - CelExpr targetExpr = CelExpr.ofConstantExpr(1, CelConstant.ofValue("hello")); - CelExpr intArgExpr = CelExpr.ofConstantExpr(3, CelConstant.ofValue(5)); - CelExpr uintArgExpr = CelExpr.ofConstantExpr(4, CelConstant.ofValue(UnsignedLong.valueOf(6))); + CelExpr targetExpr = CelExpr.ofConstant(1, CelConstant.ofValue("hello")); + CelExpr intArgExpr = CelExpr.ofConstant(3, CelConstant.ofValue(5)); + CelExpr uintArgExpr = CelExpr.ofConstant(4, CelConstant.ofValue(UnsignedLong.valueOf(6))); assertThat(allNodes) .containsExactly( targetExpr, intArgExpr, uintArgExpr, - CelExpr.ofCallExpr( + CelExpr.ofCall( 2, Optional.of(targetExpr), "test", ImmutableList.of(intArgExpr, uintArgExpr))) .inOrder(); } @@ -1074,7 +1246,67 @@ public void callExpr_postOrder_heightSet() throws Exception { } @Test - public void createList_children_heightSet(@TestParameter TraversalOrder traversalOrder) + public void callExpr_preOrder_maxIdsSet() throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addFunctionDeclarations( + newFunctionDeclaration( + "test", + newMemberOverload( + "test_overload", + SimpleType.STRING, + SimpleType.STRING, + SimpleType.INT, + SimpleType.UINT))) + .build(); + CelAbstractSyntaxTree ast = + compiler.compile("('a' + 'b' + 'c' + 'd').test((1 + 2 + 3), 6u)").getAst(); + CelNavigableAst navigableAst = CelNavigableAst.fromAst(ast); + + ImmutableList allNodes = + navigableAst + .getRoot() + .allNodes(TraversalOrder.PRE_ORDER) + .map(CelNavigableExpr::maxId) + .collect(toImmutableList()); + + assertThat(allNodes) + .containsExactly(14L, 7L, 5L, 3L, 1L, 3L, 5L, 7L, 13L, 11L, 9L, 11L, 13L, 14L) + .inOrder(); + } + + @Test + public void callExpr_postOrder_maxIdsSet() throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addFunctionDeclarations( + newFunctionDeclaration( + "test", + newMemberOverload( + "test_overload", + SimpleType.STRING, + SimpleType.STRING, + SimpleType.INT, + SimpleType.UINT))) + .build(); + CelAbstractSyntaxTree ast = + compiler.compile("('a' + 'b' + 'c' + 'd').test((1 + 2 + 3), 6u)").getAst(); + CelNavigableAst navigableAst = CelNavigableAst.fromAst(ast); + + ImmutableList allNodes = + navigableAst + .getRoot() + .allNodes(TraversalOrder.POST_ORDER) + .map(CelNavigableExpr::maxId) + .collect(toImmutableList()); + + assertThat(allNodes) + .containsExactly(1L, 3L, 3L, 5L, 5L, 7L, 7L, 9L, 11L, 11L, 13L, 13L, 14L, 14L) + .inOrder(); + } + + @Test + public void list_children_heightSet(@TestParameter TraversalOrder traversalOrder) throws Exception { CelCompiler compiler = CelCompilerFactory.standardCelCompilerBuilder().addVar("a", SimpleType.INT).build(); @@ -1091,6 +1323,24 @@ public void createList_children_heightSet(@TestParameter TraversalOrder traversa assertThat(allNodeHeights).containsExactly(0, 0, 1, 2).inOrder(); } + @Test + public void list_children_maxIdsSet(@TestParameter TraversalOrder traversalOrder) + throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder().addVar("a", SimpleType.INT).build(); + CelAbstractSyntaxTree ast = compiler.compile("[1, a, (2 + 2), (3 + 4 + 5)]").getAst(); + + CelNavigableAst navigableAst = CelNavigableAst.fromAst(ast); + + ImmutableList allNodeHeights = + navigableAst + .getRoot() + .children(traversalOrder) + .map(CelNavigableExpr::maxId) + .collect(toImmutableList()); + assertThat(allNodeHeights).containsExactly(2L, 3L, 6L, 11L).inOrder(); + } + @Test public void maxRecursionLimitReached_throws() throws Exception { StringBuilder sb = new StringBuilder(); diff --git a/common/src/test/java/dev/cel/common/navigation/CelNavigableMutableAstTest.java b/common/src/test/java/dev/cel/common/navigation/CelNavigableMutableAstTest.java new file mode 100644 index 000000000..6b78b5f23 --- /dev/null +++ b/common/src/test/java/dev/cel/common/navigation/CelNavigableMutableAstTest.java @@ -0,0 +1,44 @@ +// 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. + +package dev.cel.common.navigation; + +import static com.google.common.truth.Truth.assertThat; + +import dev.cel.common.CelMutableAst; +import dev.cel.common.ast.CelConstant; +import dev.cel.common.ast.CelMutableExpr; +import dev.cel.compiler.CelCompiler; +import dev.cel.compiler.CelCompilerFactory; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class CelNavigableMutableAstTest { + + @Test + public void construct_success() throws Exception { + CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder().build(); + CelMutableAst ast = CelMutableAst.fromCelAst(celCompiler.compile("'Hello World'").getAst()); + + CelNavigableMutableAst navigableAst = CelNavigableMutableAst.fromAst(ast); + + assertThat(navigableAst.getAst()).isEqualTo(ast); + assertThat(navigableAst.getRoot().expr()) + .isEqualTo(CelMutableExpr.ofConstant(1, CelConstant.ofValue("Hello World"))); + assertThat(navigableAst.getRoot().parent()).isEmpty(); + assertThat(navigableAst.getRoot().depth()).isEqualTo(0); + } +} diff --git a/common/src/test/java/dev/cel/common/navigation/CelNavigableMutableExprTest.java b/common/src/test/java/dev/cel/common/navigation/CelNavigableMutableExprTest.java new file mode 100644 index 000000000..6900d41aa --- /dev/null +++ b/common/src/test/java/dev/cel/common/navigation/CelNavigableMutableExprTest.java @@ -0,0 +1,150 @@ +// 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. + +package dev.cel.common.navigation; + +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableList; +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 org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class CelNavigableMutableExprTest { + + @Test + public void construct_withoutParent() { + CelMutableExpr constExpr = CelMutableExpr.ofConstant(1, CelConstant.ofValue("test")); + + CelNavigableMutableExpr navigableExpr = + CelNavigableMutableExpr.builder().setExpr(constExpr).setDepth(2).build(); + + assertThat(navigableExpr.expr()).isEqualTo(constExpr); + assertThat(navigableExpr.depth()).isEqualTo(2); + assertThat(navigableExpr.parent()).isEmpty(); + } + + @Test + public void construct_withParent() { + CelMutableExpr constExpr = CelMutableExpr.ofConstant(1, CelConstant.ofValue("test")); + CelMutableExpr identExpr = CelMutableExpr.ofIdent(2, "a"); + + CelNavigableMutableExpr parentExpr = + CelNavigableMutableExpr.builder().setExpr(identExpr).setDepth(1).build(); + CelNavigableMutableExpr navigableExpr = + CelNavigableMutableExpr.builder() + .setExpr(constExpr) + .setDepth(2) + .setParent(parentExpr) + .build(); + + assertThat(parentExpr.expr()).isEqualTo(identExpr); + assertThat(parentExpr.depth()).isEqualTo(1); + assertThat(parentExpr.parent()).isEmpty(); + assertThat(navigableExpr.expr()).isEqualTo(constExpr); + assertThat(navigableExpr.depth()).isEqualTo(2); + assertThat(navigableExpr.parent()).hasValue(parentExpr); + } + + @Test + public void builderFromInstance_sameAsStaticBuilder() { + CelNavigableMutableExpr.Builder staticBuilder = + CelNavigableMutableExpr.builder().setExpr(CelMutableExpr.ofNotSet()); + + CelNavigableMutableExpr.Builder builderFromInstance = + CelNavigableMutableExpr.fromExpr(CelMutableExpr.ofNotSet()) + .builderFromInstance() + .setExpr(CelMutableExpr.ofNotSet()); + + assertThat(staticBuilder.build()).isEqualTo(builderFromInstance.build()); + } + + @Test + public void allNodes_filteredConstants_returnsAllConstants() { + CelNavigableMutableExpr mutableExpr = + CelNavigableMutableExpr.fromExpr( + CelMutableExpr.ofList( + CelMutableList.create( + CelMutableExpr.ofConstant(CelConstant.ofValue("element1")), + CelMutableExpr.ofList( + CelMutableList.create( + CelMutableExpr.ofConstant(CelConstant.ofValue("element2"))))))); + + ImmutableList allNodes = + mutableExpr + .allNodes() + .filter(node -> node.getKind().equals(Kind.CONSTANT)) + .map(BaseNavigableExpr::expr) + .collect(toImmutableList()); + + assertThat(allNodes) + .containsExactly( + CelMutableExpr.ofConstant(CelConstant.ofValue("element1")), + CelMutableExpr.ofConstant(CelConstant.ofValue("element2"))) + .inOrder(); + } + + @Test + public void descendants_filteredConstants_returnsAllConstants() { + CelNavigableMutableExpr mutableExpr = + CelNavigableMutableExpr.fromExpr( + CelMutableExpr.ofList( + CelMutableList.create( + CelMutableExpr.ofConstant(CelConstant.ofValue("element1")), + CelMutableExpr.ofList( + CelMutableList.create( + CelMutableExpr.ofConstant(CelConstant.ofValue("element2"))))))); + + ImmutableList allNodes = + mutableExpr + .descendants() + .filter(node -> node.getKind().equals(Kind.CONSTANT)) + .map(BaseNavigableExpr::expr) + .collect(toImmutableList()); + + assertThat(allNodes) + .containsExactly( + CelMutableExpr.ofConstant(CelConstant.ofValue("element1")), + CelMutableExpr.ofConstant(CelConstant.ofValue("element2"))) + .inOrder(); + } + + @Test + public void children_filteredConstants_returnsSingleConstant() { + CelNavigableMutableExpr mutableExpr = + CelNavigableMutableExpr.fromExpr( + CelMutableExpr.ofList( + CelMutableList.create( + CelMutableExpr.ofConstant(CelConstant.ofValue("element1")), + CelMutableExpr.ofList( + CelMutableList.create( + CelMutableExpr.ofConstant(CelConstant.ofValue("element2"))))))); + + ImmutableList allNodes = + mutableExpr + .children() + .filter(node -> node.getKind().equals(Kind.CONSTANT)) + .map(BaseNavigableExpr::expr) + .collect(toImmutableList()); + + assertThat(allNodes) + .containsExactly(CelMutableExpr.ofConstant(CelConstant.ofValue("element1"))); + } +} 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 44f3fac01..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,6 +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", @@ -8,15 +11,17 @@ java_library( srcs = glob(["*.java"]), deps = [ "//:java_truth", - "//common/resources/testdata/proto2:messages_extensions_proto2_java_proto", - "//common/resources/testdata/proto2:messages_proto2_java_proto", - "//common/resources/testdata/proto3:test_all_types_java_proto", "//common/types", "//common/types:cel_internal_types", + "//common/types:cel_proto_message_types", + "//common/types:cel_proto_types", "//common/types:cel_types", "//common/types:message_type_provider", "//common/types:type_providers", - "@cel_spec//proto/cel/expr:expr_java_proto", + "//testing/protos:single_file_java_proto", + "@cel_spec//proto/cel/expr:checked_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_testparameterinjector_test_parameter_injector", "@maven//:com_google_truth_extensions_truth_proto_extension", diff --git a/common/src/test/java/dev/cel/common/types/CelKindTest.java b/common/src/test/java/dev/cel/common/types/CelKindTest.java index a29a3a1e3..68b487afa 100644 --- a/common/src/test/java/dev/cel/common/types/CelKindTest.java +++ b/common/src/test/java/dev/cel/common/types/CelKindTest.java @@ -40,7 +40,6 @@ public void isPrimitive_false() { assertThat(CelKind.DYN.isPrimitive()).isFalse(); assertThat(CelKind.ANY.isPrimitive()).isFalse(); assertThat(CelKind.DURATION.isPrimitive()).isFalse(); - assertThat(CelKind.FUNCTION.isPrimitive()).isFalse(); assertThat(CelKind.LIST.isPrimitive()).isFalse(); assertThat(CelKind.MAP.isPrimitive()).isFalse(); assertThat(CelKind.NULL_TYPE.isPrimitive()).isFalse(); diff --git a/common/src/test/java/dev/cel/common/types/CelProtoMessageTypesTest.java b/common/src/test/java/dev/cel/common/types/CelProtoMessageTypesTest.java new file mode 100644 index 000000000..593dcc17d --- /dev/null +++ b/common/src/test/java/dev/cel/common/types/CelProtoMessageTypesTest.java @@ -0,0 +1,36 @@ +// 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.types; + +import static com.google.common.truth.Truth.assertThat; + +import dev.cel.expr.Type; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.expr.conformance.proto3.TestAllTypes; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public final class CelProtoMessageTypesTest { + + @Test + public void createMessage_fromDescriptor() { + Type type = CelProtoMessageTypes.createMessage(TestAllTypes.getDescriptor()); + + assertThat(type) + .isEqualTo( + Type.newBuilder().setMessageType(TestAllTypes.getDescriptor().getFullName()).build()); + } +} diff --git a/common/src/test/java/dev/cel/common/types/CelProtoTypesTest.java b/common/src/test/java/dev/cel/common/types/CelProtoTypesTest.java new file mode 100644 index 000000000..bcddd03fd --- /dev/null +++ b/common/src/test/java/dev/cel/common/types/CelProtoTypesTest.java @@ -0,0 +1,122 @@ +// 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. + +package dev.cel.common.types; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; + +import dev.cel.expr.Type; +import dev.cel.expr.Type.AbstractType; +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 org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public final class CelProtoTypesTest { + + @Test + public void isOptionalType_true() { + Type optionalType = CelProtoTypes.createOptionalType(CelProtoTypes.INT64); + + assertThat(CelProtoTypes.isOptionalType(optionalType)).isTrue(); + } + + @Test + public void isOptionalType_false() { + Type notOptionalType = + Type.newBuilder() + .setAbstractType(AbstractType.newBuilder().setName("notOptional").build()) + .build(); + + assertThat(CelProtoTypes.isOptionalType(notOptionalType)).isFalse(); + } + + @Test + public void createOptionalType() { + Type optionalType = CelProtoTypes.createOptionalType(CelProtoTypes.INT64); + + assertThat(optionalType.hasAbstractType()).isTrue(); + assertThat(optionalType.getAbstractType().getName()).isEqualTo("optional_type"); + assertThat(optionalType.getAbstractType().getParameterTypesCount()).isEqualTo(1); + assertThat(optionalType.getAbstractType().getParameterTypes(0)).isEqualTo(CelProtoTypes.INT64); + } + + private enum TestCases { + UNSPECIFIED(UnspecifiedType.create(), Type.getDefaultInstance()), + STRING(SimpleType.STRING, CelProtoTypes.STRING), + INT(NullableType.create(SimpleType.INT), CelProtoTypes.createWrapper(CelProtoTypes.INT64)), + UINT(NullableType.create(SimpleType.UINT), CelProtoTypes.createWrapper(CelProtoTypes.UINT64)), + DOUBLE( + NullableType.create(SimpleType.DOUBLE), CelProtoTypes.createWrapper(CelProtoTypes.DOUBLE)), + BOOL(NullableType.create(SimpleType.BOOL), CelProtoTypes.createWrapper(CelProtoTypes.BOOL)), + BYTES(SimpleType.BYTES, CelProtoTypes.BYTES), + ANY(SimpleType.ANY, CelProtoTypes.ANY), + LIST( + ListType.create(), + Type.newBuilder().setListType(Type.ListType.getDefaultInstance()).build()), + DYN(ListType.create(SimpleType.DYN), CelProtoTypes.createList(CelProtoTypes.DYN)), + ENUM(EnumType.create("CustomEnum", ImmutableMap.of()), CelProtoTypes.INT64), + STRUCT_TYPE_REF( + StructTypeReference.create("MyCustomStruct"), + CelProtoTypes.createMessage("MyCustomStruct")), + OPAQUE( + OpaqueType.create("vector", SimpleType.UINT), + Type.newBuilder() + .setAbstractType( + AbstractType.newBuilder().setName("vector").addParameterTypes(CelProtoTypes.UINT64)) + .build()), + TYPE_PARAM(TypeParamType.create("T"), CelProtoTypes.createTypeParam("T")), + FUNCTION( + CelTypes.createFunctionType( + SimpleType.INT, ImmutableList.of(SimpleType.STRING, SimpleType.UINT)), + Type.newBuilder() + .setFunction( + Type.FunctionType.newBuilder() + .setResultType(CelProtoTypes.INT64) + .addAllArgTypes(ImmutableList.of(CelProtoTypes.STRING, CelProtoTypes.UINT64))) + .build()), + OPTIONAL( + OptionalType.create(SimpleType.INT), CelProtoTypes.createOptionalType(CelProtoTypes.INT64)), + TYPE( + TypeType.create(MapType.create(SimpleType.STRING, SimpleType.STRING)), + CelProtoTypes.create(CelProtoTypes.createMap(CelProtoTypes.STRING, CelProtoTypes.STRING))); + + private final CelType celType; + private final Type type; + + TestCases(CelType celType, Type type) { + this.celType = celType; + this.type = type; + } + } + + @Test + public void celTypeToType(@TestParameter TestCases testCase) { + assertThat(CelProtoTypes.celTypeToType(testCase.celType)).isEqualTo(testCase.type); + } + + @Test + public void typeToCelType(@TestParameter TestCases testCase) { + if (testCase.celType instanceof EnumType) { + // (b/178627883) Strongly typed enum is not supported yet + return; + } + + assertThat(CelProtoTypes.typeToCelType(testCase.type)).isEqualTo(testCase.celType); + } +} diff --git a/common/src/test/java/dev/cel/common/types/CelTypesTest.java b/common/src/test/java/dev/cel/common/types/CelTypesTest.java index da5c556c5..ae6e0808c 100644 --- a/common/src/test/java/dev/cel/common/types/CelTypesTest.java +++ b/common/src/test/java/dev/cel/common/types/CelTypesTest.java @@ -15,10 +15,8 @@ package dev.cel.common.types; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; import dev.cel.expr.Type; -import dev.cel.expr.Type.AbstractType; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.testing.junit.testparameterinjector.TestParameter; @@ -29,52 +27,6 @@ @RunWith(TestParameterInjector.class) public final class CelTypesTest { - private enum TestCases { - UNSPECIFIED(UnspecifiedType.create(), Type.getDefaultInstance()), - STRING(SimpleType.STRING, CelTypes.STRING), - INT(NullableType.create(SimpleType.INT), CelTypes.createWrapper(CelTypes.INT64)), - UINT(NullableType.create(SimpleType.UINT), CelTypes.createWrapper(CelTypes.UINT64)), - DOUBLE(NullableType.create(SimpleType.DOUBLE), CelTypes.createWrapper(CelTypes.DOUBLE)), - BOOL(NullableType.create(SimpleType.BOOL), CelTypes.createWrapper(CelTypes.BOOL)), - BYTES(SimpleType.BYTES, CelTypes.BYTES), - ANY(SimpleType.ANY, CelTypes.ANY), - LIST( - ListType.create(), - Type.newBuilder().setListType(Type.ListType.getDefaultInstance()).build()), - DYN(ListType.create(SimpleType.DYN), CelTypes.createList(CelTypes.DYN)), - ENUM(EnumType.create("CustomEnum", ImmutableMap.of()), CelTypes.INT64), - STRUCT_TYPE_REF( - StructTypeReference.create("MyCustomStruct"), CelTypes.createMessage("MyCustomStruct")), - OPAQUE( - OpaqueType.create("vector", SimpleType.UINT), - Type.newBuilder() - .setAbstractType( - AbstractType.newBuilder().setName("vector").addParameterTypes(CelTypes.UINT64)) - .build()), - TYPE_PARAM(TypeParamType.create("T"), CelTypes.createTypeParam("T")), - FUNCTION( - CelTypes.createFunctionType( - SimpleType.INT, ImmutableList.of(SimpleType.STRING, SimpleType.UINT)), - Type.newBuilder() - .setFunction( - Type.FunctionType.newBuilder() - .setResultType(CelTypes.INT64) - .addAllArgTypes(ImmutableList.of(CelTypes.STRING, CelTypes.UINT64))) - .build()), - OPTIONAL(OptionalType.create(SimpleType.INT), CelTypes.createOptionalType(CelTypes.INT64)), - TYPE( - TypeType.create(MapType.create(SimpleType.STRING, SimpleType.STRING)), - CelTypes.create(CelTypes.createMap(CelTypes.STRING, CelTypes.STRING))); - - private final CelType celType; - private final Type type; - - TestCases(CelType celType, Type type) { - this.celType = celType; - this.type = type; - } - } - @Test public void isWellKnownType_true() { assertThat(CelTypes.isWellKnownType(CelTypes.ANY_MESSAGE)).isTrue(); @@ -85,48 +37,6 @@ public void isWellKnownType_false() { assertThat(CelTypes.isWellKnownType("CustomType")).isFalse(); } - @Test - public void createOptionalType() { - Type optionalType = CelTypes.createOptionalType(CelTypes.INT64); - - assertThat(optionalType.hasAbstractType()).isTrue(); - assertThat(optionalType.getAbstractType().getName()).isEqualTo("optional_type"); - assertThat(optionalType.getAbstractType().getParameterTypesCount()).isEqualTo(1); - assertThat(optionalType.getAbstractType().getParameterTypes(0)).isEqualTo(CelTypes.INT64); - } - - @Test - public void isOptionalType_true() { - Type optionalType = CelTypes.createOptionalType(CelTypes.INT64); - - assertThat(CelTypes.isOptionalType(optionalType)).isTrue(); - } - - @Test - public void isOptionalType_false() { - Type notOptionalType = - Type.newBuilder() - .setAbstractType(AbstractType.newBuilder().setName("notOptional").build()) - .build(); - - assertThat(CelTypes.isOptionalType(notOptionalType)).isFalse(); - } - - @Test - public void celTypeToType(@TestParameter TestCases testCase) { - assertThat(CelTypes.celTypeToType(testCase.celType)).isEqualTo(testCase.type); - } - - @Test - public void typeToCelType(@TestParameter TestCases testCase) { - if (testCase.celType instanceof EnumType) { - // (b/178627883) Strongly typed enum is not supported yet - return; - } - - assertThat(CelTypes.typeToCelType(testCase.type)).isEqualTo(testCase.celType); - } - private enum FormatTestCases { UNSPECIFIED(UnspecifiedType.create(), ""), STRING(SimpleType.STRING, "string"), @@ -171,9 +81,9 @@ public void format_withCelType(@TestParameter FormatTestCases testCase) { @Test public void format_withType(@TestParameter FormatTestCases testCase) { - Type type = CelTypes.celTypeToType(testCase.celType); + Type type = CelProtoTypes.celTypeToType(testCase.celType); - assertThat(CelTypes.format(type)).isEqualTo(testCase.formattedString); + assertThat(CelProtoTypes.format(type)).isEqualTo(testCase.formattedString); } @Test 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 f07a67b71..c9f9d9e21 100644 --- a/common/src/test/java/dev/cel/common/types/ProtoMessageTypeProviderTest.java +++ b/common/src/test/java/dev/cel/common/types/ProtoMessageTypeProviderTest.java @@ -20,9 +20,10 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import dev.cel.common.types.CelTypeProvider.CombinedCelTypeProvider; -import dev.cel.testing.testdata.proto2.MessagesProto2; -import dev.cel.testing.testdata.proto2.MessagesProto2Extensions; -import dev.cel.testing.testdata.proto3.TestAllTypesProto.TestAllTypes; +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.SingleFile; import java.util.Optional; import org.junit.Test; import org.junit.runner.RunWith; @@ -34,12 +35,15 @@ public final class ProtoMessageTypeProviderTest { private final ProtoMessageTypeProvider emptyProvider = new ProtoMessageTypeProvider(); private final ProtoMessageTypeProvider proto3Provider = - new ProtoMessageTypeProvider(ImmutableList.of(TestAllTypes.getDescriptor())); + new ProtoMessageTypeProvider( + ImmutableList.of(dev.cel.expr.conformance.proto3.TestAllTypes.getDescriptor())); private final ProtoMessageTypeProvider proto2Provider = new ProtoMessageTypeProvider( ImmutableSet.of( - MessagesProto2.getDescriptor(), MessagesProto2Extensions.getDescriptor())); + dev.cel.expr.conformance.proto3.TestAllTypes.getDescriptor().getFile(), + TestAllTypes.getDescriptor().getFile(), + TestAllTypesExtensions.getDescriptor())); @Test public void types_emptyTypeSet() { @@ -55,21 +59,20 @@ public void findType_emptyTypeSet() { public void types_allGlobalAndNestedDeclarations() { assertThat(proto3Provider.types().stream().map(CelType::name).collect(toImmutableList())) .containsAtLeast( - "dev.cel.testing.testdata.proto3.GlobalEnum", - "dev.cel.testing.testdata.proto3.TestAllTypes", - "dev.cel.testing.testdata.proto3.TestAllTypes.NestedMessage", - "dev.cel.testing.testdata.proto3.TestAllTypes.NestedEnum", - "dev.cel.testing.testdata.proto3.NestedTestAllTypes"); + "cel.expr.conformance.proto3.GlobalEnum", + "cel.expr.conformance.proto3.TestAllTypes", + "cel.expr.conformance.proto3.TestAllTypes.NestedMessage", + "cel.expr.conformance.proto3.TestAllTypes.NestedEnum", + "cel.expr.conformance.proto3.NestedTestAllTypes"); } @Test public void findType_globalEnumWithAllNamesAndNumbers() { - Optional celType = - proto3Provider.findType("dev.cel.testing.testdata.proto3.GlobalEnum"); + Optional celType = proto3Provider.findType("cel.expr.conformance.proto3.GlobalEnum"); assertThat(celType).isPresent(); assertThat(celType.get()).isInstanceOf(EnumType.class); EnumType enumType = (EnumType) celType.get(); - assertThat(enumType.name()).isEqualTo("dev.cel.testing.testdata.proto3.GlobalEnum"); + assertThat(enumType.name()).isEqualTo("cel.expr.conformance.proto3.GlobalEnum"); assertThat(enumType.findNameByNumber(0)).hasValue("GOO"); assertThat(enumType.findNameByNumber(1)).hasValue("GAR"); assertThat(enumType.findNameByNumber(2)).hasValue("GAZ"); @@ -79,12 +82,11 @@ public void findType_globalEnumWithAllNamesAndNumbers() { @Test public void findType_nestedEnumWithAllNamesAndNumbers() { Optional celType = - proto3Provider.findType("dev.cel.testing.testdata.proto3.TestAllTypes.NestedEnum"); + proto3Provider.findType("cel.expr.conformance.proto3.TestAllTypes.NestedEnum"); assertThat(celType).isPresent(); assertThat(celType.get()).isInstanceOf(EnumType.class); EnumType enumType = (EnumType) celType.get(); - assertThat(enumType.name()) - .isEqualTo("dev.cel.testing.testdata.proto3.TestAllTypes.NestedEnum"); + assertThat(enumType.name()).isEqualTo("cel.expr.conformance.proto3.TestAllTypes.NestedEnum"); assertThat(enumType.findNumberByName("FOO")).hasValue(0); assertThat(enumType.findNumberByName("BAR")).hasValue(1); assertThat(enumType.findNumberByName("BAZ")).hasValue(2); @@ -94,11 +96,11 @@ public void findType_nestedEnumWithAllNamesAndNumbers() { @Test public void findType_globalMessageTypeNoExtensions() { Optional celType = - proto3Provider.findType("dev.cel.testing.testdata.proto3.NestedTestAllTypes"); + proto3Provider.findType("cel.expr.conformance.proto3.NestedTestAllTypes"); assertThat(celType).isPresent(); assertThat(celType.get()).isInstanceOf(ProtoMessageType.class); ProtoMessageType protoType = (ProtoMessageType) celType.get(); - assertThat(protoType.name()).isEqualTo("dev.cel.testing.testdata.proto3.NestedTestAllTypes"); + assertThat(protoType.name()).isEqualTo("cel.expr.conformance.proto3.NestedTestAllTypes"); assertThat(protoType.findField("payload")).isPresent(); assertThat(protoType.findField("child")).isPresent(); assertThat(protoType.findField("missing")).isEmpty(); @@ -108,102 +110,94 @@ public void findType_globalMessageTypeNoExtensions() { @Test public void findType_globalMessageWithExtensions() { - Optional celType = - proto2Provider.findType("dev.cel.testing.testdata.proto2.Proto2Message"); + Optional celType = proto2Provider.findType("cel.expr.conformance.proto2.TestAllTypes"); assertThat(celType).isPresent(); assertThat(celType.get()).isInstanceOf(ProtoMessageType.class); ProtoMessageType protoType = (ProtoMessageType) celType.get(); - assertThat(protoType.name()).isEqualTo("dev.cel.testing.testdata.proto2.Proto2Message"); + assertThat(protoType.name()).isEqualTo("cel.expr.conformance.proto2.TestAllTypes"); assertThat(protoType.findField("single_int32")).isPresent(); - assertThat(protoType.findField("single_enum")).isPresent(); - assertThat(protoType.findField("single_nested_test_all_types")).isPresent(); + assertThat(protoType.findField("single_uint64")).isPresent(); + assertThat(protoType.findField("oneof_type")).isPresent(); assertThat(protoType.findField("nestedgroup")).isPresent(); - assertThat(protoType.findField("nested_ext")).isEmpty(); + assertThat(protoType.findField("nested_enum_ext")).isEmpty(); - assertThat(protoType.findExtension("dev.cel.testing.testdata.proto2.nested_ext")).isPresent(); - assertThat(protoType.findExtension("dev.cel.testing.testdata.proto2.int32_ext")).isPresent(); - assertThat(protoType.findExtension("dev.cel.testing.testdata.proto2.test_all_types_ext")) - .isPresent(); - assertThat(protoType.findExtension("dev.cel.testing.testdata.proto2.nested_enum_ext")) + assertThat(protoType.findExtension("cel.expr.conformance.proto2.nested_ext")).isPresent(); + assertThat(protoType.findExtension("cel.expr.conformance.proto2.int32_ext")).isPresent(); + assertThat(protoType.findExtension("cel.expr.conformance.proto2.test_all_types_ext")) .isPresent(); - assertThat( - protoType.findExtension("dev.cel.testing.testdata.proto2.repeated_string_holder_ext")) + assertThat(protoType.findExtension("cel.expr.conformance.proto2.nested_enum_ext")).isPresent(); + assertThat(protoType.findExtension("cel.expr.conformance.proto2.repeated_test_all_types")) .isPresent(); - assertThat(protoType.findExtension("dev.cel.testing.testdata.proto2.Proto2Message.int32_ext")) + assertThat(protoType.findExtension("cel.expr.conformance.proto2.TestAllTypes.int32_ext")) .isEmpty(); Optional holderType = - proto2Provider.findType("dev.cel.testing.testdata.proto2.StringHolder"); + proto2Provider.findType("cel.expr.conformance.proto2.TestRequired"); assertThat(holderType).isPresent(); ProtoMessageType stringHolderType = (ProtoMessageType) holderType.get(); - assertThat(stringHolderType.findExtension("dev.cel.testing.testdata.proto2.nested_enum_ext")) + assertThat(stringHolderType.findExtension("cel.expr.conformance.proto2.nested_enum_ext")) .isEmpty(); } @Test public void findType_scopedMessageWithExtensions() { - Optional celType = - proto2Provider.findType("dev.cel.testing.testdata.proto2.Proto2Message"); + Optional celType = proto2Provider.findType("cel.expr.conformance.proto2.TestAllTypes"); assertThat(celType).isPresent(); assertThat(celType.get()).isInstanceOf(ProtoMessageType.class); ProtoMessageType protoType = (ProtoMessageType) celType.get(); assertThat( protoType.findExtension( - "dev.cel.testing.testdata.proto2.Proto2ExtensionScopedMessage.message_scoped_nested_ext")) + "cel.expr.conformance.proto2.Proto2ExtensionScopedMessage.message_scoped_nested_ext")) .isPresent(); assertThat( protoType.findExtension( - "dev.cel.testing.testdata.proto2.Proto2ExtensionScopedMessage.int64_ext")) + "cel.expr.conformance.proto2.Proto2ExtensionScopedMessage.int64_ext")) .isPresent(); assertThat( protoType.findExtension( - "dev.cel.testing.testdata.proto2.Proto2ExtensionScopedMessage.string_ext")) + "cel.expr.conformance.proto2.Proto2ExtensionScopedMessage.message_scoped_repeated_test_all_types")) .isPresent(); assertThat( protoType.findExtension( - "dev.cel.testing.testdata.proto2.Proto2ExtensionScopedMessage.nested_message_inside_ext")) + "cel.expr.conformance.proto2.Proto2ExtensionScopedMessage.message_scoped_nested_ext")) .isPresent(); } @Test public void findType_withRepeatedEnumField() { - Optional celType = - proto3Provider.findType("dev.cel.testing.testdata.proto3.TestAllTypes"); + Optional celType = proto3Provider.findType("cel.expr.conformance.proto3.TestAllTypes"); assertThat(celType).isPresent(); assertThat(celType.get()).isInstanceOf(ProtoMessageType.class); ProtoMessageType protoType = (ProtoMessageType) celType.get(); - assertThat(protoType.name()).isEqualTo("dev.cel.testing.testdata.proto3.TestAllTypes"); + assertThat(protoType.name()).isEqualTo("cel.expr.conformance.proto3.TestAllTypes"); assertThat(protoType.findField("repeated_nested_enum")).isPresent(); CelType fieldType = protoType.findField("repeated_nested_enum").get().type(); assertThat(fieldType.kind()).isEqualTo(CelKind.LIST); assertThat(fieldType.parameters()).hasSize(1); CelType elemType = fieldType.parameters().get(0); - assertThat(elemType.name()) - .isEqualTo("dev.cel.testing.testdata.proto3.TestAllTypes.NestedEnum"); + assertThat(elemType.name()).isEqualTo("cel.expr.conformance.proto3.TestAllTypes.NestedEnum"); assertThat(elemType.kind()).isEqualTo(CelKind.INT); assertThat(elemType).isInstanceOf(EnumType.class); - assertThat(proto3Provider.findType("dev.cel.testing.testdata.proto3.TestAllTypes.NestedEnum")) + assertThat(proto3Provider.findType("cel.expr.conformance.proto3.TestAllTypes.NestedEnum")) .hasValue(elemType); } @Test public void findType_withOneofField() { - Optional celType = - proto3Provider.findType("dev.cel.testing.testdata.proto3.TestAllTypes"); + Optional celType = proto3Provider.findType("cel.expr.conformance.proto3.TestAllTypes"); ProtoMessageType protoType = (ProtoMessageType) celType.get(); - assertThat(protoType.name()).isEqualTo("dev.cel.testing.testdata.proto3.TestAllTypes"); + assertThat(protoType.name()).isEqualTo("cel.expr.conformance.proto3.TestAllTypes"); assertThat(protoType.findField("single_nested_message").map(f -> f.type().name())) - .hasValue("dev.cel.testing.testdata.proto3.TestAllTypes.NestedMessage"); + .hasValue("cel.expr.conformance.proto3.TestAllTypes.NestedMessage"); } @Test public void findType_withMapField() { - Optional celType = - proto3Provider.findType("dev.cel.testing.testdata.proto3.TestAllTypes"); + Optional celType = proto3Provider.findType("cel.expr.conformance.proto3.TestAllTypes"); ProtoMessageType protoType = (ProtoMessageType) celType.get(); CelType fieldType = protoType.findField("map_int64_nested_type").get().type(); @@ -213,14 +207,13 @@ public void findType_withMapField() { CelType valueType = fieldType.parameters().get(1); assertThat(keyType.name()).isEqualTo("int"); assertThat(keyType.kind()).isEqualTo(CelKind.INT); - assertThat(valueType.name()).isEqualTo("dev.cel.testing.testdata.proto3.NestedTestAllTypes"); + assertThat(valueType.name()).isEqualTo("cel.expr.conformance.proto3.NestedTestAllTypes"); assertThat(valueType.kind()).isEqualTo(CelKind.STRUCT); } @Test public void findType_withWellKnownTypes() { - Optional celType = - proto3Provider.findType("dev.cel.testing.testdata.proto3.TestAllTypes"); + Optional celType = proto3Provider.findType("cel.expr.conformance.proto3.TestAllTypes"); ProtoMessageType protoType = (ProtoMessageType) celType.get(); assertThat(protoType.findField("single_any").map(f -> f.type())).hasValue(SimpleType.ANY); assertThat(protoType.findField("single_duration").map(f -> f.type())) @@ -228,7 +221,7 @@ public void findType_withWellKnownTypes() { assertThat(protoType.findField("single_timestamp").map(f -> f.type())) .hasValue(SimpleType.TIMESTAMP); assertThat(protoType.findField("single_value").map(f -> f.type())).hasValue(SimpleType.DYN); - assertThat(protoType.findField("single_list_value").map(f -> f.type())) + assertThat(protoType.findField("list_value").map(f -> f.type())) .hasValue(ListType.create(SimpleType.DYN)); assertThat(protoType.findField("single_struct").map(f -> f.type())) .hasValue(MapType.create(SimpleType.STRING, SimpleType.DYN)); @@ -263,4 +256,23 @@ public void types_combinedDuplicateProviderIsSameAsFirst() { CombinedCelTypeProvider combined = new CombinedCelTypeProvider(proto3Provider, proto3Provider); assertThat(combined.types()).hasSize(proto3Provider.types().size()); } + + @Test + public void findField_withJsonNameOption() { + ProtoMessageTypeProvider typeProvider = + ProtoMessageTypeProvider.newBuilder() + .addFileDescriptors(SingleFile.getDescriptor().getFile()) + .setAllowJsonFieldNames(true) + .build(); + + ProtoMessageType msgType = + (ProtoMessageType) typeProvider.findType(SingleFile.getDescriptor().getFullName()).get(); + + // Note that these are the same fields, with json_name option set + 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/types/ProtoMessageTypeTest.java b/common/src/test/java/dev/cel/common/types/ProtoMessageTypeTest.java index 3feab06fd..d6e90b1b4 100644 --- a/common/src/test/java/dev/cel/common/types/ProtoMessageTypeTest.java +++ b/common/src/test/java/dev/cel/common/types/ProtoMessageTypeTest.java @@ -46,7 +46,8 @@ public void setUp() { "my.package.TestMessage", FIELD_MAP.keySet(), (field) -> Optional.ofNullable(FIELD_MAP.get(field)), - (extension) -> Optional.ofNullable(EXTENSION_MAP.get(extension))); + (extension) -> Optional.ofNullable(EXTENSION_MAP.get(extension)), + (unused) -> false); } @Test 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 5b3ac349c..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,6 +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,30 +12,36 @@ java_library( deps = [ "//:java_truth", "//bundle:cel", - "//common", + "//common:cel_ast", + "//common:cel_descriptor_util", "//common:options", - "//common:runtime_exception", "//common/internal:cel_descriptor_pools", + "//common/internal:cel_lite_descriptor_pool", + "//common/internal:default_lite_descriptor_pool", "//common/internal:default_message_factory", "//common/internal:dynamic_proto", "//common/internal:proto_message_factory", - "//common/resources/testdata/proto2:messages_extensions_proto2_java_proto", - "//common/resources/testdata/proto2:messages_proto2_java_proto", - "//common/resources/testdata/proto2:test_all_types_java_proto", "//common/types", "//common/types:type_providers", "//common/values", "//common/values:cel_byte_string", - "//common/values:cel_value", "//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", "//common/values:proto_message_value", "//common/values:proto_message_value_provider", + "//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", - "@maven//:com_google_protobuf_protobuf_java_util", "@maven//:com_google_testparameterinjector_test_parameter_injector", "@maven//:junit_junit", + "@maven_android//:com_google_protobuf_protobuf_javalite", ], ) diff --git a/common/src/test/java/dev/cel/common/values/BoolValueTest.java b/common/src/test/java/dev/cel/common/values/BoolValueTest.java deleted file mode 100644 index dbe9ab577..000000000 --- a/common/src/test/java/dev/cel/common/values/BoolValueTest.java +++ /dev/null @@ -1,55 +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.common.values; - -import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.assertThrows; - -import dev.cel.common.types.SimpleType; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public class BoolValueTest { - - @Test - public void falseBool() { - BoolValue boolValue = BoolValue.create(false); - - assertThat(boolValue.value()).isFalse(); - assertThat(boolValue.isZeroValue()).isTrue(); - } - - @Test - public void trueBool() { - BoolValue boolValue = BoolValue.create(true); - - assertThat(boolValue.value()).isTrue(); - assertThat(boolValue.isZeroValue()).isFalse(); - } - - @Test - public void celTypeTest() { - BoolValue value = BoolValue.create(true); - - assertThat(value.celType()).isEqualTo(SimpleType.BOOL); - } - - @Test - public void create_nullValue_throws() { - assertThrows(NullPointerException.class, () -> BoolValue.create(null)); - } -} diff --git a/common/src/test/java/dev/cel/common/values/BytesValueTest.java b/common/src/test/java/dev/cel/common/values/BytesValueTest.java deleted file mode 100644 index 115838a0f..000000000 --- a/common/src/test/java/dev/cel/common/values/BytesValueTest.java +++ /dev/null @@ -1,55 +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.common.values; - -import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.assertThrows; - -import dev.cel.common.types.SimpleType; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public class BytesValueTest { - - @Test - public void emptyBytes() { - BytesValue bytesValue = BytesValue.create(CelByteString.EMPTY); - - assertThat(bytesValue.value()).isEqualTo(CelByteString.of(new byte[0])); - assertThat(bytesValue.isZeroValue()).isTrue(); - } - - @Test - public void constructBytes() { - BytesValue bytesValue = BytesValue.create(CelByteString.of(new byte[] {0x1, 0x5, 0xc})); - - assertThat(bytesValue.value()).isEqualTo(CelByteString.of(new byte[] {0x1, 0x5, 0xc})); - assertThat(bytesValue.isZeroValue()).isFalse(); - } - - @Test - public void celTypeTest() { - BytesValue value = BytesValue.create(CelByteString.EMPTY); - - assertThat(value.celType()).isEqualTo(SimpleType.BYTES); - } - - @Test - public void create_nullValue_throws() { - assertThrows(NullPointerException.class, () -> BytesValue.create(null)); - } -} 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 c373f04fc..ccb8e605f 100644 --- a/common/src/test/java/dev/cel/common/values/CelValueConverterTest.java +++ b/common/src/test/java/dev/cel/common/values/CelValueConverterTest.java @@ -16,9 +16,6 @@ import static com.google.common.truth.Truth.assertThat; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import dev.cel.common.CelOptions; import java.util.Optional; import org.junit.Test; import org.junit.runner.RunWith; @@ -26,77 +23,31 @@ @RunWith(JUnit4.class) public class CelValueConverterTest { - private static final CelValueConverter CEL_VALUE_CONVERTER = - new CelValueConverter(CelOptions.DEFAULT) {}; - - @Test - public void fromJavaPrimitiveToCelValue_returnsOpaqueValue() { - OpaqueValue opaqueValue = - (OpaqueValue) CEL_VALUE_CONVERTER.fromJavaPrimitiveToCelValue(new UserDefinedClass()); - - assertThat(opaqueValue.celType().name()).contains("UserDefinedClass"); - } + private static final CelValueConverter CEL_VALUE_CONVERTER = new CelValueConverter() {}; @Test @SuppressWarnings("unchecked") // Test only - public void fromJavaObjectToCelValue_optionalValue() { - OptionalValue optionalValue = - (OptionalValue) - CEL_VALUE_CONVERTER.fromJavaObjectToCelValue(Optional.of("test")); + public void toRuntimeValue_optionalValue() { + OptionalValue optionalValue = + (OptionalValue) CEL_VALUE_CONVERTER.toRuntimeValue(Optional.of("test")); - assertThat(optionalValue).isEqualTo(OptionalValue.create(StringValue.create("test"))); - } - - @Test - public void fromJavaObjectToCelValue_errorValue() { - IllegalArgumentException e = new IllegalArgumentException("error"); - - ErrorValue errorValue = (ErrorValue) CEL_VALUE_CONVERTER.fromJavaObjectToCelValue(e); - - assertThat(errorValue.value()).isEqualTo(e); + assertThat(optionalValue).isEqualTo(OptionalValue.create("test")); } @Test @SuppressWarnings("unchecked") // Test only - public void fromCelValueToJavaObject_mapValue() { - ImmutableMap result = - (ImmutableMap) - CEL_VALUE_CONVERTER.fromCelValueToJavaObject( - ImmutableMapValue.create( - ImmutableMap.of(StringValue.create("test"), IntValue.create(1)))); - - assertThat(result).containsExactly("test", 1L); - } - - @Test - @SuppressWarnings("unchecked") // Test only - public void fromCelValueToJavaObject_listValue() { - ImmutableList result = - (ImmutableList) - CEL_VALUE_CONVERTER.fromCelValueToJavaObject( - ImmutableListValue.create(ImmutableList.of(BoolValue.create(true)))); - - assertThat(result).containsExactly(true); - } - - @Test - @SuppressWarnings("unchecked") // Test only - public void fromCelValueToJavaObject_optionalValue() { + public void unwrap_optionalValue() { Optional result = - (Optional) - CEL_VALUE_CONVERTER.fromCelValueToJavaObject(OptionalValue.create(IntValue.create(2))); + (Optional) CEL_VALUE_CONVERTER.maybeUnwrap(OptionalValue.create(2L)); assertThat(result).isEqualTo(Optional.of(2L)); } @Test @SuppressWarnings("unchecked") // Test only - public void fromCelValueToJavaObject_emptyOptionalValue() { - Optional result = - (Optional) CEL_VALUE_CONVERTER.fromCelValueToJavaObject(OptionalValue.EMPTY); + public void unwrap_emptyOptionalValue() { + Optional result = (Optional) CEL_VALUE_CONVERTER.maybeUnwrap(OptionalValue.EMPTY); assertThat(result).isEqualTo(Optional.empty()); } - - private static class UserDefinedClass {} } 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/DoubleValueTest.java b/common/src/test/java/dev/cel/common/values/DoubleValueTest.java deleted file mode 100644 index f2c8374a1..000000000 --- a/common/src/test/java/dev/cel/common/values/DoubleValueTest.java +++ /dev/null @@ -1,84 +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.common.values; - -import static com.google.common.truth.Truth.assertThat; - -import com.google.common.testing.ClassSanityTester; -import com.google.common.testing.EqualsTester; -import dev.cel.common.types.SimpleType; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public class DoubleValueTest { - - @Test - public void emptyDouble() { - DoubleValue doubleValue = DoubleValue.create(0.0d); - - assertThat(doubleValue.value()).isEqualTo(0.0d); - assertThat(doubleValue.isZeroValue()).isTrue(); - } - - @Test - public void constructDouble() { - DoubleValue doubleValue = DoubleValue.create(5.0d); - - assertThat(doubleValue.value()).isEqualTo(5.0d); - assertThat(doubleValue.isZeroValue()).isFalse(); - } - - @Test - public void celTypeTest() { - DoubleValue value = DoubleValue.create(0.0d); - - assertThat(value.celType()).isEqualTo(SimpleType.DOUBLE); - } - - @Test - public void equalityTest() { - new EqualsTester() - .addEqualityGroup(DoubleValue.create(10.5)) - .addEqualityGroup(DoubleValue.create(0.0d), DoubleValue.create(0)) - .addEqualityGroup(DoubleValue.create(15.3), DoubleValue.create(15.3)) - .addEqualityGroup( - DoubleValue.create(Double.MAX_VALUE), DoubleValue.create(Double.MAX_VALUE)) - .addEqualityGroup( - DoubleValue.create(Double.MIN_VALUE), DoubleValue.create(Double.MIN_VALUE)) - .testEquals(); - } - - @Test - public void sanityTest() throws Exception { - new ClassSanityTester() - .setDefault(DoubleValue.class, DoubleValue.create(100.94d)) - .setDistinctValues(DoubleValue.class, DoubleValue.create(0.0d), DoubleValue.create(100.0d)) - .forAllPublicStaticMethods(DoubleValue.class) - .thatReturn(DoubleValue.class) - .testEquals() - .testNulls(); - } - - @Test - public void hashCode_smokeTest() { - assertThat(DoubleValue.create(0).hashCode()).isEqualTo(1000003); - assertThat(DoubleValue.create(0.0d).hashCode()).isEqualTo(1000003); - assertThat(DoubleValue.create(100.5d).hashCode()).isEqualTo(1079403075); - assertThat(DoubleValue.create(Double.MAX_VALUE).hashCode()).isEqualTo(-2145435069); - assertThat(DoubleValue.create(Double.MIN_VALUE).hashCode()).isEqualTo(1000002); - } -} diff --git a/common/src/test/java/dev/cel/common/values/DurationValueTest.java b/common/src/test/java/dev/cel/common/values/DurationValueTest.java deleted file mode 100644 index e53e75379..000000000 --- a/common/src/test/java/dev/cel/common/values/DurationValueTest.java +++ /dev/null @@ -1,56 +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.common.values; - -import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.assertThrows; - -import dev.cel.common.types.SimpleType; -import java.time.Duration; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public class DurationValueTest { - - @Test - public void emptyDuration() { - DurationValue durationValue = DurationValue.create(Duration.ZERO); - - assertThat(durationValue.value()).isEqualTo(Duration.ofSeconds(0)); - assertThat(durationValue.isZeroValue()).isTrue(); - } - - @Test - public void constructDuration() { - DurationValue durationValue = DurationValue.create(Duration.ofSeconds(10000)); - - assertThat(durationValue.value()).isEqualTo(Duration.ofSeconds(10000)); - assertThat(durationValue.isZeroValue()).isFalse(); - } - - @Test - public void celTypeTest() { - DurationValue value = DurationValue.create(Duration.ZERO); - - assertThat(value.celType()).isEqualTo(SimpleType.DURATION); - } - - @Test - public void create_nullValue_throws() { - assertThrows(NullPointerException.class, () -> DurationValue.create(null)); - } -} diff --git a/common/src/test/java/dev/cel/common/values/EnumValueTest.java b/common/src/test/java/dev/cel/common/values/EnumValueTest.java deleted file mode 100644 index d9fde4b76..000000000 --- a/common/src/test/java/dev/cel/common/values/EnumValueTest.java +++ /dev/null @@ -1,53 +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.common.values; - -import static com.google.common.truth.Truth.assertThat; - -import dev.cel.common.types.SimpleType; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public class EnumValueTest { - - private enum TestKind { - ONE, - TWO - } - - @Test - public void enumValue_construct() { - EnumValue one = EnumValue.create(TestKind.ONE); - EnumValue two = EnumValue.create(TestKind.TWO); - - assertThat(one.value()).isEqualTo(TestKind.ONE); - assertThat(two.value()).isEqualTo(TestKind.TWO); - } - - @Test - public void enumValue_isZeroValue_returnsFalse() { - assertThat(EnumValue.create(TestKind.ONE).isZeroValue()).isFalse(); - assertThat(EnumValue.create(TestKind.TWO).isZeroValue()).isFalse(); - } - - @Test - public void celTypeTest() { - EnumValue value = EnumValue.create(TestKind.ONE); - - assertThat(value.celType()).isEqualTo(SimpleType.INT); - } -} diff --git a/common/src/test/java/dev/cel/common/values/ErrorValueTest.java b/common/src/test/java/dev/cel/common/values/ErrorValueTest.java index 0db711f72..a6a4edb66 100644 --- a/common/src/test/java/dev/cel/common/values/ErrorValueTest.java +++ b/common/src/test/java/dev/cel/common/values/ErrorValueTest.java @@ -27,7 +27,7 @@ public class ErrorValueTest { @Test public void errorValue_construct() { IllegalArgumentException exception = new IllegalArgumentException("test"); - ErrorValue opaqueValue = ErrorValue.create(exception); + ErrorValue opaqueValue = ErrorValue.create(0L, exception); assertThat(opaqueValue.value()).isEqualTo(exception); assertThat(opaqueValue.isZeroValue()).isFalse(); @@ -35,12 +35,12 @@ public void errorValue_construct() { @Test public void create_nullValue_throws() { - assertThrows(NullPointerException.class, () -> ErrorValue.create(null)); + assertThrows(NullPointerException.class, () -> ErrorValue.create(0L, null)); } @Test public void celTypeTest() { - ErrorValue value = ErrorValue.create(new IllegalArgumentException("test")); + ErrorValue value = ErrorValue.create(0L, new IllegalArgumentException("test")); assertThat(value.celType()).isEqualTo(SimpleType.ERROR); } diff --git a/common/src/test/java/dev/cel/common/values/ImmutableListValueTest.java b/common/src/test/java/dev/cel/common/values/ImmutableListValueTest.java deleted file mode 100644 index 1d5099ec2..000000000 --- a/common/src/test/java/dev/cel/common/values/ImmutableListValueTest.java +++ /dev/null @@ -1,73 +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.common.values; - -import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.assertThrows; - -import com.google.common.collect.ImmutableList; -import dev.cel.common.types.ListType; -import dev.cel.common.types.SimpleType; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public class ImmutableListValueTest { - - @Test - public void emptyList() { - ListValue listValue = ImmutableListValue.create(ImmutableList.of()); - - assertThat(listValue.value()).isEmpty(); - assertThat(listValue.isZeroValue()).isTrue(); - } - - @Test - public void listValue_construct() { - IntValue one = IntValue.create(1L); - IntValue two = IntValue.create(2L); - IntValue three = IntValue.create(3L); - - ListValue listValue = ImmutableListValue.create(ImmutableList.of(one, two, three)); - - assertThat(listValue.value()).containsExactly(one, two, three).inOrder(); - assertThat(listValue.isZeroValue()).isFalse(); - } - - @Test - public void listValue_mixedTypes() { - IntValue one = IntValue.create(1L); - DoubleValue two = DoubleValue.create(2.0d); - StringValue three = StringValue.create("test"); - - ListValue listValue = ImmutableListValue.create(ImmutableList.of(one, two, three)); - - assertThat(listValue.value()).containsExactly(one, two, three).inOrder(); - assertThat(listValue.isZeroValue()).isFalse(); - } - - @Test - public void create_nullValue_throws() { - assertThrows(NullPointerException.class, () -> ImmutableListValue.create(null)); - } - - @Test - public void celTypeTest() { - ListValue value = ImmutableListValue.create(ImmutableList.of()); - - assertThat(value.celType()).isEqualTo(ListType.create(SimpleType.DYN)); - } -} diff --git a/common/src/test/java/dev/cel/common/values/ImmutableMapValueTest.java b/common/src/test/java/dev/cel/common/values/ImmutableMapValueTest.java deleted file mode 100644 index ee91712ec..000000000 --- a/common/src/test/java/dev/cel/common/values/ImmutableMapValueTest.java +++ /dev/null @@ -1,109 +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.common.values; - -import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.assertThrows; - -import com.google.common.collect.ImmutableMap; -import com.google.testing.junit.testparameterinjector.TestParameterInjector; -import com.google.testing.junit.testparameterinjector.TestParameters; -import dev.cel.common.CelRuntimeException; -import dev.cel.common.types.MapType; -import dev.cel.common.types.SimpleType; -import org.junit.Test; -import org.junit.runner.RunWith; - -@RunWith(TestParameterInjector.class) -public class ImmutableMapValueTest { - - @Test - public void emptyMap() { - ImmutableMapValue mapValue = ImmutableMapValue.create(ImmutableMap.of()); - - assertThat(mapValue.value()).isEmpty(); - assertThat(mapValue.isZeroValue()).isTrue(); - } - - @Test - public void mapValue_construct() { - IntValue one = IntValue.create(1L); - StringValue hello = StringValue.create("hello"); - - ImmutableMapValue mapValue = - ImmutableMapValue.create(ImmutableMap.of(one, hello)); - - assertThat(mapValue.value()).containsExactly(one, hello); - assertThat(mapValue.isZeroValue()).isFalse(); - } - - @Test - public void get_success() { - IntValue one = IntValue.create(1L); - StringValue hello = StringValue.create("hello"); - ImmutableMapValue mapValue = - ImmutableMapValue.create(ImmutableMap.of(one, hello)); - - assertThat(mapValue.get(one)).isEqualTo(hello); - } - - @Test - public void get_nonExistentKey_throws() { - IntValue one = IntValue.create(1L); - StringValue hello = StringValue.create("hello"); - ImmutableMapValue mapValue = - ImmutableMapValue.create(ImmutableMap.of(one, hello)); - - CelRuntimeException exception = - assertThrows(CelRuntimeException.class, () -> mapValue.get(IntValue.create(100L))); - assertThat(exception).hasMessageThat().contains("key '100' is not present in map."); - } - - @Test - @TestParameters("{key: 1, expectedResult: true}") - @TestParameters("{key: 100, expectedResult: false}") - public void find_success(long key, boolean expectedResult) { - IntValue one = IntValue.create(1L); - StringValue hello = StringValue.create("hello"); - ImmutableMapValue mapValue = - ImmutableMapValue.create(ImmutableMap.of(one, hello)); - - assertThat(mapValue.find(IntValue.create(key)).isPresent()).isEqualTo(expectedResult); - } - - @Test - public void mapValue_mixedTypes() { - IntValue one = IntValue.create(1L); - StringValue hello = StringValue.create("hello"); - - ImmutableMapValue mapValue = - ImmutableMapValue.create(ImmutableMap.of(one, hello, hello, one)); - - assertThat(mapValue.value()).containsExactly(one, hello, hello, one); - assertThat(mapValue.isZeroValue()).isFalse(); - } - - @Test - public void create_nullValue_throws() { - assertThrows(NullPointerException.class, () -> ImmutableMapValue.create(null)); - } - - @Test - public void celTypeTest() { - MapValue value = ImmutableMapValue.create(ImmutableMap.of()); - - assertThat(value.celType()).isEqualTo(MapType.create(SimpleType.DYN, SimpleType.DYN)); - } -} diff --git a/common/src/test/java/dev/cel/common/values/IntValueTest.java b/common/src/test/java/dev/cel/common/values/IntValueTest.java deleted file mode 100644 index fa2b15595..000000000 --- a/common/src/test/java/dev/cel/common/values/IntValueTest.java +++ /dev/null @@ -1,81 +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.common.values; - -import static com.google.common.truth.Truth.assertThat; - -import com.google.common.testing.ClassSanityTester; -import com.google.common.testing.EqualsTester; -import dev.cel.common.types.SimpleType; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public class IntValueTest { - - @Test - public void emptyInt() { - IntValue intValue = IntValue.create(0L); - - assertThat(intValue.value()).isEqualTo(0L); - assertThat(intValue.isZeroValue()).isTrue(); - } - - @Test - public void constructInt() { - IntValue uintValue = IntValue.create(5L); - - assertThat(uintValue.value()).isEqualTo(5L); - assertThat(uintValue.isZeroValue()).isFalse(); - } - - @Test - public void equalityTest() { - new EqualsTester() - .addEqualityGroup(IntValue.create(10)) - .addEqualityGroup(IntValue.create(0), IntValue.create(0)) - .addEqualityGroup(IntValue.create(15), IntValue.create(15)) - .addEqualityGroup(IntValue.create(Long.MAX_VALUE), IntValue.create(Long.MAX_VALUE)) - .addEqualityGroup(IntValue.create(Long.MIN_VALUE), IntValue.create(Long.MIN_VALUE)) - .testEquals(); - } - - @Test - public void sanityTest() throws Exception { - new ClassSanityTester() - .setDefault(IntValue.class, IntValue.create(100)) - .setDistinctValues(IntValue.class, IntValue.create(0), IntValue.create(100)) - .forAllPublicStaticMethods(IntValue.class) - .thatReturn(IntValue.class) - .testEquals() - .testNulls(); - } - - @Test - public void hashCode_smokeTest() { - assertThat(IntValue.create(0).hashCode()).isEqualTo(1000003); - assertThat(IntValue.create(100).hashCode()).isEqualTo(999975); - assertThat(IntValue.create(Long.MAX_VALUE).hashCode()).isEqualTo(-2146483645); - assertThat(IntValue.create(Long.MIN_VALUE).hashCode()).isEqualTo(-2146483645); - } - - @Test - public void celTypeTest() { - IntValue value = IntValue.create(0); - - assertThat(value.celType()).isEqualTo(SimpleType.INT); - } -} 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 6a991a6a9..f00954e3d 100644 --- a/common/src/test/java/dev/cel/common/values/OptionalValueTest.java +++ b/common/src/test/java/dev/cel/common/values/OptionalValueTest.java @@ -34,7 +34,7 @@ public class OptionalValueTest { @Test public void emptyOptional() { - OptionalValue optionalValue = OptionalValue.EMPTY; + OptionalValue optionalValue = OptionalValue.EMPTY; assertThat(optionalValue.isZeroValue()).isTrue(); NoSuchElementException exception = @@ -44,7 +44,7 @@ public void emptyOptional() { @Test public void optionalValue_selectEmpty() { - CelValue optionalValue = OptionalValue.EMPTY.select(StringValue.create("bogus")); + OptionalValue optionalValue = OptionalValue.EMPTY.select("bogus"); assertThat(optionalValue).isEqualTo(OptionalValue.EMPTY); assertThat(optionalValue.isZeroValue()).isTrue(); @@ -52,19 +52,18 @@ public void optionalValue_selectEmpty() { @Test public void optionalValue_construct() { - OptionalValue optionalValue = OptionalValue.create(IntValue.create(1L)); + OptionalValue optionalValue = OptionalValue.create(1L); - assertThat(optionalValue.value()).isEqualTo(IntValue.create(1L)); + assertThat(optionalValue.value()).isEqualTo(1L); assertThat(optionalValue.isZeroValue()).isFalse(); } @Test public void optSelectField_map_success() { - IntValue one = IntValue.create(1L); - StringValue hello = StringValue.create("hello"); - ImmutableMapValue mapValue = - ImmutableMapValue.create(ImmutableMap.of(one, hello)); - OptionalValue> optionalValueContainingMap = + Long one = 1L; + String hello = "hello"; + ImmutableMap mapValue = ImmutableMap.of(one, hello); + OptionalValue, Long> optionalValueContainingMap = OptionalValue.create(mapValue); assertThat(optionalValueContainingMap.select(one)).isEqualTo(OptionalValue.create(hello)); @@ -72,11 +71,10 @@ public void optSelectField_map_success() { @Test public void optSelectField_map_returnsEmpty() { - IntValue one = IntValue.create(1L); - StringValue hello = StringValue.create("hello"); - ImmutableMapValue mapValue = - ImmutableMapValue.create(ImmutableMap.of(one, hello)); - OptionalValue> optionalValueContainingMap = + Long one = 1L; + String hello = "hello"; + ImmutableMap mapValue = ImmutableMap.of(one, hello); + OptionalValue, Object> optionalValueContainingMap = OptionalValue.create(mapValue); assertThat(optionalValueContainingMap.select(NullValue.NULL_VALUE)) @@ -86,36 +84,32 @@ public void optSelectField_map_returnsEmpty() { @Test public void optSelectField_struct_success() { CelCustomStruct celCustomStruct = new CelCustomStruct(5L); - OptionalValue optionalValueContainingStruct = + OptionalValue optionalValueContainingStruct = OptionalValue.create(celCustomStruct); - assertThat(optionalValueContainingStruct.select(StringValue.create("data"))) - .isEqualTo(OptionalValue.create(IntValue.create(5L))); + assertThat(optionalValueContainingStruct.select("data")).isEqualTo(OptionalValue.create(5L)); } @Test public void optSelectField_struct_returnsEmpty() { CelCustomStruct celCustomStruct = new CelCustomStruct(5L); - OptionalValue optionalValueContainingStruct = + OptionalValue optionalValueContainingStruct = OptionalValue.create(celCustomStruct); - assertThat(optionalValueContainingStruct.select(StringValue.create("bogus"))) - .isEqualTo(OptionalValue.EMPTY); + assertThat(optionalValueContainingStruct.select("bogus")).isEqualTo(OptionalValue.EMPTY); } @Test @TestParameters("{key: 1, expectedResult: true}") @TestParameters("{key: 100, expectedResult: false}") public void findField_map_success(long key, boolean expectedResult) { - IntValue one = IntValue.create(1L); - StringValue hello = StringValue.create("hello"); - ImmutableMapValue mapValue = - ImmutableMapValue.create(ImmutableMap.of(one, hello)); - OptionalValue> optionalValueContainingMap = + Long one = 1L; + String hello = "hello"; + ImmutableMap mapValue = ImmutableMap.of(one, hello); + OptionalValue, Long> optionalValueContainingMap = OptionalValue.create(mapValue); - assertThat(optionalValueContainingMap.find(IntValue.create(key)).isPresent()) - .isEqualTo(expectedResult); + assertThat(optionalValueContainingMap.find(key).isPresent()).isEqualTo(expectedResult); } @Test @@ -123,16 +117,15 @@ public void findField_map_success(long key, boolean expectedResult) { @TestParameters("{field: 'bogus', expectedResult: false}") public void findField_struct_success(String field, boolean expectedResult) { CelCustomStruct celCustomStruct = new CelCustomStruct(5L); - OptionalValue optionalValueContainingStruct = + OptionalValue optionalValueContainingStruct = OptionalValue.create(celCustomStruct); - assertThat(optionalValueContainingStruct.find(StringValue.create(field)).isPresent()) - .isEqualTo(expectedResult); + assertThat(optionalValueContainingStruct.find(field).isPresent()).isEqualTo(expectedResult); } @Test public void findField_onEmptyOptional() { - assertThat(OptionalValue.EMPTY.find(StringValue.create("bogus"))).isEmpty(); + assertThat(OptionalValue.EMPTY.find("bogus")).isEmpty(); } @Test @@ -142,13 +135,13 @@ public void create_nullValue_throws() { @Test public void celTypeTest() { - OptionalValue value = OptionalValue.EMPTY; + OptionalValue value = OptionalValue.EMPTY; assertThat(value.celType()).isEqualTo(OptionalType.create(SimpleType.DYN)); } @SuppressWarnings("Immutable") // Test only - private static class CelCustomStruct extends StructValue { + private static class CelCustomStruct extends StructValue { private final long data; @Override @@ -167,14 +160,14 @@ public CelType celType() { } @Override - public CelValue select(StringValue field) { + public Long select(String field) { return find(field).get(); } @Override - public Optional find(StringValue field) { - if (field.value().equals("data")) { - return Optional.of(IntValue.create(value())); + public Optional find(String field) { + if (field.equals("data")) { + return Optional.of(value()); } return Optional.empty(); diff --git a/common/src/test/java/dev/cel/common/values/ProtoCelValueConverterTest.java b/common/src/test/java/dev/cel/common/values/ProtoCelValueConverterTest.java index 2c1e92e1d..17c012db7 100644 --- a/common/src/test/java/dev/cel/common/values/ProtoCelValueConverterTest.java +++ b/common/src/test/java/dev/cel/common/values/ProtoCelValueConverterTest.java @@ -16,16 +16,10 @@ import static com.google.common.truth.Truth.assertThat; -import com.google.protobuf.ByteString; -import com.google.protobuf.Duration; -import com.google.protobuf.Timestamp; -import com.google.protobuf.util.Durations; -import com.google.protobuf.util.Timestamps; import dev.cel.common.CelOptions; import dev.cel.common.internal.DefaultDescriptorPool; import dev.cel.common.internal.DefaultMessageFactory; import dev.cel.common.internal.DynamicProto; -import java.time.Instant; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -35,46 +29,17 @@ public class ProtoCelValueConverterTest { private static final ProtoCelValueConverter PROTO_CEL_VALUE_CONVERTER = ProtoCelValueConverter.newInstance( - CelOptions.DEFAULT, DefaultDescriptorPool.INSTANCE, - DynamicProto.create(DefaultMessageFactory.INSTANCE)); + DynamicProto.create(DefaultMessageFactory.INSTANCE), + CelOptions.DEFAULT); @Test - public void fromCelValueToJavaObject_returnsTimestampValue() { - Timestamp timestamp = - (Timestamp) - PROTO_CEL_VALUE_CONVERTER.fromCelValueToJavaObject( - TimestampValue.create(Instant.ofEpochSecond(50))); + public void unwrap_nullValue() { + NullValue nullValue = (NullValue) PROTO_CEL_VALUE_CONVERTER.maybeUnwrap(NullValue.NULL_VALUE); - assertThat(timestamp).isEqualTo(Timestamps.fromSeconds(50)); - } - - @Test - public void fromCelValueToJavaObject_returnsDurationValue() { - Duration duration = - (Duration) - PROTO_CEL_VALUE_CONVERTER.fromCelValueToJavaObject( - DurationValue.create(java.time.Duration.ofSeconds(10))); - - assertThat(duration).isEqualTo(Durations.fromSeconds(10)); - } - - @Test - public void fromCelValueToJavaObject_returnsBytesValue() { - ByteString byteString = - (ByteString) - PROTO_CEL_VALUE_CONVERTER.fromCelValueToJavaObject( - BytesValue.create(CelByteString.of(new byte[] {0x1, 0x5, 0xc}))); - - assertThat(byteString).isEqualTo(ByteString.copyFrom(new byte[] {0x1, 0x5, 0xc})); - } - - @Test - public void fromCelValueToJavaObject_returnsProtobufNullValue() { - com.google.protobuf.NullValue nullValue = - (com.google.protobuf.NullValue) - PROTO_CEL_VALUE_CONVERTER.fromCelValueToJavaObject(NullValue.NULL_VALUE); - - assertThat(nullValue).isEqualTo(com.google.protobuf.NullValue.NULL_VALUE); + // Note: No conversion is attempted. We're using dev.cel.common.values.NullValue.NULL_VALUE as + // the + // sentinel type for CEL's `null`. + assertThat(nullValue).isEqualTo(NullValue.NULL_VALUE); } } diff --git a/common/src/test/java/dev/cel/common/values/ProtoLiteCelValueConverterTest.java b/common/src/test/java/dev/cel/common/values/ProtoLiteCelValueConverterTest.java new file mode 100644 index 000000000..3b66171e4 --- /dev/null +++ b/common/src/test/java/dev/cel/common/values/ProtoLiteCelValueConverterTest.java @@ -0,0 +1,329 @@ +// 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.values; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Multimap; +import com.google.common.primitives.UnsignedLong; +import com.google.protobuf.BoolValue; +import com.google.protobuf.ByteString; +import com.google.protobuf.BytesValue; +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; +import com.google.protobuf.MessageLite; +import com.google.protobuf.StringValue; +import com.google.protobuf.TextFormat; +import com.google.protobuf.Timestamp; +import com.google.protobuf.UInt32Value; +import com.google.protobuf.UInt64Value; +import com.google.testing.junit.testparameterinjector.TestParameter; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.common.internal.CelLiteDescriptorPool; +import dev.cel.common.internal.DefaultLiteDescriptorPool; +import dev.cel.common.values.ProtoLiteCelValueConverter.MessageFields; +import dev.cel.expr.conformance.proto3.TestAllTypes; +import dev.cel.expr.conformance.proto3.TestAllTypesCelDescriptor; +import java.time.Instant; +import java.util.LinkedHashMap; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public class ProtoLiteCelValueConverterTest { + private static final CelLiteDescriptorPool DESCRIPTOR_POOL = + DefaultLiteDescriptorPool.newInstance( + ImmutableSet.of(TestAllTypesCelDescriptor.getDescriptor())); + + private static final ProtoLiteCelValueConverter PROTO_LITE_CEL_VALUE_CONVERTER = + ProtoLiteCelValueConverter.newInstance(DESCRIPTOR_POOL); + + @Test + public void + fromProtoMessageToCelValue_withTestMessage_convertsToProtoMessageLiteValueFromProtoMessage() { + ProtoMessageLiteValue protoMessageLiteValue = + (ProtoMessageLiteValue) + PROTO_LITE_CEL_VALUE_CONVERTER.toRuntimeValue(TestAllTypes.getDefaultInstance()); + + assertThat(protoMessageLiteValue.value()).isEqualTo(TestAllTypes.getDefaultInstance()); + } + + @SuppressWarnings("ImmutableEnumChecker") // Test only + private enum WellKnownProtoTestCase { + BOOL(BoolValue.of(true), true), + BYTES(BytesValue.of(ByteString.copyFromUtf8("test")), CelByteString.copyFromUtf8("test")), + FLOAT(FloatValue.of(1.0f), 1.0d), + DOUBLE(DoubleValue.of(1.0), 1.0d), + INT32(Int32Value.of(1), 1L), + INT64(Int64Value.of(1L), 1L), + STRING(StringValue.of("test"), "test"), + + DURATION( + Duration.newBuilder().setSeconds(10).setNanos(50).build(), + java.time.Duration.ofSeconds(10, 50)), + TIMESTAMP( + Timestamp.newBuilder().setSeconds(1678886400L).setNanos(123000000).build(), + Instant.ofEpochSecond(1678886400L, 123000000)), + UINT32(UInt32Value.of(1), UnsignedLong.valueOf(1)), + UINT64(UInt64Value.of(1L), UnsignedLong.valueOf(1L)), + ; + + private final MessageLite msg; + private final Object value; + + WellKnownProtoTestCase(MessageLite msg, Object value) { + this.msg = msg; + this.value = value; + } + } + + @Test + public void fromProtoMessageToCelValue_withWellKnownProto_convertsToPrimitivesFromProtoMessage( + @TestParameter WellKnownProtoTestCase testCase) { + Object adaptedValue = PROTO_LITE_CEL_VALUE_CONVERTER.toRuntimeValue(testCase.msg); + + 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 { + PACKED(new byte[] {(byte) 0x82, 0x2, 0x3, 0x1, 0x2, 0x3}), + NON_PACKED(new byte[] {(byte) 0x80, 0x2, 0x1, (byte) 0x80, 0x2, 0x2, (byte) 0x80, 0x2, 0x3}), + // 1L is not packed, but 2L and 3L are + MIXED(new byte[] {(byte) 0x80, 0x2, 0x1, (byte) 0x82, 0x2, 0x2, 0x2, 0x3}); + + private final byte[] bytes; + + RepeatedFieldBytesTestCase(byte[] bytes) { + this.bytes = bytes; + } + } + + @Test + public void readAllFields_repeatedFields_packedBytesCombinations( + @TestParameter RepeatedFieldBytesTestCase testCase) throws Exception { + MessageFields fields = + PROTO_LITE_CEL_VALUE_CONVERTER.readAllFields( + testCase.bytes, "cel.expr.conformance.proto3.TestAllTypes"); + + assertThat(fields.values()).containsExactly("repeated_int64", ImmutableList.of(1L, 2L, 3L)); + } + + /** + * Unknown test with the following hypothetical fields: + * + *
{@code
+   * message TestAllTypes {
+   *   int64 single_int64_unknown = 2500;
+   *   fixed32 single_fixed32_unknown = 2501;
+   *   fixed64 single_fixed64_unknown = 2502;
+   *   string single_string_unknown = 2503;
+   *   repeated int64 repeated_int64_unknown = 2504;
+   *   map map_string_int64_unknown = 2505;
+   * }
+   * }
+ */ + @SuppressWarnings("ImmutableEnumChecker") // Test only + private enum UnknownFieldsTestCase { + INT64(new byte[] {-96, -100, 1, 1}, "2500: 1", ImmutableListMultimap.of(2500, 1L)), + FIXED32( + new byte[] {-83, -100, 1, 2, 0, 0, 0}, + "2501: 0x00000002", + ImmutableListMultimap.of(2501, 2)), + FIXED64( + new byte[] {-79, -100, 1, 3, 0, 0, 0, 0, 0, 0, 0}, + "2502: 0x0000000000000003", + ImmutableListMultimap.of(2502, 3L)), + STRING( + new byte[] {-70, -100, 1, 11, 72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100}, + "2503: \"Hello world\"", + ImmutableListMultimap.of(2503, ByteString.copyFromUtf8("Hello world"))), + REPEATED_INT64( + new byte[] {-62, -100, 1, 2, 4, 5}, + "2504: \"\\004\\005\"", + ImmutableListMultimap.of(2504, ByteString.copyFrom(new byte[] {4, 5}))), + MAP_STRING_INT64( + new byte[] { + -54, -100, 1, 7, 10, 3, 102, 111, 111, 16, 4, -54, -100, 1, 7, 10, 3, 98, 97, 114, 16, 5 + }, + "2505: {\n" + + " 1: \"foo\"\n" + + " 2: 4\n" + + "}\n" + + "2505: {\n" + + " 1: \"bar\"\n" + + " 2: 5\n" + + "}", + ImmutableListMultimap.of( + 2505, + ByteString.copyFromUtf8("\n\003foo\020\004"), + 2505, + ByteString.copyFromUtf8("\n\003bar\020\005"))); + + private final byte[] bytes; + private final String formattedOutput; + private final Multimap unknownMap; + + UnknownFieldsTestCase( + byte[] bytes, String formattedOutput, Multimap unknownMap) { + this.bytes = bytes; + this.formattedOutput = formattedOutput; + this.unknownMap = unknownMap; + } + } + + @Test + public void unknowns_repeatedEncodedBytes_allRecordsKeptWithKeysSorted() throws Exception { + // 2500: 2 + // 2504: \"\\004\\005\"" + // 2501: 0x00000002 + // 2500: 1 + byte[] bytes = + new byte[] { + -96, -100, 1, 2, // keep + -62, -100, 1, 2, 4, 5, // keep + -83, -100, 1, 2, 0, 0, 0, // keep + -96, -100, 1, 1 // keep + }; + + MessageFields messageFields = + PROTO_LITE_CEL_VALUE_CONVERTER.readAllFields( + bytes, "cel.expr.conformance.proto3.TestAllTypes"); + + assertThat(messageFields.values()).isEmpty(); + assertThat(messageFields.unknowns()) + .containsExactly( + 2500, 2L, 2500, 1L, 2501, 2, 2504, ByteString.copyFrom(new byte[] {0x04, 0x05})) + .inOrder(); + } + + @Test + public void readAllFields_unknownFields(@TestParameter UnknownFieldsTestCase testCase) + throws Exception { + TestAllTypes parsedMsg = + TestAllTypes.parseFrom(testCase.bytes, ExtensionRegistryLite.getEmptyRegistry()); + + MessageFields messageFields = + PROTO_LITE_CEL_VALUE_CONVERTER.readAllFields( + testCase.bytes, "cel.expr.conformance.proto3.TestAllTypes"); + + assertThat(messageFields.values()).isEmpty(); + assertThat(messageFields.unknowns()).containsExactlyEntriesIn(testCase.unknownMap).inOrder(); + assertThat(TextFormat.printer().printToString(parsedMsg).trim()) + .isEqualTo(testCase.formattedOutput); + } + + /** + * Tests the following message: + * + *
{@code
+   * TestAllTypes.newBuilder()
+   *     // Unknowns
+   *     .setSingleInt64Unknown(1L)
+   *     .setSingleFixed32Unknown(2)
+   *     .setSingleFixed64Unknown(3L)
+   *     .setSingleStringUnknown("Hello world")
+   *     .addAllRepeatedInt64Unknown(ImmutableList.of(4L, 5L))
+   *     .putMapStringInt64Unknown("foo", 4L)
+   *     .putMapStringInt64Unknown("bar", 5L)
+   *     // Known values
+   *     .putMapBoolDouble(true, 1.5d)
+   *     .putMapBoolDouble(false, 2.5d)
+   *     .build();
+   * }
+ */ + @Test + @SuppressWarnings("unchecked") + public void readAllFields_unknownFieldsWithValues() throws Exception { + byte[] unknownMessageBytes = { + -70, 4, 11, 8, 1, 17, 0, 0, 0, 0, 0, 0, -8, 63, -70, 4, 11, 8, 0, 17, 0, 0, 0, 0, 0, 0, 4, 64, + -96, -100, 1, 1, -83, -100, 1, 2, 0, 0, 0, -79, -100, 1, 3, 0, 0, 0, 0, 0, 0, 0, -70, -100, 1, + 11, 72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, -62, -100, 1, 2, 4, 5, -54, -100, 1, + 7, 10, 3, 102, 111, 111, 16, 4, -54, -100, 1, 7, 10, 3, 98, 97, 114, 16, 5 + }; + TestAllTypes parsedMsg = + TestAllTypes.parseFrom(unknownMessageBytes, ExtensionRegistryLite.getEmptyRegistry()); + + MessageFields fields = + PROTO_LITE_CEL_VALUE_CONVERTER.readAllFields( + unknownMessageBytes, "cel.expr.conformance.proto3.TestAllTypes"); + + assertThat(TextFormat.printer().printToString(parsedMsg)) + .isEqualTo( + "map_bool_double {\n" + + " key: false\n" + + " value: 2.5\n" + + "}\n" + + "map_bool_double {\n" + + " key: true\n" + + " value: 1.5\n" + + "}\n" + + "2500: 1\n" + + "2501: 0x00000002\n" + + "2502: 0x0000000000000003\n" + + "2503: \"Hello world\"\n" + + "2504: \"\\004\\005\"\n" + + "2505: {\n" + + " 1: \"foo\"\n" + + " 2: 4\n" + + "}\n" + + "2505: {\n" + + " 1: \"bar\"\n" + + " 2: 5\n" + + "}\n"); + assertThat(fields.values()).containsKey("map_bool_double"); + LinkedHashMap mapBoolDoubleValues = + (LinkedHashMap) fields.values().get("map_bool_double"); + assertThat(mapBoolDoubleValues).containsExactly(true, 1.5d, false, 2.5d).inOrder(); + Multimap unknownValues = fields.unknowns(); + assertThat(unknownValues) + .containsExactly( + 2500, + 1L, + 2501, + 2, + 2502, + 3L, + 2503, + ByteString.copyFromUtf8("Hello world"), + 2504, + ByteString.copyFrom(new byte[] {0x04, 0x05}), + 2505, + ByteString.copyFromUtf8("\n\003foo\020\004"), + 2505, + ByteString.copyFromUtf8("\n\003bar\020\005")) + .inOrder(); + } +} diff --git a/common/src/test/java/dev/cel/common/values/ProtoMessageLiteValueProviderTest.java b/common/src/test/java/dev/cel/common/values/ProtoMessageLiteValueProviderTest.java new file mode 100644 index 000000000..bbe116c80 --- /dev/null +++ b/common/src/test/java/dev/cel/common/values/ProtoMessageLiteValueProviderTest.java @@ -0,0 +1,54 @@ +// 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.values; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableMap; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.common.types.StructTypeReference; +import dev.cel.expr.conformance.proto3.TestAllTypes; +import dev.cel.expr.conformance.proto3.TestAllTypesCelDescriptor; +import java.util.Optional; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public class ProtoMessageLiteValueProviderTest { + private static final ProtoMessageLiteValueProvider VALUE_PROVIDER = + ProtoMessageLiteValueProvider.newInstance(TestAllTypesCelDescriptor.getDescriptor()); + + @Test + public void newValue_unknownType_returnsEmpty() { + assertThat(VALUE_PROVIDER.newValue("unknownType", ImmutableMap.of())).isEmpty(); + } + + @Test + public void newValue_emptyFields_success() { + Optional value = + VALUE_PROVIDER.newValue("cel.expr.conformance.proto3.TestAllTypes", ImmutableMap.of()); + ProtoMessageLiteValue protoMessageLiteValue = (ProtoMessageLiteValue) value.get(); + + assertThat(protoMessageLiteValue.value()).isEqualTo(TestAllTypes.getDefaultInstance()); + assertThat(protoMessageLiteValue.isZeroValue()).isTrue(); + assertThat(protoMessageLiteValue.celType()) + .isEqualTo(StructTypeReference.create("cel.expr.conformance.proto3.TestAllTypes")); + } + + @Test + public void getProtoLiteCelValueConverter() { + assertThat(VALUE_PROVIDER.protoCelValueConverter()).isNotNull(); + } +} diff --git a/common/src/test/java/dev/cel/common/values/ProtoMessageLiteValueTest.java b/common/src/test/java/dev/cel/common/values/ProtoMessageLiteValueTest.java new file mode 100644 index 000000000..dbfb55cf9 --- /dev/null +++ b/common/src/test/java/dev/cel/common/values/ProtoMessageLiteValueTest.java @@ -0,0 +1,256 @@ +// 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.values; + +import static com.google.common.truth.Truth.assertThat; + +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.Any; +import com.google.protobuf.ByteString; +import com.google.protobuf.DynamicMessage; +import com.google.protobuf.FloatValue; +import com.google.protobuf.Int32Value; +import com.google.protobuf.Int64Value; +import com.google.protobuf.Timestamp; +import com.google.protobuf.UInt32Value; +import com.google.protobuf.UInt64Value; +import com.google.testing.junit.testparameterinjector.TestParameter; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.common.internal.CelLiteDescriptorPool; +import dev.cel.common.internal.DefaultLiteDescriptorPool; +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.expr.conformance.proto3.TestAllTypesCelDescriptor; +import java.time.Duration; +import java.time.Instant; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public class ProtoMessageLiteValueTest { + private static final CelLiteDescriptorPool DESCRIPTOR_POOL = + DefaultLiteDescriptorPool.newInstance( + ImmutableSet.of(TestAllTypesCelDescriptor.getDescriptor())); + + private static final ProtoLiteCelValueConverter PROTO_LITE_CEL_VALUE_CONVERTER = + ProtoLiteCelValueConverter.newInstance(DESCRIPTOR_POOL); + + @Test + public void create_withEmptyMessage() { + ProtoMessageLiteValue messageLiteValue = + ProtoMessageLiteValue.create( + TestAllTypes.getDefaultInstance(), + "cel.expr.conformance.proto3.TestAllTypes", + PROTO_LITE_CEL_VALUE_CONVERTER); + + assertThat(messageLiteValue.value()).isEqualTo(TestAllTypes.getDefaultInstance()); + assertThat(messageLiteValue.isZeroValue()).isTrue(); + } + + @Test + public void create_withPopulatedMessage() { + ProtoMessageLiteValue messageLiteValue = + ProtoMessageLiteValue.create( + TestAllTypes.newBuilder().setSingleInt64(1L).build(), + "cel.expr.conformance.proto3.TestAllTypes", + PROTO_LITE_CEL_VALUE_CONVERTER); + + assertThat(messageLiteValue.value()) + .isEqualTo(TestAllTypes.newBuilder().setSingleInt64(1L).build()); + assertThat(messageLiteValue.isZeroValue()).isFalse(); + } + + @SuppressWarnings("ImmutableEnumChecker") // Test only + private enum SelectFieldTestCase { + BOOL("single_bool", true), + INT32("single_int32", 4L), + INT64("single_int64", 5L), + SINT32("single_sint32", 1L), + SINT64("single_sint64", 2L), + UINT32("single_uint32", UnsignedLong.valueOf(1L)), + UINT64("single_uint64", UnsignedLong.MAX_VALUE), + FLOAT("single_float", 1.5d), + DOUBLE("single_double", 2.5d), + FIXED32("single_fixed32", 20), + SFIXED32("single_sfixed32", 30), + FIXED64("single_fixed64", 40), + SFIXED64("single_sfixed64", 50), + STRING("single_string", "test"), + BYTES("single_bytes", CelByteString.of(new byte[] {0x01})), + DURATION("single_duration", Duration.ofSeconds(100)), + TIMESTAMP("single_timestamp", Instant.ofEpochSecond(100)), + INT32_WRAPPER("single_int32_wrapper", 5L), + INT64_WRAPPER("single_int64_wrapper", 10L), + UINT32_WRAPPER("single_uint32_wrapper", UnsignedLong.valueOf(1L)), + UINT64_WRAPPER("single_uint64_wrapper", UnsignedLong.MAX_VALUE), + FLOAT_WRAPPER("single_float_wrapper", 7.5d), + DOUBLE_WRAPPER("single_double_wrapper", 8.5d), + STRING_WRAPPER("single_string_wrapper", "hello"), + BYTES_WRAPPER("single_bytes_wrapper", CelByteString.of(new byte[] {0x02})), + REPEATED_INT64("repeated_int64", ImmutableList.of(5L, 6L)), + REPEATED_UINT64( + "repeated_uint64", ImmutableList.of(UnsignedLong.valueOf(7L), UnsignedLong.valueOf(8L))), + REPEATED_FLOAT("repeated_float", ImmutableList.of(1.5d, 2.5d)), + REPEATED_DOUBLE("repeated_double", ImmutableList.of(3.5d, 4.5d)), + + REPEATED_STRING("repeated_string", ImmutableList.of("foo", "bar")), + + MAP_INT64_INT64("map_int64_int64", ImmutableMap.of(1L, 2L, 3L, 4L)), + + MAP_UINT32_UINT64( + "map_uint32_uint64", + ImmutableMap.of( + UnsignedLong.valueOf(5L), + UnsignedLong.valueOf(6L), + UnsignedLong.valueOf(7L), + UnsignedLong.valueOf(8L))), + + MAP_STRING_STRING("map_string_string", ImmutableMap.of("a", "b")), + + NESTED_ENUM("standalone_enum", 1L); + + private final String fieldName; + private final Object value; + + SelectFieldTestCase(String fieldName, Object value) { + this.fieldName = fieldName; + this.value = value; + } + } + + @Test + public void selectField_success(@TestParameter SelectFieldTestCase testCase) { + TestAllTypes testAllTypes = + TestAllTypes.newBuilder() + .setSingleBool(true) + .setSingleInt32(4) + .setSingleInt64(5L) + .setSingleSint32(1) + .setSingleSint64(2L) + .setSingleUint32(1) + .setSingleUint64(UnsignedLong.MAX_VALUE.longValue()) + .setSingleFixed32(20) + .setSingleSfixed32(30) + .setSingleFixed64(40) + .setSingleSfixed64(50) + .setSingleFloat(1.5f) + .setSingleDouble(2.5d) + .setSingleString("test") + .setSingleBytes(ByteString.copyFrom(new byte[] {0x01})) + .setSingleAny( + Any.pack(DynamicMessage.newBuilder(com.google.protobuf.BoolValue.of(true)).build())) + .setSingleDuration(com.google.protobuf.Duration.newBuilder().setSeconds(100)) + .setSingleTimestamp(Timestamp.newBuilder().setSeconds(100)) + .setSingleInt32Wrapper(Int32Value.of(5)) + .setSingleInt64Wrapper(Int64Value.of(10L)) + .setSingleUint32Wrapper(UInt32Value.of(1)) + .setSingleUint64Wrapper(UInt64Value.of(UnsignedLong.MAX_VALUE.longValue())) + .setSingleStringWrapper(com.google.protobuf.StringValue.of("hello")) + .setSingleFloatWrapper(FloatValue.of(7.5f)) + .setSingleDoubleWrapper(com.google.protobuf.DoubleValue.of(8.5d)) + .setSingleBytesWrapper( + com.google.protobuf.BytesValue.of(ByteString.copyFrom(new byte[] {0x02}))) + .addRepeatedInt64(5L) + .addRepeatedInt64(6L) + .addRepeatedUint64(7L) + .addRepeatedUint64(8L) + .addRepeatedFloat(1.5f) + .addRepeatedFloat(2.5f) + .addRepeatedDouble(3.5d) + .addRepeatedDouble(4.5d) + .addRepeatedString("foo") + .addRepeatedString("bar") + .putMapStringString("a", "b") + .putMapInt64Int64(1L, 2L) + .putMapInt64Int64(3L, 4L) + .putMapUint32Uint64(5, 6L) + .putMapUint32Uint64(7, 8L) + .setStandaloneMessage(NestedMessage.getDefaultInstance()) + .setStandaloneEnum(NestedEnum.BAR) + .build(); + ProtoMessageLiteValue protoMessageValue = + ProtoMessageLiteValue.create( + testAllTypes, + "cel.expr.conformance.proto3.TestAllTypes", + PROTO_LITE_CEL_VALUE_CONVERTER); + + Object selectedValue = protoMessageValue.select(testCase.fieldName); + + assertThat(selectedValue).isEqualTo(testCase.value); + } + + @SuppressWarnings("ImmutableEnumChecker") // Test only + private enum DefaultValueTestCase { + BOOL("single_bool", false), + INT32("single_int32", 0L), + INT64("single_int64", 0L), + SINT32("single_sint32", 0L), + SINT64("single_sint64", 0L), + UINT32("single_uint32", UnsignedLong.ZERO), + UINT64("single_uint64", UnsignedLong.ZERO), + FIXED32("single_fixed32", 0), + SFIXED32("single_sfixed32", 0), + FIXED64("single_fixed64", 0), + SFIXED64("single_sfixed64", 0), + FLOAT("single_float", 0d), + DOUBLE("single_double", 0d), + STRING("single_string", ""), + BYTES("single_bytes", CelByteString.EMPTY), + DURATION("single_duration", Duration.ZERO), + TIMESTAMP("single_timestamp", Instant.EPOCH), + INT32_WRAPPER("single_int32_wrapper", NullValue.NULL_VALUE), + INT64_WRAPPER("single_int64_wrapper", NullValue.NULL_VALUE), + UINT32_WRAPPER("single_uint32_wrapper", NullValue.NULL_VALUE), + UINT64_WRAPPER("single_uint64_wrapper", NullValue.NULL_VALUE), + FLOAT_WRAPPER("single_float_wrapper", NullValue.NULL_VALUE), + DOUBLE_WRAPPER("single_double_wrapper", NullValue.NULL_VALUE), + STRING_WRAPPER("single_string_wrapper", NullValue.NULL_VALUE), + BYTES_WRAPPER("single_bytes_wrapper", NullValue.NULL_VALUE), + REPEATED_INT64("repeated_int64", ImmutableList.of()), + MAP_INT64_INT64("map_int64_int64", ImmutableMap.of()), + NESTED_ENUM("standalone_enum", 0L), + NESTED_MESSAGE( + "single_nested_message", + ProtoMessageLiteValue.create( + NestedMessage.getDefaultInstance(), + "cel.expr.conformance.proto3.TestAllTypes.NestedMessage", + PROTO_LITE_CEL_VALUE_CONVERTER)); + + private final String fieldName; + private final Object value; + + DefaultValueTestCase(String fieldName, Object value) { + this.fieldName = fieldName; + this.value = value; + } + } + + @Test + public void selectField_defaultValue(@TestParameter DefaultValueTestCase testCase) { + ProtoMessageLiteValue protoMessageValue = + ProtoMessageLiteValue.create( + TestAllTypes.getDefaultInstance(), + "cel.expr.conformance.proto3.TestAllTypes", + PROTO_LITE_CEL_VALUE_CONVERTER); + + Object selectedValue = protoMessageValue.select(testCase.fieldName); + + assertThat(selectedValue).isEqualTo(testCase.value); + } +} diff --git a/common/src/test/java/dev/cel/common/values/ProtoMessageValueProviderTest.java b/common/src/test/java/dev/cel/common/values/ProtoMessageValueProviderTest.java index de08149f0..ed8fc7b2a 100644 --- a/common/src/test/java/dev/cel/common/values/ProtoMessageValueProviderTest.java +++ b/common/src/test/java/dev/cel/common/values/ProtoMessageValueProviderTest.java @@ -20,21 +20,16 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.primitives.UnsignedLong; -import com.google.protobuf.util.Durations; -import com.google.protobuf.util.Timestamps; import com.google.testing.junit.testparameterinjector.TestParameterInjector; import dev.cel.common.CelDescriptorUtil; import dev.cel.common.CelOptions; -import dev.cel.common.CelRuntimeException; import dev.cel.common.internal.CelDescriptorPool; import dev.cel.common.internal.DefaultDescriptorPool; import dev.cel.common.internal.DefaultMessageFactory; import dev.cel.common.internal.DynamicProto; import dev.cel.common.internal.ProtoMessageFactory; -import dev.cel.common.values.CelValueProvider.CombinedCelValueProvider; -import dev.cel.testing.testdata.proto2.MessagesProto2Extensions; -import dev.cel.testing.testdata.proto2.Proto2Message; -import dev.cel.testing.testdata.proto2.TestAllTypesProto.TestAllTypes; +import dev.cel.expr.conformance.proto2.TestAllTypes; +import dev.cel.expr.conformance.proto2.TestAllTypesExtensions; import java.time.Duration; import java.time.Instant; import java.util.Optional; @@ -48,17 +43,15 @@ public class ProtoMessageValueProviderTest { DefaultDescriptorPool.create( CelDescriptorUtil.getAllDescriptorsFromFileDescriptor( ImmutableList.of( - TestAllTypes.getDescriptor().getFile(), - MessagesProto2Extensions.getDescriptor()))); + TestAllTypes.getDescriptor().getFile(), TestAllTypesExtensions.getDescriptor()))); private static final ProtoMessageFactory MESSAGE_FACTORY = DefaultMessageFactory.create(DESCRIPTOR_POOL); - private static final DynamicProto DYNAMIC_PROTO = DynamicProto.create(MESSAGE_FACTORY); @Test public void newValue_createEmptyProtoMessage() { ProtoMessageValueProvider protoMessageValueProvider = - ProtoMessageValueProvider.newInstance(DYNAMIC_PROTO, CelOptions.DEFAULT); + ProtoMessageValueProvider.newInstance(CelOptions.DEFAULT, DYNAMIC_PROTO); ProtoMessageValue protoMessageValue = (ProtoMessageValue) @@ -72,7 +65,7 @@ public void newValue_createEmptyProtoMessage() { @Test public void newValue_createProtoMessage_fieldsPopulated() { ProtoMessageValueProvider protoMessageValueProvider = - ProtoMessageValueProvider.newInstance(DYNAMIC_PROTO, CelOptions.DEFAULT); + ProtoMessageValueProvider.newInstance(CelOptions.current().build(), DYNAMIC_PROTO); ProtoMessageValue protoMessageValue = (ProtoMessageValue) @@ -95,37 +88,27 @@ public void newValue_createProtoMessage_fieldsPopulated() { "single_string", "hello", "single_timestamp", - Timestamps.fromSeconds(50), + Instant.ofEpochSecond(50), "single_duration", - Durations.fromSeconds(100))) + Duration.ofSeconds(100))) .get(); assertThat(protoMessageValue.isZeroValue()).isFalse(); - assertThat(protoMessageValue.select(StringValue.create("single_int32"))) - .isEqualTo(IntValue.create(1L)); - assertThat(protoMessageValue.select(StringValue.create("single_int64"))) - .isEqualTo(IntValue.create(2L)); - assertThat(protoMessageValue.select(StringValue.create("single_uint32"))) - .isEqualTo(UintValue.create(3L, false)); - assertThat(protoMessageValue.select(StringValue.create("single_uint64"))) - .isEqualTo(UintValue.create(4L, false)); - assertThat(protoMessageValue.select(StringValue.create("single_double"))) - .isEqualTo(DoubleValue.create(5.5d)); - assertThat(protoMessageValue.select(StringValue.create("single_bool"))) - .isEqualTo(BoolValue.create(true)); - assertThat(protoMessageValue.select(StringValue.create("single_string"))) - .isEqualTo(StringValue.create("hello")); - assertThat(protoMessageValue.select(StringValue.create("single_timestamp"))) - .isEqualTo(TimestampValue.create(Instant.ofEpochSecond(50))); - assertThat(protoMessageValue.select(StringValue.create("single_duration"))) - .isEqualTo(DurationValue.create(Duration.ofSeconds(100))); + assertThat(protoMessageValue.select("single_int32")).isEqualTo(1L); + assertThat(protoMessageValue.select("single_int64")).isEqualTo(2L); + assertThat(protoMessageValue.select("single_uint32")).isEqualTo(UnsignedLong.valueOf(3L)); + assertThat(protoMessageValue.select("single_uint64")).isEqualTo(UnsignedLong.valueOf(4L)); + assertThat(protoMessageValue.select("single_double")).isEqualTo(5.5d); + assertThat(protoMessageValue.select("single_bool")).isEqualTo(true); + assertThat(protoMessageValue.select("single_string")).isEqualTo("hello"); + assertThat(protoMessageValue.select("single_timestamp")).isEqualTo(Instant.ofEpochSecond(50)); + assertThat(protoMessageValue.select("single_duration")).isEqualTo(Duration.ofSeconds(100)); } @Test public void newValue_createProtoMessage_unsignedLongFieldsPopulated() { ProtoMessageValueProvider protoMessageValueProvider = - ProtoMessageValueProvider.newInstance( - DYNAMIC_PROTO, CelOptions.current().enableUnsignedLongs(true).build()); + ProtoMessageValueProvider.newInstance(CelOptions.DEFAULT, DYNAMIC_PROTO); ProtoMessageValue protoMessageValue = (ProtoMessageValue) @@ -137,16 +120,14 @@ public void newValue_createProtoMessage_unsignedLongFieldsPopulated() { .get(); assertThat(protoMessageValue.isZeroValue()).isFalse(); - assertThat(protoMessageValue.select(StringValue.create("single_uint32")).value()) - .isEqualTo(UnsignedLong.valueOf(3L)); - assertThat(protoMessageValue.select(StringValue.create("single_uint64")).value()) - .isEqualTo(UnsignedLong.MAX_VALUE); + assertThat(protoMessageValue.select("single_uint32")).isEqualTo(UnsignedLong.valueOf(3L)); + assertThat(protoMessageValue.select("single_uint64")).isEqualTo(UnsignedLong.MAX_VALUE); } @Test public void newValue_createProtoMessage_wrappersPopulated() { ProtoMessageValueProvider protoMessageValueProvider = - ProtoMessageValueProvider.newInstance(DYNAMIC_PROTO, CelOptions.DEFAULT); + ProtoMessageValueProvider.newInstance(CelOptions.DEFAULT, DYNAMIC_PROTO); ProtoMessageValue protoMessageValue = (ProtoMessageValue) @@ -171,64 +152,47 @@ public void newValue_createProtoMessage_wrappersPopulated() { .get(); assertThat(protoMessageValue.isZeroValue()).isFalse(); - assertThat(protoMessageValue.select(StringValue.create("single_int32_wrapper")).value()) - .isEqualTo(1L); - assertThat(protoMessageValue.select(StringValue.create("single_int32_wrapper")).value()) - .isEqualTo(1L); - assertThat(protoMessageValue.select(StringValue.create("single_int64_wrapper")).value()) - .isEqualTo(2L); - assertThat(protoMessageValue.select(StringValue.create("single_uint32_wrapper")).value()) - .isEqualTo(3L); - assertThat(protoMessageValue.select(StringValue.create("single_uint64_wrapper")).value()) - .isEqualTo(4L); - assertThat(protoMessageValue.select(StringValue.create("single_double_wrapper")).value()) - .isEqualTo(5.5d); - assertThat(protoMessageValue.select(StringValue.create("single_bool_wrapper")).value()) - .isEqualTo(true); - assertThat(protoMessageValue.select(StringValue.create("single_string_wrapper")).value()) - .isEqualTo("hello"); + assertThat(protoMessageValue.select("single_int32_wrapper")).isEqualTo(1L); + assertThat(protoMessageValue.select("single_int32_wrapper")).isEqualTo(1L); + assertThat(protoMessageValue.select("single_int64_wrapper")).isEqualTo(2L); + assertThat(protoMessageValue.select("single_uint32_wrapper")) + .isEqualTo(UnsignedLong.valueOf(3L)); + assertThat(protoMessageValue.select("single_uint64_wrapper")) + .isEqualTo(UnsignedLong.valueOf(4L)); + assertThat(protoMessageValue.select("single_double_wrapper")).isEqualTo(5.5d); + assertThat(protoMessageValue.select("single_bool_wrapper")).isEqualTo(true); + assertThat(protoMessageValue.select("single_string_wrapper")).isEqualTo("hello"); } @Test public void newValue_createProtoMessage_extensionFieldsPopulated() { ProtoMessageValueProvider protoMessageValueProvider = - ProtoMessageValueProvider.newInstance(DYNAMIC_PROTO, CelOptions.DEFAULT); + ProtoMessageValueProvider.newInstance(CelOptions.DEFAULT, DYNAMIC_PROTO); ProtoMessageValue protoMessageValue = (ProtoMessageValue) protoMessageValueProvider .newValue( - Proto2Message.getDescriptor().getFullName(), - ImmutableMap.of("dev.cel.testing.testdata.proto2.int32_ext", 1)) + TestAllTypes.getDescriptor().getFullName(), + ImmutableMap.of("cel.expr.conformance.proto2.int32_ext", 1)) .get(); assertThat(protoMessageValue.isZeroValue()).isFalse(); - assertThat( - protoMessageValue - .select(StringValue.create("dev.cel.testing.testdata.proto2.int32_ext")) - .value()) - .isEqualTo(1); + assertThat(protoMessageValue.select("cel.expr.conformance.proto2.int32_ext")).isEqualTo(1); } @Test - public void newValue_invalidMessageName_throws() { + public void newValue_invalidMessageName_returnsEmpty() { ProtoMessageValueProvider protoMessageValueProvider = - ProtoMessageValueProvider.newInstance(DYNAMIC_PROTO, CelOptions.DEFAULT); + ProtoMessageValueProvider.newInstance(CelOptions.DEFAULT, DYNAMIC_PROTO); - CelRuntimeException e = - assertThrows( - CelRuntimeException.class, - () -> protoMessageValueProvider.newValue("bogus", ImmutableMap.of())); - - assertThat(e) - .hasMessageThat() - .isEqualTo("java.lang.IllegalArgumentException: cannot resolve 'bogus' as a message"); + assertThat(protoMessageValueProvider.newValue("bogus", ImmutableMap.of())).isEmpty(); } @Test public void newValue_invalidField_throws() { ProtoMessageValueProvider protoMessageValueProvider = - ProtoMessageValueProvider.newInstance(DYNAMIC_PROTO, CelOptions.DEFAULT); + ProtoMessageValueProvider.newInstance(CelOptions.DEFAULT, DYNAMIC_PROTO); IllegalArgumentException e = assertThrows( @@ -241,16 +205,16 @@ public void newValue_invalidField_throws() { .hasMessageThat() .isEqualTo( "field 'bogus' is not declared in message" - + " 'dev.cel.testing.testdata.proto2.TestAllTypes'"); + + " 'cel.expr.conformance.proto2.TestAllTypes'"); } @Test public void newValue_onCombinedProvider() { CelValueProvider celValueProvider = (structType, fields) -> Optional.empty(); ProtoMessageValueProvider protoMessageValueProvider = - ProtoMessageValueProvider.newInstance(DYNAMIC_PROTO, CelOptions.DEFAULT); + ProtoMessageValueProvider.newInstance(CelOptions.DEFAULT, DYNAMIC_PROTO); CelValueProvider combinedProvider = - new CombinedCelValueProvider(celValueProvider, protoMessageValueProvider); + CombinedCelValueProvider.combine(celValueProvider, protoMessageValueProvider); ProtoMessageValue protoMessageValue = (ProtoMessageValue) @@ -260,7 +224,6 @@ public void newValue_onCombinedProvider() { .get(); assertThat(protoMessageValue.isZeroValue()).isFalse(); - assertThat(protoMessageValue.select(StringValue.create("single_int32"))) - .isEqualTo(IntValue.create(1L)); + assertThat(protoMessageValue.select("single_int32")).isEqualTo(1L); } } 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 32f89e7a1..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; @@ -41,11 +42,10 @@ import dev.cel.common.internal.DefaultMessageFactory; import dev.cel.common.internal.DynamicProto; import dev.cel.common.types.StructTypeReference; -import dev.cel.testing.testdata.proto2.MessagesProto2Extensions; -import dev.cel.testing.testdata.proto2.Proto2Message; -import dev.cel.testing.testdata.proto2.TestAllTypesProto.TestAllTypes; -import dev.cel.testing.testdata.proto2.TestAllTypesProto.TestAllTypes.NestedEnum; -import dev.cel.testing.testdata.proto2.TestAllTypesProto.TestAllTypes.NestedMessage; +import dev.cel.expr.conformance.proto2.TestAllTypes; +import dev.cel.expr.conformance.proto2.TestAllTypes.NestedEnum; +import dev.cel.expr.conformance.proto2.TestAllTypes.NestedMessage; +import dev.cel.expr.conformance.proto2.TestAllTypesExtensions; import java.time.Duration; import java.time.Instant; import org.junit.Test; @@ -56,9 +56,9 @@ public final class ProtoMessageValueTest { private static final ProtoCelValueConverter PROTO_CEL_VALUE_CONVERTER = ProtoCelValueConverter.newInstance( - CelOptions.current().enableUnsignedLongs(true).build(), DefaultDescriptorPool.INSTANCE, - DynamicProto.create(DefaultMessageFactory.INSTANCE)); + DynamicProto.create(DefaultMessageFactory.INSTANCE), + CelOptions.DEFAULT); @Test public void emptyProtoMessage() { @@ -66,7 +66,8 @@ public void emptyProtoMessage() { ProtoMessageValue.create( TestAllTypes.getDefaultInstance(), DefaultDescriptorPool.INSTANCE, - PROTO_CEL_VALUE_CONVERTER); + PROTO_CEL_VALUE_CONVERTER, + /* enableJsonFieldNames= */ false); assertThat(protoMessageValue.value()).isEqualTo(TestAllTypes.getDefaultInstance()); assertThat(protoMessageValue.isZeroValue()).isTrue(); @@ -78,7 +79,7 @@ public void constructProtoMessage() { TestAllTypes.newBuilder().setSingleBool(true).setSingleInt64(5L).build(); ProtoMessageValue protoMessageValue = ProtoMessageValue.create( - testAllTypes, DefaultDescriptorPool.INSTANCE, PROTO_CEL_VALUE_CONVERTER); + testAllTypes, DefaultDescriptorPool.INSTANCE, PROTO_CEL_VALUE_CONVERTER, false); assertThat(protoMessageValue.value()).isEqualTo(testAllTypes); assertThat(protoMessageValue.isZeroValue()).isFalse(); @@ -94,11 +95,11 @@ public void findField_fieldIsSet_fieldExists() { .build(); ProtoMessageValue protoMessageValue = ProtoMessageValue.create( - testAllTypes, DefaultDescriptorPool.INSTANCE, PROTO_CEL_VALUE_CONVERTER); + testAllTypes, DefaultDescriptorPool.INSTANCE, PROTO_CEL_VALUE_CONVERTER, false); - assertThat(protoMessageValue.find(StringValue.create("single_bool"))).isPresent(); - assertThat(protoMessageValue.find(StringValue.create("single_int64"))).isPresent(); - assertThat(protoMessageValue.find(StringValue.create("repeated_int64"))).isPresent(); + assertThat(protoMessageValue.find("single_bool")).isPresent(); + assertThat(protoMessageValue.find("single_int64")).isPresent(); + assertThat(protoMessageValue.find("repeated_int64")).isPresent(); } @Test @@ -107,11 +108,12 @@ public void findField_fieldIsUnset_fieldDoesNotExist() { ProtoMessageValue.create( TestAllTypes.getDefaultInstance(), DefaultDescriptorPool.INSTANCE, - PROTO_CEL_VALUE_CONVERTER); + PROTO_CEL_VALUE_CONVERTER, + /* enableJsonFieldNames= */ false); - assertThat(protoMessageValue.find(StringValue.create("single_int32"))).isEmpty(); - assertThat(protoMessageValue.find(StringValue.create("single_uint64"))).isEmpty(); - assertThat(protoMessageValue.find(StringValue.create("repeated_int32"))).isEmpty(); + assertThat(protoMessageValue.find("single_int32")).isEmpty(); + assertThat(protoMessageValue.find("single_uint64")).isEmpty(); + assertThat(protoMessageValue.find("repeated_int32")).isEmpty(); } @Test @@ -120,17 +122,16 @@ public void findField_fieldIsUndeclared_throwsException() { ProtoMessageValue.create( TestAllTypes.getDefaultInstance(), DefaultDescriptorPool.INSTANCE, - PROTO_CEL_VALUE_CONVERTER); + PROTO_CEL_VALUE_CONVERTER, + /* enableJsonFieldNames= */ false); IllegalArgumentException exception = - assertThrows( - IllegalArgumentException.class, - () -> protoMessageValue.select(StringValue.create("bogus"))); + assertThrows(IllegalArgumentException.class, () -> protoMessageValue.select("bogus")); assertThat(exception) .hasMessageThat() .isEqualTo( "field 'bogus' is not declared in message" - + " 'dev.cel.testing.testdata.proto2.TestAllTypes'"); + + " 'cel.expr.conformance.proto2.TestAllTypes'"); } @Test @@ -138,88 +139,77 @@ public void findField_extensionField_success() { CelDescriptorPool descriptorPool = DefaultDescriptorPool.create( CelDescriptorUtil.getAllDescriptorsFromFileDescriptor( - ImmutableList.of(MessagesProto2Extensions.getDescriptor()))); - ProtoCelValueConverter protoCelValueConverter = - ProtoCelValueConverter.newInstance( - CelOptions.DEFAULT, - DefaultDescriptorPool.INSTANCE, - DynamicProto.create(DefaultMessageFactory.create(descriptorPool))); - Proto2Message proto2Message = - Proto2Message.newBuilder().setExtension(MessagesProto2Extensions.int32Ext, 1).build(); + ImmutableList.of(TestAllTypesExtensions.getDescriptor()))); + TestAllTypes proto2Message = + TestAllTypes.newBuilder().setExtension(TestAllTypesExtensions.int32Ext, 1).build(); ProtoMessageValue protoMessageValue = - ProtoMessageValue.create(proto2Message, descriptorPool, protoCelValueConverter); + ProtoMessageValue.create(proto2Message, descriptorPool, PROTO_CEL_VALUE_CONVERTER, false); - assertThat( - protoMessageValue.find(StringValue.create("dev.cel.testing.testdata.proto2.int32_ext"))) - .isPresent(); + assertThat(protoMessageValue.find("cel.expr.conformance.proto2.int32_ext")).isPresent(); } @Test public void findField_extensionField_throwsWhenDescriptorMissing() { - Proto2Message proto2Message = - Proto2Message.newBuilder().setExtension(MessagesProto2Extensions.int32Ext, 1).build(); + TestAllTypes proto2Message = + TestAllTypes.newBuilder().setExtension(TestAllTypesExtensions.int32Ext, 1).build(); ProtoMessageValue protoMessageValue = ProtoMessageValue.create( - proto2Message, DefaultDescriptorPool.INSTANCE, PROTO_CEL_VALUE_CONVERTER); + proto2Message, DefaultDescriptorPool.INSTANCE, PROTO_CEL_VALUE_CONVERTER, false); IllegalArgumentException exception = assertThrows( IllegalArgumentException.class, - () -> - protoMessageValue.select( - StringValue.create("dev.cel.testing.testdata.proto2.int32_ext"))); + () -> protoMessageValue.select("cel.expr.conformance.proto2.int32_ext")); assertThat(exception) .hasMessageThat() .isEqualTo( - "field 'dev.cel.testing.testdata.proto2.int32_ext' is not declared in message" - + " 'dev.cel.testing.testdata.proto2.Proto2Message'"); + "field 'cel.expr.conformance.proto2.int32_ext' is not declared in message" + + " 'cel.expr.conformance.proto2.TestAllTypes'"); } + @SuppressWarnings("ImmutableEnumChecker") // Test only private enum SelectFieldTestCase { // Primitives - BOOL("single_bool", BoolValue.create(true)), - INT32("single_int32", IntValue.create(4L)), - INT64("single_int64", IntValue.create(5L)), - UINT32("single_uint32", UintValue.create(UnsignedLong.valueOf(1L))), - UINT64("single_uint64", UintValue.create(UnsignedLong.MAX_VALUE)), - FLOAT("single_float", DoubleValue.create(1.5d)), - DOUBLE("single_double", DoubleValue.create(2.5d)), - STRING("single_string", StringValue.create("test")), - BYTES("single_bytes", BytesValue.create(CelByteString.of(new byte[] {0x01}))), + BOOL("single_bool", true), + INT32("single_int32", 4L), + INT64("single_int64", 5L), + UINT32("single_uint32", UnsignedLong.valueOf(1L)), + UINT64("single_uint64", UnsignedLong.MAX_VALUE), + FLOAT("single_float", 1.5d), + DOUBLE("single_double", 2.5d), + STRING("single_string", "test"), + BYTES("single_bytes", CelByteString.of(new byte[] {0x01})), // Well known types - ANY("single_any", BoolValue.create(true)), - DURATION("single_duration", DurationValue.create(Duration.ofSeconds(100))), - TIMESTAMP("single_timestamp", TimestampValue.create(Instant.ofEpochSecond(100))), - INT32_WRAPPER("single_int32_wrapper", IntValue.create(5L)), - INT64_WRAPPER("single_int64_wrapper", IntValue.create(10L)), - UINT32_WRAPPER("single_uint32_wrapper", UintValue.create(UnsignedLong.valueOf(1L))), - UINT64_WRAPPER("single_uint64_wrapper", UintValue.create(UnsignedLong.MAX_VALUE)), - FLOAT_WRAPPER("single_float_wrapper", DoubleValue.create(7.5d)), - DOUBLE_WRAPPER("single_double_wrapper", DoubleValue.create(8.5d)), - STRING_WRAPPER("single_string_wrapper", StringValue.create("hello")), - BYTES_WRAPPER("single_bytes_wrapper", BytesValue.create(CelByteString.of(new byte[] {0x02}))), - REPEATED_INT64( - "repeated_int64", ImmutableListValue.create(ImmutableList.of(IntValue.create(5L)))), - MAP_STRING_STRING( - "map_string_string", - ImmutableMapValue.create( - ImmutableMap.of(StringValue.create("a"), StringValue.create("b")))), + ANY("single_any", true), + DURATION("single_duration", Duration.ofSeconds(100)), + TIMESTAMP("single_timestamp", Instant.ofEpochSecond(100)), + INT32_WRAPPER("single_int32_wrapper", 5L), + INT64_WRAPPER("single_int64_wrapper", 10L), + UINT32_WRAPPER("single_uint32_wrapper", UnsignedLong.valueOf(1L)), + UINT64_WRAPPER("single_uint64_wrapper", UnsignedLong.MAX_VALUE), + FLOAT_WRAPPER("single_float_wrapper", 7.5d), + DOUBLE_WRAPPER("single_double_wrapper", 8.5d), + STRING_WRAPPER("single_string_wrapper", "hello"), + BYTES_WRAPPER("single_bytes_wrapper", CelByteString.of(new byte[] {0x02})), + REPEATED_INT64("repeated_int64", ImmutableList.of(5L)), + MAP_STRING_STRING("map_string_string", ImmutableMap.of("a", "b")), NESTED_MESSAGE( "standalone_message", ProtoMessageValue.create( NestedMessage.getDefaultInstance(), DefaultDescriptorPool.INSTANCE, - PROTO_CEL_VALUE_CONVERTER)), - NESTED_ENUM("standalone_enum", IntValue.create(1L)); + PROTO_CEL_VALUE_CONVERTER, + /* enableJsonFieldNames= */ false)), + NESTED_ENUM("standalone_enum", 1L); private final String fieldName; - private final CelValue celValue; + private final Object value; - SelectFieldTestCase(String fieldName, CelValue celValue) { + SelectFieldTestCase(String fieldName, Object value) { this.fieldName = fieldName; - this.celValue = celValue; + this.value = value; } } @@ -257,10 +247,9 @@ public void selectField_success(@TestParameter SelectFieldTestCase testCase) { ProtoMessageValue protoMessageValue = ProtoMessageValue.create( - testAllTypes, DefaultDescriptorPool.INSTANCE, PROTO_CEL_VALUE_CONVERTER); + testAllTypes, DefaultDescriptorPool.INSTANCE, PROTO_CEL_VALUE_CONVERTER, false); - assertThat(protoMessageValue.select(StringValue.create(testCase.fieldName))) - .isEqualTo(testCase.celValue); + assertThat(protoMessageValue.select(testCase.fieldName)).isEqualTo(testCase.value); } @Test @@ -272,10 +261,10 @@ public void selectField_dynamicMessage_success() { ProtoMessageValue.create( DynamicMessage.newBuilder(testAllTypes).build(), DefaultDescriptorPool.INSTANCE, - PROTO_CEL_VALUE_CONVERTER); + PROTO_CEL_VALUE_CONVERTER, + /* enableJsonFieldNames= */ false); - assertThat(protoMessageValue.select(StringValue.create("single_int32_wrapper"))) - .isEqualTo(IntValue.create(5)); + assertThat(protoMessageValue.select("single_int32_wrapper")).isEqualTo(5); } @Test @@ -289,10 +278,10 @@ public void selectField_timestampNanosOutOfRange_success(int nanos) { ProtoMessageValue protoMessageValue = ProtoMessageValue.create( - testAllTypes, DefaultDescriptorPool.INSTANCE, PROTO_CEL_VALUE_CONVERTER); + testAllTypes, DefaultDescriptorPool.INSTANCE, PROTO_CEL_VALUE_CONVERTER, false); - assertThat(protoMessageValue.select(StringValue.create("single_timestamp"))) - .isEqualTo(TimestampValue.create(Instant.ofEpochSecond(0, nanos))); + assertThat(protoMessageValue.select("single_timestamp")) + .isEqualTo(Instant.ofEpochSecond(0, nanos)); } @Test @@ -309,17 +298,58 @@ public void selectField_durationOutOfRange_success(int seconds, int nanos) { ProtoMessageValue protoMessageValue = ProtoMessageValue.create( - testAllTypes, DefaultDescriptorPool.INSTANCE, PROTO_CEL_VALUE_CONVERTER); + testAllTypes, DefaultDescriptorPool.INSTANCE, PROTO_CEL_VALUE_CONVERTER, false); + + assertThat(protoMessageValue.select("single_duration")) + .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(StringValue.create("single_duration"))) - .isEqualTo(DurationValue.create(Duration.ofSeconds(seconds, nanos))); + assertThat(protoMessageValue.select("single_fixed64")).isEqualTo(UnsignedLong.MAX_VALUE); } + @SuppressWarnings("ImmutableEnumChecker") // Test only private enum SelectFieldJsonValueTestCase { NULL(Value.newBuilder().build(), NullValue.NULL_VALUE), - BOOL(Value.newBuilder().setBoolValue(true).build(), BoolValue.create(true)), - DOUBLE(Value.newBuilder().setNumberValue(4.5d).build(), DoubleValue.create(4.5d)), - STRING(Value.newBuilder().setStringValue("test").build(), StringValue.create("test")), + BOOL(Value.newBuilder().setBoolValue(true).build(), true), + DOUBLE(Value.newBuilder().setNumberValue(4.5d).build(), 4.5d), + STRING(Value.newBuilder().setStringValue("test").build(), "test"), STRUCT( Value.newBuilder() .setStructValue( @@ -327,8 +357,7 @@ private enum SelectFieldJsonValueTestCase { .putFields("a", Value.newBuilder().setBoolValue(false).build()) .build()) .build(), - ImmutableMapValue.create( - ImmutableMap.of(StringValue.create("a"), BoolValue.create(false)))), + ImmutableMap.of("a", false)), LIST( Value.newBuilder() .setListValue( @@ -336,14 +365,14 @@ private enum SelectFieldJsonValueTestCase { .addValues(Value.newBuilder().setStringValue("test").build()) .build()) .build(), - ImmutableListValue.create(ImmutableList.of(StringValue.create("test")))); + ImmutableList.of("test")); private final Value jsonValue; - private final CelValue celValue; + private final Object value; - SelectFieldJsonValueTestCase(Value jsonValue, CelValue celValue) { + SelectFieldJsonValueTestCase(Value jsonValue, Object value) { this.jsonValue = jsonValue; - this.celValue = celValue; + this.value = value; } } @@ -354,10 +383,9 @@ public void selectField_jsonValue(@TestParameter SelectFieldJsonValueTestCase te ProtoMessageValue protoMessageValue = ProtoMessageValue.create( - testAllTypes, DefaultDescriptorPool.INSTANCE, PROTO_CEL_VALUE_CONVERTER); + testAllTypes, DefaultDescriptorPool.INSTANCE, PROTO_CEL_VALUE_CONVERTER, false); - assertThat(protoMessageValue.select(StringValue.create("single_value"))) - .isEqualTo(testCase.celValue); + assertThat(protoMessageValue.select("single_value")).isEqualTo(testCase.value); } @Test @@ -372,12 +400,9 @@ public void selectField_jsonStruct() { ProtoMessageValue protoMessageValue = ProtoMessageValue.create( - testAllTypes, DefaultDescriptorPool.INSTANCE, PROTO_CEL_VALUE_CONVERTER); + testAllTypes, DefaultDescriptorPool.INSTANCE, PROTO_CEL_VALUE_CONVERTER, false); - assertThat(protoMessageValue.select(StringValue.create("single_struct"))) - .isEqualTo( - ImmutableMapValue.create( - ImmutableMap.of(StringValue.create("a"), BoolValue.create(false)))); + assertThat(protoMessageValue.select("single_struct")).isEqualTo(ImmutableMap.of("a", false)); } @Test @@ -392,10 +417,9 @@ public void selectField_jsonList() { ProtoMessageValue protoMessageValue = ProtoMessageValue.create( - testAllTypes, DefaultDescriptorPool.INSTANCE, PROTO_CEL_VALUE_CONVERTER); + testAllTypes, DefaultDescriptorPool.INSTANCE, PROTO_CEL_VALUE_CONVERTER, false); - assertThat(protoMessageValue.select(StringValue.create("list_value"))) - .isEqualTo(ImmutableListValue.create(ImmutableList.of(BoolValue.create(false)))); + assertThat(protoMessageValue.select("list_value")).isEqualTo(ImmutableList.of(false)); } @Test @@ -404,10 +428,10 @@ public void selectField_wrapperFieldUnset_returnsNull() { ProtoMessageValue.create( TestAllTypes.getDefaultInstance(), DefaultDescriptorPool.INSTANCE, - PROTO_CEL_VALUE_CONVERTER); + PROTO_CEL_VALUE_CONVERTER, + /* enableJsonFieldNames= */ false); - assertThat(protoMessageValue.select(StringValue.create("single_int64_wrapper"))) - .isEqualTo(NullValue.NULL_VALUE); + assertThat(protoMessageValue.select("single_int64_wrapper")).isEqualTo(NullValue.NULL_VALUE); } @Test @@ -416,9 +440,21 @@ public void celTypeTest() { ProtoMessageValue.create( TestAllTypes.getDefaultInstance(), DefaultDescriptorPool.INSTANCE, - PROTO_CEL_VALUE_CONVERTER); + PROTO_CEL_VALUE_CONVERTER, + /* enableJsonFieldNames= */ false); assertThat(protoMessageValue.celType()) .isEqualTo(StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())); } + + @Test + public void findField_jsonName_success() { + TestAllTypes testAllTypes = TestAllTypes.newBuilder().setSingleInt32(42).build(); + + ProtoMessageValue protoMessageValue = + ProtoMessageValue.create( + testAllTypes, DefaultDescriptorPool.INSTANCE, PROTO_CEL_VALUE_CONVERTER, true); + + assertThat(protoMessageValue.find("singleInt32")).isPresent(); + } } diff --git a/common/src/test/java/dev/cel/common/values/StringValueTest.java b/common/src/test/java/dev/cel/common/values/StringValueTest.java deleted file mode 100644 index 6a0a8113c..000000000 --- a/common/src/test/java/dev/cel/common/values/StringValueTest.java +++ /dev/null @@ -1,63 +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.common.values; - -import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.assertThrows; - -import dev.cel.common.types.SimpleType; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public class StringValueTest { - - @Test - public void emptyString() { - StringValue stringValue = StringValue.create(""); - - assertThat(stringValue.value()).isEmpty(); - assertThat(stringValue.isZeroValue()).isTrue(); - } - - @Test - public void blankString() { - StringValue stringValue = StringValue.create(" "); - - assertThat(stringValue.value()).isEqualTo(" "); - assertThat(stringValue.isZeroValue()).isFalse(); - } - - @Test - public void constructString() { - StringValue stringValue = StringValue.create("Hello World"); - - assertThat(stringValue.value()).isEqualTo("Hello World"); - assertThat(stringValue.isZeroValue()).isFalse(); - } - - @Test - public void create_nullValue_throws() { - assertThrows(NullPointerException.class, () -> StringValue.create(null)); - } - - @Test - public void celTypeTest() { - StringValue value = StringValue.create(""); - - assertThat(value.celType()).isEqualTo(SimpleType.STRING); - } -} 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 bd4e820b8..978222869 100644 --- a/common/src/test/java/dev/cel/common/values/StructValueTest.java +++ b/common/src/test/java/dev/cel/common/values/StructValueTest.java @@ -26,6 +26,7 @@ import dev.cel.bundle.CelFactory; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelOptions; +import dev.cel.common.internal.DynamicProto; import dev.cel.common.types.CelType; import dev.cel.common.types.CelTypeProvider; import dev.cel.common.types.SimpleType; @@ -58,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(); } @@ -77,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(); } @@ -85,15 +102,14 @@ public void constructStruct() { public void selectField_success() { CelCustomStructValue celCustomStruct = new CelCustomStructValue(5); - assertThat(celCustomStruct.select(StringValue.create("data"))).isEqualTo(IntValue.create(5L)); + assertThat(celCustomStruct.select("data")).isEqualTo(5L); } @Test public void selectField_nonExistentField_throws() { CelCustomStructValue celCustomStruct = new CelCustomStructValue(5); - assertThrows( - IllegalArgumentException.class, () -> celCustomStruct.select(StringValue.create("bogus"))); + assertThrows(IllegalArgumentException.class, () -> celCustomStruct.select("bogus")); } @Test @@ -102,8 +118,7 @@ public void selectField_nonExistentField_throws() { public void findField_success(String fieldName, boolean expectedResult) { CelCustomStructValue celCustomStruct = new CelCustomStructValue(5); - assertThat(celCustomStruct.find(StringValue.create(fieldName)).isPresent()) - .isEqualTo(expectedResult); + assertThat(celCustomStruct.find(fieldName).isPresent()).isEqualTo(expectedResult); } @Test @@ -113,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) @@ -164,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(); @@ -176,19 +207,53 @@ public void evaluate_usingCustomClass_selectField() throws Exception { assertThat(result).isEqualTo(5L); } - @SuppressWarnings("Immutable") // Test only - private static class CelCustomStructValue extends StructValue { + @Test + public void evaluate_usingMultipleProviders_selectFieldFromCustomClass() throws Exception { + Cel cel = + CelFactory.plannerCelBuilder() + .setOptions(CelOptions.current().enableHeterogeneousNumericComparisons(true).build()) + .setTypeProvider(CUSTOM_STRUCT_TYPE_PROVIDER) + .setValueProvider( + CombinedCelValueProvider.combine( + ProtoMessageValueProvider.newInstance( + CelOptions.DEFAULT, DynamicProto.create(unused -> Optional.empty())), + CUSTOM_STRUCT_VALUE_PROVIDER)) + .build(); + CelAbstractSyntaxTree ast = cel.compile("custom_struct{data: 5}.data").getAst(); + + Object result = cel.createProgram(ast).eval(); + + assertThat(result).isEqualTo(5L); + } + + // 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 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 @@ -197,15 +262,15 @@ public CelType celType() { } @Override - public CelValue select(StringValue field) { + public Object select(String field) { return find(field) .orElseThrow(() -> new IllegalArgumentException("Invalid field name: " + field)); } @Override - public Optional find(StringValue field) { - if (field.value().equals("data")) { - return Optional.of(IntValue.create(value().data)); + public Optional find(String field) { + if (field.equals("data")) { + return Optional.of(pojo.getData()); } return Optional.empty(); @@ -216,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/src/test/java/dev/cel/common/values/TimestampValueTest.java b/common/src/test/java/dev/cel/common/values/TimestampValueTest.java deleted file mode 100644 index c20466533..000000000 --- a/common/src/test/java/dev/cel/common/values/TimestampValueTest.java +++ /dev/null @@ -1,56 +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.common.values; - -import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.assertThrows; - -import dev.cel.common.types.SimpleType; -import java.time.Instant; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public class TimestampValueTest { - - @Test - public void emptyTimestamp() { - TimestampValue timestampValue = TimestampValue.create(Instant.ofEpochSecond(0)); - - assertThat(timestampValue.value()).isEqualTo(Instant.EPOCH); - assertThat(timestampValue.isZeroValue()).isTrue(); - } - - @Test - public void constructTimestamp() { - TimestampValue timestampValue = TimestampValue.create(Instant.ofEpochMilli(100000)); - - assertThat(timestampValue.value()).isEqualTo(Instant.ofEpochMilli(100000)); - assertThat(timestampValue.isZeroValue()).isFalse(); - } - - @Test - public void create_nullValue_throws() { - assertThrows(NullPointerException.class, () -> TimestampValue.create(null)); - } - - @Test - public void celTypeTest() { - TimestampValue value = TimestampValue.create(Instant.ofEpochSecond(0)); - - assertThat(value.celType()).isEqualTo(SimpleType.TIMESTAMP); - } -} diff --git a/common/src/test/java/dev/cel/common/values/TypeValueTest.java b/common/src/test/java/dev/cel/common/values/TypeValueTest.java deleted file mode 100644 index 7f8ab3eb7..000000000 --- a/common/src/test/java/dev/cel/common/values/TypeValueTest.java +++ /dev/null @@ -1,48 +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.common.values; - -import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.assertThrows; - -import dev.cel.common.types.SimpleType; -import dev.cel.common.types.TypeType; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public class TypeValueTest { - - @Test - public void constructTypeValue() { - TypeValue typeValue = TypeValue.create(SimpleType.INT); - - assertThat(typeValue.value()).isEqualTo(SimpleType.INT); - assertThat(typeValue.isZeroValue()).isFalse(); - } - - @Test - public void create_nullValue_throws() { - assertThrows(NullPointerException.class, () -> TypeValue.create(null)); - } - - @Test - public void celTypeTest() { - TypeValue value = TypeValue.create(SimpleType.INT); - - assertThat(value.celType()).isEqualTo(TypeType.create(SimpleType.INT)); - } -} diff --git a/common/src/test/java/dev/cel/common/values/UintValueTest.java b/common/src/test/java/dev/cel/common/values/UintValueTest.java deleted file mode 100644 index 2d073b461..000000000 --- a/common/src/test/java/dev/cel/common/values/UintValueTest.java +++ /dev/null @@ -1,56 +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.common.values; - -import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.assertThrows; - -import com.google.common.primitives.UnsignedLong; -import dev.cel.common.types.SimpleType; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public class UintValueTest { - - @Test - public void emptyUint() { - UintValue uintValue = UintValue.create(UnsignedLong.valueOf(0L)); - - assertThat(uintValue.value()).isEqualTo(UnsignedLong.valueOf(0L)); - assertThat(uintValue.isZeroValue()).isTrue(); - } - - @Test - public void constructUint() { - UintValue uintValue = UintValue.create(UnsignedLong.valueOf(5L)); - - assertThat(uintValue.value()).isEqualTo(UnsignedLong.valueOf(5L)); - assertThat(uintValue.isZeroValue()).isFalse(); - } - - @Test - public void create_nullValue_throws() { - assertThrows(NullPointerException.class, () -> UintValue.create(null)); - } - - @Test - public void celTypeTest() { - UintValue value = UintValue.create(UnsignedLong.valueOf(0L)); - - assertThat(value.celType()).isEqualTo(SimpleType.UINT); - } -} diff --git a/common/src/test/resources/BUILD.bazel b/common/src/test/resources/BUILD.bazel index f53608da7..68b07702e 100644 --- a/common/src/test/resources/BUILD.bazel +++ b/common/src/test/resources/BUILD.bazel @@ -1,3 +1,6 @@ +load("@com_google_protobuf//bazel:java_proto_library.bzl", "java_proto_library") +load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") + package( default_applicable_licenses = [ "//:license", @@ -15,26 +18,6 @@ filegroup( ], ) -proto_library( - name = "multi_file_proto", - srcs = ["multi_file.proto"], -) - -java_proto_library( - name = "multi_file_java_proto", - deps = [":multi_file_proto"], -) - -proto_library( - name = "single_file_proto", - srcs = ["single_file.proto"], -) - -java_proto_library( - name = "single_file_java_proto", - deps = [":single_file_proto"], -) - proto_library( name = "default_instance_message_test_protos", srcs = [ diff --git a/common/testing/BUILD.bazel b/common/testing/BUILD.bazel index b98bc4c68..fdc3a1106 100644 --- a/common/testing/BUILD.bazel +++ b/common/testing/BUILD.bazel @@ -1,7 +1,9 @@ +load("@rules_java//java:defs.bzl", "java_library") + package( default_applicable_licenses = ["//:license"], default_testonly = True, - default_visibility = ["//visibility:public"], + default_visibility = ["//:internal"], ) java_library( diff --git a/common/types/BUILD.bazel b/common/types/BUILD.bazel index af81a975b..df249ddbc 100644 --- a/common/types/BUILD.bazel +++ b/common/types/BUILD.bazel @@ -1,3 +1,6 @@ +load("@rules_java//java:defs.bzl", "java_library") +load("//:cel_android_rules.bzl", "cel_android_library") + package( default_applicable_licenses = ["//:license"], default_visibility = ["//visibility:public"], @@ -10,14 +13,14 @@ java_library( java_library( name = "cel_internal_types", - visibility = ["//visibility:public"], + visibility = ["//:internal"], exports = ["//common/src/main/java/dev/cel/common/types:cel_internal_types"], ) java_library( name = "json", visibility = [ - "//visibility:public", + "//:internal", ], exports = ["//common/src/main/java/dev/cel/common/types:json"], ) @@ -37,8 +40,49 @@ java_library( exports = ["//common/src/main/java/dev/cel/common/types:cel_types"], ) +java_library( + name = "cel_proto_types", + exports = ["//common/src/main/java/dev/cel/common/types:cel_proto_types"], +) + +java_library( + name = "cel_proto_message_types", + exports = ["//common/src/main/java/dev/cel/common/types:cel_proto_message_types"], +) + +java_library( + name = "default_type_provider", + visibility = ["//:internal"], + exports = ["//common/src/main/java/dev/cel/common/types:default_type_provider"], +) + java_library( name = "cel_v1alpha1_types", - visibility = ["//visibility:public"], + visibility = ["//:internal"], exports = ["//common/src/main/java/dev/cel/common/types:cel_v1alpha1_types"], ) + +cel_android_library( + name = "cel_types_android", + exports = ["//common/src/main/java/dev/cel/common/types:cel_types_android"], +) + +cel_android_library( + name = "types_android", + exports = ["//common/src/main/java/dev/cel/common/types:types_android"], +) + +cel_android_library( + name = "type_providers_android", + exports = ["//common/src/main/java/dev/cel/common/types:type_providers_android"], +) + +cel_android_library( + name = "cel_proto_types_android", + exports = ["//common/src/main/java/dev/cel/common/types:cel_proto_types_android"], +) + +cel_android_library( + name = "default_type_provider_android", + exports = ["//common/src/main/java/dev/cel/common/types:default_type_provider_android"], +) diff --git a/common/values/BUILD.bazel b/common/values/BUILD.bazel index 931999b4c..9853289a9 100644 --- a/common/values/BUILD.bazel +++ b/common/values/BUILD.bazel @@ -1,24 +1,86 @@ +load("@rules_java//java:defs.bzl", "java_library") +load("//:cel_android_rules.bzl", "cel_android_library") + package( default_applicable_licenses = ["//:license"], - default_visibility = ["//visibility:public"], # TODO: Expose to public when ready + default_visibility = ["//visibility:public"], ) java_library( name = "cel_value", - visibility = ["//visibility:public"], + visibility = ["//:internal"], exports = ["//common/src/main/java/dev/cel/common/values:cel_value"], ) +cel_android_library( + name = "cel_value_android", + exports = ["//common/src/main/java/dev/cel/common/values:cel_value_android"], +) + java_library( name = "cel_value_provider", exports = ["//common/src/main/java/dev/cel/common/values:cel_value_provider"], ) +cel_android_library( + name = "cel_value_provider_android", + exports = ["//common/src/main/java/dev/cel/common/values:cel_value_provider_android"], +) + +java_library( + name = "combined_cel_value_provider", + exports = ["//common/src/main/java/dev/cel/common/values:combined_cel_value_provider"], +) + +cel_android_library( + name = "combined_cel_value_provider_android", + 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"], ) +cel_android_library( + name = "values_android", + 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"], +) + +cel_android_library( + name = "base_proto_cel_value_converter_android", + exports = ["//common/src/main/java/dev/cel/common/values:base_proto_cel_value_converter_android"], +) + java_library( name = "proto_message_value_provider", exports = ["//common/src/main/java/dev/cel/common/values:proto_message_value_provider"], @@ -26,6 +88,7 @@ java_library( java_library( name = "cel_byte_string", + # used_by_android exports = ["//common/src/main/java/dev/cel/common/values:cel_byte_string"], ) @@ -33,3 +96,33 @@ java_library( name = "proto_message_value", exports = ["//common/src/main/java/dev/cel/common/values:proto_message_value"], ) + +java_library( + name = "proto_message_lite_value", + exports = ["//common/src/main/java/dev/cel/common/values:proto_message_lite_value"], +) + +cel_android_library( + name = "proto_message_lite_value_android", + exports = ["//common/src/main/java/dev/cel/common/values:proto_message_lite_value_android"], +) + +java_library( + name = "proto_message_lite_value_provider", + exports = ["//common/src/main/java/dev/cel/common/values:proto_message_lite_value_provider"], +) + +cel_android_library( + name = "proto_message_lite_value_provider_android", + exports = ["//common/src/main/java/dev/cel/common/values:proto_message_lite_value_provider_android"], +) + +java_library( + name = "base_proto_message_value_provider", + exports = ["//common/src/main/java/dev/cel/common/values:base_proto_message_value_provider"], +) + +cel_android_library( + name = "base_proto_message_value_provider_android", + exports = ["//common/src/main/java/dev/cel/common/values:base_proto_message_value_provider_android"], +) diff --git a/compiler/BUILD.bazel b/compiler/BUILD.bazel index e5e41e0a5..5bb7d7129 100644 --- a/compiler/BUILD.bazel +++ b/compiler/BUILD.bazel @@ -1,3 +1,5 @@ +load("@rules_java//java:defs.bzl", "java_library") + package( default_applicable_licenses = ["//:license"], default_visibility = ["//visibility:public"], diff --git a/compiler/src/main/java/dev/cel/compiler/BUILD.bazel b/compiler/src/main/java/dev/cel/compiler/BUILD.bazel index fc20e8170..6a9a80709 100644 --- a/compiler/src/main/java/dev/cel/compiler/BUILD.bazel +++ b/compiler/src/main/java/dev/cel/compiler/BUILD.bazel @@ -1,3 +1,5 @@ +load("@rules_java//java:defs.bzl", "java_library") + package( default_applicable_licenses = [ "//:license", @@ -32,17 +34,20 @@ java_library( "//checker:checker_builder", "//checker:checker_legacy_environment", "//checker:proto_type_mask", - "//common", + "//checker:standard_decl", + "//common:cel_ast", + "//common:cel_source", "//common:compiler_common", + "//common:container", "//common:options", "//common/annotations", "//common/internal:env_visitor", - "//common/types:cel_types", + "//common/types:cel_proto_types", "//common/types:type_providers", "//parser", "//parser:macro", "//parser:parser_builder", - "@cel_spec//proto/cel/expr:expr_java_proto", + "@cel_spec//proto/cel/expr:checked_java_proto", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", @@ -59,12 +64,14 @@ java_library( "//checker:checker_builder", "//checker:checker_legacy_environment", "//checker:proto_type_mask", + "//checker:standard_decl", "//common:compiler_common", + "//common:container", "//common:options", "//common/types:type_providers", "//parser:macro", "//parser:parser_builder", - "@cel_spec//proto/cel/expr:expr_java_proto", + "@cel_spec//proto/cel/expr:checked_java_proto", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_protobuf_protobuf_java", ], diff --git a/compiler/src/main/java/dev/cel/compiler/CelCompilerBuilder.java b/compiler/src/main/java/dev/cel/compiler/CelCompilerBuilder.java index 886667782..6dd2ee12e 100644 --- a/compiler/src/main/java/dev/cel/compiler/CelCompilerBuilder.java +++ b/compiler/src/main/java/dev/cel/compiler/CelCompilerBuilder.java @@ -21,8 +21,10 @@ import com.google.protobuf.DescriptorProtos.FileDescriptorSet; import com.google.protobuf.Descriptors.Descriptor; import com.google.protobuf.Descriptors.FileDescriptor; +import dev.cel.checker.CelStandardDeclarations; import dev.cel.checker.ProtoTypeMask; import dev.cel.checker.TypeProvider; +import dev.cel.common.CelContainer; import dev.cel.common.CelFunctionDecl; import dev.cel.common.CelOptions; import dev.cel.common.CelVarDecl; @@ -74,11 +76,14 @@ public interface CelCompilerBuilder { CelCompilerBuilder setOptions(CelOptions options); /** - * Set the {@code container} name to use as the namespace for resolving CEL expression variables - * and functions. + * Set the {@link CelContainer} to use as the namespace for resolving CEL expression variables and + * functions. */ @CanIgnoreReturnValue - CelCompilerBuilder setContainer(String container); + CelCompilerBuilder setContainer(CelContainer container); + + /** Retrieves the currently configured {@link CelContainer} in the builder. */ + CelContainer container(); /** Add a variable declaration with a given {@code name} and proto based {@link Type}. */ @CanIgnoreReturnValue @@ -199,6 +204,14 @@ public interface CelCompilerBuilder { @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. + */ + @CanIgnoreReturnValue + CelCompilerBuilder setStandardDeclarations(CelStandardDeclarations standardDeclarations); + /** Adds one or more libraries for parsing and type-checking. */ @CanIgnoreReturnValue CelCompilerBuilder addLibraries(CelCompilerLibrary... libraries); diff --git a/compiler/src/main/java/dev/cel/compiler/CelCompilerImpl.java b/compiler/src/main/java/dev/cel/compiler/CelCompilerImpl.java index ac81f5d0c..e8804f348 100644 --- a/compiler/src/main/java/dev/cel/compiler/CelCompilerImpl.java +++ b/compiler/src/main/java/dev/cel/compiler/CelCompilerImpl.java @@ -25,9 +25,11 @@ import com.google.protobuf.Descriptors.FileDescriptor; import dev.cel.checker.CelChecker; import dev.cel.checker.CelCheckerBuilder; +import dev.cel.checker.CelStandardDeclarations; import dev.cel.checker.ProtoTypeMask; import dev.cel.checker.TypeProvider; import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelContainer; import dev.cel.common.CelFunctionDecl; import dev.cel.common.CelOptions; import dev.cel.common.CelSource; @@ -36,9 +38,9 @@ import dev.cel.common.annotations.Internal; import dev.cel.common.internal.EnvVisitable; import dev.cel.common.internal.EnvVisitor; +import dev.cel.common.types.CelProtoTypes; import dev.cel.common.types.CelType; import dev.cel.common.types.CelTypeProvider; -import dev.cel.common.types.CelTypes; import dev.cel.parser.CelMacro; import dev.cel.parser.CelParser; import dev.cel.parser.CelParserBuilder; @@ -74,11 +76,19 @@ public CelValidationResult check(CelAbstractSyntaxTree ast) { return checker.check(ast); } + @Override + public CelTypeProvider getTypeProvider() { + return checker.getTypeProvider(); + } + @Override public void accept(EnvVisitor envVisitor) { if (checker instanceof EnvVisitable) { ((EnvVisitable) checker).accept(envVisitor); } + if (parser instanceof EnvVisitable) { + ((EnvVisitable) parser).accept(envVisitor); + } } @Override @@ -149,14 +159,19 @@ public CelCompilerBuilder addMacros(Iterable macros) { } @Override - public CelCompilerBuilder setContainer(String container) { + public CelCompilerBuilder setContainer(CelContainer container) { checkerBuilder.setContainer(container); return this; } + @Override + public CelContainer container() { + return checkerBuilder.container(); + } + @Override public CelCompilerBuilder addVar(String name, Type type) { - return addVar(name, CelTypes.typeToCelType(type)); + return addVar(name, CelProtoTypes.typeToCelType(type)); } @Override @@ -215,7 +230,7 @@ public CelCompilerBuilder addProtoTypeMasks(Iterable typeMasks) { @Override public CelCompilerBuilder setResultType(CelType resultType) { checkNotNull(resultType); - return setProtoResultType(CelTypes.celTypeToType(resultType)); + return setProtoResultType(CelProtoTypes.celTypeToType(resultType)); } @Override @@ -273,6 +288,13 @@ public CelCompilerBuilder setStandardEnvironmentEnabled(boolean value) { return this; } + @Override + public CelCompilerBuilder setStandardDeclarations( + CelStandardDeclarations standardDeclarations) { + checkerBuilder.setStandardDeclarations(standardDeclarations); + return this; + } + @Override public CelCompilerBuilder addLibraries(CelCompilerLibrary... libraries) { checkNotNull(libraries); diff --git a/compiler/src/main/java/dev/cel/compiler/tools/BUILD.bazel b/compiler/src/main/java/dev/cel/compiler/tools/BUILD.bazel new file mode 100644 index 000000000..92375ec8f --- /dev/null +++ b/compiler/src/main/java/dev/cel/compiler/tools/BUILD.bazel @@ -0,0 +1,33 @@ +load("@rules_java//java:defs.bzl", "java_binary") + +package( + default_applicable_licenses = [ + "//:license", + ], + default_visibility = [ + "//compiler/tools:__pkg__", + ], +) + +java_binary( + name = "cel_compiler_tool", + srcs = ["CelCompilerTool.java"], + main_class = "dev.cel.compiler.tools.CelCompilerTool", + neverlink = 1, + deps = [ + "//bundle:environment", + "//bundle:environment_exception", + "//bundle:environment_yaml_parser", + "//common:cel_ast", + "//common:cel_descriptor_util", + "//common:options", + "//common:proto_ast", + "//compiler", + "//compiler:compiler_builder", + "//parser:macro", + "@cel_spec//proto/cel/expr:checked_java_proto", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + "@maven//:info_picocli_picocli", + ], +) diff --git a/compiler/src/main/java/dev/cel/compiler/tools/CelCompilerTool.java b/compiler/src/main/java/dev/cel/compiler/tools/CelCompilerTool.java new file mode 100644 index 000000000..abe780c4a --- /dev/null +++ b/compiler/src/main/java/dev/cel/compiler/tools/CelCompilerTool.java @@ -0,0 +1,166 @@ +// 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.compiler.tools; + +import dev.cel.expr.CheckedExpr; +import com.google.common.collect.ImmutableSet; +import com.google.common.io.Files; +import com.google.protobuf.DescriptorProtos.FileDescriptorSet; +import com.google.protobuf.Descriptors.FileDescriptor; +import com.google.protobuf.ExtensionRegistry; +import dev.cel.bundle.CelEnvironment; +import dev.cel.bundle.CelEnvironmentException; +import dev.cel.bundle.CelEnvironmentYamlParser; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelDescriptorUtil; +import dev.cel.common.CelOptions; +import dev.cel.common.CelProtoAbstractSyntaxTree; +import dev.cel.compiler.CelCompiler; +import dev.cel.compiler.CelCompilerBuilder; +import dev.cel.compiler.CelCompilerFactory; +import dev.cel.parser.CelStandardMacro; +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; +import java.util.concurrent.Callable; +import picocli.CommandLine; +import picocli.CommandLine.Option; + +/** + * CelCompilerTool is a binary that takes a CEL expression in string, compiles it into a + * dev.cel.expr.CheckedExpr protobuf message, then writes the content to a .binary pb file. + */ +final class CelCompilerTool implements Callable { + + @Option( + names = {"--cel_expression"}, + description = "CEL expression") + private String celExpression = ""; + + @Option( + names = {"--environment_path"}, + description = "Path to the CEL environment (in YAML)") + private String celEnvironmentPath = ""; + + @Option( + names = {"--transitive_descriptor_set"}, + description = "Path to the transitive set of descriptors") + private String transitiveDescriptorSetPath = ""; + + @Option( + names = {"--output"}, + description = "Output path for the compiled binarypb") + private String output = ""; + + private static final CelOptions CEL_OPTIONS = CelOptions.DEFAULT; + + private static CelCompiler prepareCompiler( + String celEnvironmentPath, String transitiveDescriptorSetPath) + throws CelEnvironmentException, IOException { + CelCompilerBuilder celCompilerBuilder = + CelCompilerFactory.standardCelCompilerBuilder() + // TODO: Configure the below through YAML + .setOptions(CEL_OPTIONS) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS); + + if (!transitiveDescriptorSetPath.isEmpty()) { + ImmutableSet transitiveFileDescriptors = + CelDescriptorUtil.getFileDescriptorsFromFileDescriptorSet( + load(transitiveDescriptorSetPath)); + celCompilerBuilder.addFileTypes(transitiveFileDescriptors); + } + if (celEnvironmentPath.isEmpty()) { + return celCompilerBuilder.build(); + } + + if (!celEnvironmentPath.toLowerCase(Locale.getDefault()).trim().endsWith(".yaml")) { + throw new IllegalArgumentException( + "Only YAML extension is supported for CEL environments. Got: " + celEnvironmentPath); + } + + CelEnvironmentYamlParser environmentYamlParser = CelEnvironmentYamlParser.newInstance(); + String yamlContent = new String(readFileBytes(celEnvironmentPath), StandardCharsets.UTF_8); + CelEnvironment environment = environmentYamlParser.parse(yamlContent); + + return environment.extend(celCompilerBuilder.build(), CEL_OPTIONS); + } + + private static void writeCheckedExpr(CelAbstractSyntaxTree ast, String filePath) + throws IOException { + CheckedExpr checkedExpr = CelProtoAbstractSyntaxTree.fromCelAst(ast).toCheckedExpr(); + Path path = Paths.get(filePath); + try (FileOutputStream output = new FileOutputStream(path.toFile())) { + checkedExpr.writeTo(output); + } + } + + private static FileDescriptorSet load(String descriptorSetPath) { + try { + byte[] descriptorBytes = readFileBytes(descriptorSetPath); + return FileDescriptorSet.parseFrom(descriptorBytes, ExtensionRegistry.getEmptyRegistry()); + } catch (IOException e) { + throw new IllegalArgumentException( + "Failed to load FileDescriptorSet from path: " + descriptorSetPath, e); + } + } + + private static byte[] readFileBytes(String path) throws IOException { + return Files.toByteArray(new File(path)); + } + + @Override + public Integer call() { + CelCompiler celCompiler; + try { + celCompiler = prepareCompiler(celEnvironmentPath, transitiveDescriptorSetPath); + } catch (Exception e) { + String errorMessage = + String.format( + "Failed to create a CEL compilation environment. Reason: %s", e.getMessage()); + System.err.print(errorMessage); + return -1; + } + + try { + CelAbstractSyntaxTree ast = celCompiler.compile(celExpression).getAst(); + writeCheckedExpr(ast, output); + } catch (Exception e) { + String errorMessage = + String.format( + "\nFailed to compile CEL Expression: [%s].\nReason: %s\n\n", + celExpression, e.getMessage()); + System.err.print(errorMessage); + return -1; + } + + return 0; + } + + public static void main(String[] args) { + CelCompilerTool compilerTool = new CelCompilerTool(); + CommandLine cmd = new CommandLine(compilerTool); + cmd.setTrimQuotes(false); + cmd.parseArgs(args); + + int exitCode = cmd.execute(args); + System.exit(exitCode); + } + + CelCompilerTool() {} +} diff --git a/compiler/src/test/java/dev/cel/compiler/tools/BUILD.bazel b/compiler/src/test/java/dev/cel/compiler/tools/BUILD.bazel new file mode 100644 index 000000000..c388ea3ea --- /dev/null +++ b/compiler/src/test/java/dev/cel/compiler/tools/BUILD.bazel @@ -0,0 +1,36 @@ +load("@rules_java//java:defs.bzl", "java_library") +load("//:testing.bzl", "junit4_test_suites") + +package( + default_applicable_licenses = ["//:license"], + default_testonly = True, +) + +java_library( + name = "tests", + testonly = True, + srcs = glob(["*Test.java"]), + deps = [ + "//:java_truth", + "//common:cel_ast", + "//common:options", + "//extensions", + "//extensions:optional_library", + "//runtime", + "//runtime:function_binding", + "//testing/compiled:compiled_expr_utils", + "@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", + ], +) + +junit4_test_suites( + name = "test_suites", + sizes = [ + "small", + ], + src_dir = "src/test/java", + deps = [":tests"], +) diff --git a/compiler/src/test/java/dev/cel/compiler/tools/CelCompilerToolTest.java b/compiler/src/test/java/dev/cel/compiler/tools/CelCompilerToolTest.java new file mode 100644 index 000000000..ed3d8b473 --- /dev/null +++ b/compiler/src/test/java/dev/cel/compiler/tools/CelCompilerToolTest.java @@ -0,0 +1,99 @@ +// 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.compiler.tools; + +import static com.google.common.truth.Truth.assertThat; +import static dev.cel.testing.compiled.CompiledExprUtils.readCheckedExpr; + +import com.google.common.collect.ImmutableMap; +import com.google.protobuf.StringValue; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelOptions; +import dev.cel.expr.conformance.proto3.TestAllTypes; +import dev.cel.extensions.CelExtensions; +import dev.cel.extensions.CelOptionalLibrary; +import dev.cel.runtime.CelRuntime; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.CelRuntimeFactory; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class CelCompilerToolTest { + private static final CelRuntime CEL_RUNTIME = + CelRuntimeFactory.standardCelRuntimeBuilder() + .addFunctionBindings( + CelFunctionBinding.from("wrapper_string_isEmpty", String.class, String::isEmpty)) + .addLibraries( + CelExtensions.encoders(), + CelExtensions.math(CelOptions.DEFAULT), + CelExtensions.lists(), + CelExtensions.strings(), + CelOptionalLibrary.INSTANCE) + .addMessageTypes(TestAllTypes.getDescriptor()) + .build(); + + @Test + public void compiledCheckedExpr_string() throws Exception { + CelAbstractSyntaxTree ast = readCheckedExpr("compiled_hello_world"); + + String result = (String) CEL_RUNTIME.createProgram(ast).eval(); + assertThat(result).isEqualTo("hello world"); + } + + @Test + @SuppressWarnings("unchecked") + public void compiledCheckedExpr_comprehension() throws Exception { + CelAbstractSyntaxTree ast = readCheckedExpr("compiled_comprehension"); + + List result = (List) CEL_RUNTIME.createProgram(ast).eval(); + assertThat(result).containsExactly(2L, 3L, 4L).inOrder(); + } + + @Test + public void compiledCheckedExpr_protoMessage() throws Exception { + CelAbstractSyntaxTree ast = readCheckedExpr("compiled_proto_message"); + + TestAllTypes result = (TestAllTypes) CEL_RUNTIME.createProgram(ast).eval(); + assertThat(result).isEqualTo(TestAllTypes.newBuilder().setSingleInt32(1).build()); + } + + @Test + public void compiledCheckedExpr_extensions() throws Exception { + CelAbstractSyntaxTree ast = readCheckedExpr("compiled_extensions"); + + assertThat(CEL_RUNTIME.createProgram(ast).eval()).isEqualTo(true); + } + + @Test + public void compiledCheckedExpr_extended_env() throws Exception { + CelAbstractSyntaxTree ast = readCheckedExpr("compiled_extended_env"); + + boolean result = + (boolean) + CEL_RUNTIME + .createProgram(ast) + .eval( + ImmutableMap.of( + "msg", + TestAllTypes.newBuilder() + .setSingleStringWrapper(StringValue.of("foo")) + .build())); + + assertThat(result).isTrue(); + } +} diff --git a/compiler/tools/BUILD.bazel b/compiler/tools/BUILD.bazel new file mode 100644 index 000000000..1cf5154e0 --- /dev/null +++ b/compiler/tools/BUILD.bazel @@ -0,0 +1,9 @@ +package( + default_applicable_licenses = ["//:license"], + default_visibility = ["//:android_allow_list"], +) + +alias( + name = "cel_compiler_tool", + actual = "//compiler/src/main/java/dev/cel/compiler/tools:cel_compiler_tool", +) diff --git a/compiler/tools/compile_cel.bzl b/compiler/tools/compile_cel.bzl new file mode 100644 index 000000000..a428850f9 --- /dev/null +++ b/compiler/tools/compile_cel.bzl @@ -0,0 +1,71 @@ +# 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. +"""Rule for compiling CEL expressions at build time. +""" + +load("@rules_proto//proto:defs.bzl", "proto_descriptor_set") + +def compile_cel( + name, + expression, + proto_srcs = [], + environment = None, + output = None): + """Compiles a CEL expression, generating a cel.expr.CheckedExpr proto. This proto is written to a `.binarypb` file. + + Args: + name: str name for the generated artifact + expression: str CEL expression to compile + proto_srcs: (optional) list of str label(s) pointing to a proto_library rule (important: NOT java_proto_library). This must be provided when compiling a CEL expression containing protobuf messages. + environment: (optional) str label or filename pointing to a YAML file that describes a CEL environment. + output: (optional) str file name for the output checked expression. `.binarypb` extension is automatically appended in the filename. + """ + + args = [] + genrule_srcs = [] + + args.append("--cel_expression \"%s\" " % expression) + + if output == None: + output = name + + output = output + ".binarypb" + args.append("--output $(location %s) " % output) + + if len(proto_srcs) > 0: + transitive_descriptor_set_name = "%s_transitive_descriptor_set" % name + proto_descriptor_set( + name = transitive_descriptor_set_name, + deps = proto_srcs, + ) + args.append("--transitive_descriptor_set $(location %s) " % transitive_descriptor_set_name) + genrule_srcs.append(transitive_descriptor_set_name) + + if environment != None: + args.append("--environment_path=$(location {})".format(environment)) + genrule_srcs.append(environment) + + arg_str = " ".join(args) + cmd = ( + "$(location //compiler/tools:cel_compiler_tool) " + + arg_str + ) + + native.genrule( + name = name, + cmd = cmd, + srcs = genrule_srcs, + outs = [output], + tools = ["//compiler/tools:cel_compiler_tool"], + ) diff --git a/conformance/src/test/java/dev/cel/conformance/BUILD.bazel b/conformance/src/test/java/dev/cel/conformance/BUILD.bazel new file mode 100644 index 000000000..e9ed58642 --- /dev/null +++ b/conformance/src/test/java/dev/cel/conformance/BUILD.bazel @@ -0,0 +1,209 @@ +load("@rules_java//java:defs.bzl", "java_library") +load("//conformance/src/test/java/dev/cel/conformance:conformance_test.bzl", "MODE", "conformance_test") + +package(default_applicable_licenses = [ + "//:license", +]) + +exports_files([ + "conformance_test.bzl", + "conformance_test.sh", +]) + +java_library( + name = "run", + testonly = True, + srcs = glob(["*.java"]), + deps = [ + "//:java_truth", + "//checker:checker_builder", + "//common:compiler_common", + "//common:container", + "//common:options", + "//common/types:cel_proto_types", + "//compiler", + "//compiler:compiler_builder", + "//extensions", + "//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", + "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", + "@cel_spec//proto/cel/expr/conformance/test:simple_java_proto", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + "@maven//:com_google_truth_extensions_truth_proto_extension", + "@maven//:junit_junit", + ], +) + +MAVEN_JAR_DEPS = [ + "@maven_conformance//:dev_cel_compiler", + "@maven_conformance//:dev_cel_common", + "@maven_conformance//:dev_cel_runtime", + "@maven_conformance//:dev_cel_protobuf", + "@maven_conformance//:dev_cel_cel", +] + +java_library( + name = "run_maven_jar", + testonly = True, + srcs = glob(["*.java"]), + tags = ["conformance_maven"], + 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", + "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", + "@cel_spec//proto/cel/expr/conformance/test:simple_java_proto", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + "@maven//:com_google_truth_extensions_truth_proto_extension", + "@maven//:junit_junit", + ], +) + +_ALL_TESTS = [ + "@cel_spec//tests/simple:testdata/basic.textproto", + "@cel_spec//tests/simple:testdata/bindings_ext.textproto", + "@cel_spec//tests/simple:testdata/comparisons.textproto", + "@cel_spec//tests/simple:testdata/conversions.textproto", + "@cel_spec//tests/simple:testdata/dynamic.textproto", + "@cel_spec//tests/simple:testdata/encoders_ext.textproto", + "@cel_spec//tests/simple:testdata/enums.textproto", + "@cel_spec//tests/simple:testdata/fields.textproto", + "@cel_spec//tests/simple:testdata/fp_math.textproto", + "@cel_spec//tests/simple:testdata/integer_math.textproto", + "@cel_spec//tests/simple:testdata/lists.textproto", + "@cel_spec//tests/simple:testdata/logic.textproto", + "@cel_spec//tests/simple:testdata/macros.textproto", + "@cel_spec//tests/simple:testdata/macros2.textproto", + "@cel_spec//tests/simple:testdata/math_ext.textproto", + "@cel_spec//tests/simple:testdata/namespace.textproto", + "@cel_spec//tests/simple:testdata/optionals.textproto", + "@cel_spec//tests/simple:testdata/parse.textproto", + "@cel_spec//tests/simple:testdata/plumbing.textproto", + "@cel_spec//tests/simple:testdata/proto2.textproto", + "@cel_spec//tests/simple:testdata/proto3.textproto", + "@cel_spec//tests/simple:testdata/proto2_ext.textproto", + "@cel_spec//tests/simple:testdata/string.textproto", + "@cel_spec//tests/simple:testdata/string_ext.textproto", + "@cel_spec//tests/simple:testdata/timestamps.textproto", + "@cel_spec//tests/simple:testdata/type_deduction.textproto", + "@cel_spec//tests/simple:testdata/unknowns.textproto", + "@cel_spec//tests/simple:testdata/wrappers.textproto", +] + +_TESTS_TO_SKIP_LEGACY = [ + # Broken test cases which should be supported. + # TODO: Support setting / getting enum values out of the defined enum value range. + "enums/legacy_proto2/select_big,select_neg", + "enums/legacy_proto2/assign_standalone_int_big,assign_standalone_int_neg", + # TODO: Generate errors on enum value assignment overflows for proto3. + "enums/legacy_proto3/assign_standalone_int_too_big,assign_standalone_int_too_neg", + # TODO: Ensure overflow occurs on conversions of double values which might not work properly on all platforms. + "conversions/int/double_int_min_range", + # TODO: Duration and timestamp operations should error on overflow. + "timestamps/timestamp_range/sub_time_duration_over,sub_time_duration_under", + # TODO: Ensure adding negative duration values is appropriately supported. + "timestamps/timestamp_arithmetic/add_time_to_duration_nanos_negative", + + # Skip until fixed. + "fields/qualified_identifier_resolution/map_value_repeat_key_heterogeneous", + # TODO: Add strings.format.quote. + "string_ext/format", + "string_ext/format_errors", + + # Future features for CEL 1.0 + # TODO: Strong typing support for enums, specified but not implemented. + "enums/strong_proto2", + "enums/strong_proto3", + + # com.google.protobuf.TextFormat does not conform to the spec. Unknown enum values are supposed + # to be allowed in proto3. Currently they are rejected. + # "enums/legacy_proto3/select_big", + # "enums/legacy_proto3/select_neg", + # "enums/legacy_proto3/assign_standalone_int_big", + # "enums/legacy_proto3/assign_standalone_int_neg", + + # 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", +] + +_TESTS_TO_SKIP_PLANNER = [ + # TODO: Add strings.format. + "string_ext/format", + "string_ext/format_errors", + + # TODO: Check behavior for go/cpp + "basic/functions/unbound_is_runtime_error", + + # TODO: Ensure overflow occurs on conversions of double values which might not work properly on all platforms. + "conversions/int/double_int_min_range", + "enums/legacy_proto3/assign_standalone_int_too_big", + "enums/legacy_proto3/assign_standalone_int_too_neg", + + # TODO: Duration and timestamp operations should error on overflow. + "timestamps/timestamp_range/sub_time_duration_over", + "timestamps/timestamp_range/sub_time_duration_under", + + # Skip until fixed. + "parse/receiver_function_names", + + # 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_LEGACY, +) + +conformance_test( + name = "conformance_maven", + data = _ALL_TESTS, + mode = MODE.MAVEN_TEST, + skip_tests = _TESTS_TO_SKIP_LEGACY, +) + +conformance_test( + name = "conformance_dashboard", + 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 new file mode 100644 index 000000000..db57ccb79 --- /dev/null +++ b/conformance/src/test/java/dev/cel/conformance/ConformanceTest.java @@ -0,0 +1,265 @@ +// 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. + +package dev.cel.conformance; + +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.fromValue; +import static dev.cel.testing.utils.ExprValueUtils.toExprValue; + +import dev.cel.expr.Decl; +import dev.cel.expr.ExprValue; +import dev.cel.expr.MapValue; +import dev.cel.expr.Type; +import dev.cel.expr.Value; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.protobuf.ExtensionRegistry; +import com.google.protobuf.TypeRegistry; +import dev.cel.checker.CelChecker; +import dev.cel.common.CelContainer; +import dev.cel.common.CelOptions; +import dev.cel.common.CelValidationResult; +import dev.cel.common.types.CelProtoTypes; +import dev.cel.compiler.CelCompilerFactory; +import dev.cel.compiler.CelCompilerLibrary; +import dev.cel.expr.conformance.test.SimpleTest; +import dev.cel.extensions.CelExtensions; +import dev.cel.extensions.CelOptionalLibrary; +import dev.cel.parser.CelParser; +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 org.junit.runners.model.Statement; + +// Qualifying proto2/proto3 TestAllTypes makes it less clear. +@SuppressWarnings("UnnecessarilyFullyQualified") +public final class ConformanceTest extends Statement { + + private static final CelOptions OPTIONS = + CelOptions.current() + .enableHeterogeneousNumericComparisons(true) + .enableProtoDifferencerEquality(true) + .enableOptionalSyntax(true) + .enableQuotedIdentifierSyntax(true) + .build(); + + private static final ImmutableList CANONICAL_COMPILER_EXTENSIONS = + ImmutableList.of( + CelExtensions.bindings(), + CelExtensions.comprehensions(), + CelExtensions.encoders(OPTIONS), + CelExtensions.math(OPTIONS), + CelExtensions.protos(), + CelExtensions.sets(OPTIONS), + CelExtensions.strings(), + CelOptionalLibrary.INSTANCE); + + private static final ImmutableList CANONICAL_RUNTIME_EXTENSIONS = + ImmutableList.of( + CelExtensions.comprehensions(), + CelExtensions.encoders(OPTIONS), + CelExtensions.math(OPTIONS), + CelExtensions.sets(OPTIONS), + 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) + .addLibraries(CANONICAL_COMPILER_EXTENSIONS) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .build(); + + private static final CelParser PARSER_WITHOUT_MACROS = + CelParserFactory.standardCelParserBuilder() + .setOptions(OPTIONS) + .addLibraries(CANONICAL_COMPILER_EXTENSIONS) + .setStandardMacros() + .build(); + + private static CelParser getParser(SimpleTest test) { + return test.getDisableMacros() ? PARSER_WITHOUT_MACROS : PARSER_WITH_MACROS; + } + + 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(), CONFORMANCE_EXTENSION_REGISTRY)); + } + return CelCompilerFactory.standardCelCheckerBuilder() + .setOptions(OPTIONS) + .setContainer(CelContainer.ofName(test.getContainer())) + .addDeclarations(decls.build()) + .addFileTypes(dev.cel.expr.conformance.proto2.TestAllTypesExtensions.getDescriptor()) + .addLibraries(CANONICAL_COMPILER_EXTENSIONS) + .addMessageTypes(dev.cel.expr.conformance.proto2.TestAllTypes.getDescriptor()) + .addMessageTypes(dev.cel.expr.conformance.proto3.TestAllTypes.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 = + ImmutableMap.builderWithExpectedSize(test.getBindingsCount()); + for (Map.Entry entry : test.getBindingsMap().entrySet()) { + bindings.put(entry.getKey(), fromExprValue(entry.getValue())); + } + return bindings.buildOrThrow(); + } + + private static Object fromExprValue(ExprValue value) throws Exception { + switch (value.getKindCase()) { + case VALUE: + return fromValue( + value.getValue(), CONFORMANCE_TYPE_REGISTRY, CONFORMANCE_EXTENSION_REGISTRY); + default: + throw new IllegalArgumentException( + String.format("Unexpected binding value kind: %s", value.getKindCase())); + } + } + + private static SimpleTest defaultTestMatcherToTrueIfUnset(SimpleTest test) { + if (test.getResultMatcherCase() == SimpleTest.ResultMatcherCase.RESULTMATCHER_NOT_SET) { + return test.toBuilder().setValue(Value.newBuilder().setBoolValue(true).build()).build(); + } + return 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, boolean usePlanner) { + this.name = Preconditions.checkNotNull(name); + this.test = + Preconditions.checkNotNull( + defaultTestMatcherToTrueIfUnset(Preconditions.checkNotNull(test))); + this.skip = skip; + this.usePlanner = usePlanner; + } + + public String getName() { + return name; + } + + public boolean shouldSkip() { + return skip; + } + + @Override + public void evaluate() throws Throwable { + CelValidationResult response = getParser(test).parse(test.getExpr(), test.getName()); + assertThat(response.hasError()).isFalse(); + if (!test.getDisableCheck()) { + response = getChecker(test).check(response.getAst()); + } + assertThat(response.hasError()).isFalse(); + Type resultType = CelProtoTypes.celTypeToType(response.getAst().getResultType()); + + if (test.getCheckOnly()) { + assertThat(test.hasTypedResult()).isTrue(); + assertThat(resultType).isEqualTo(test.getTypedResult().getDeducedType()); + return; + } + + 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; + } + switch (test.getResultMatcherCase()) { + case VALUE: + assertThat(error).isNull(); + assertThat(result).isNotNull(); + assertThat(result) + .ignoringRepeatedFieldOrderOfFieldDescriptors( + MapValue.getDescriptor().findFieldByName("entries")) + .unpackingAnyUsing(CONFORMANCE_TYPE_REGISTRY, CONFORMANCE_EXTENSION_REGISTRY) + .isEqualTo(ExprValue.newBuilder().setValue(test.getValue()).build()); + break; + case EVAL_ERROR: + assertThat(result).isNull(); + assertThat(error).isNotNull(); + break; + case TYPED_RESULT: + assertThat(error).isNull(); + assertThat(result).isNotNull(); + assertThat(result) + .ignoringRepeatedFieldOrderOfFieldDescriptors( + MapValue.getDescriptor().findFieldByName("entries")) + .unpackingAnyUsing(CONFORMANCE_TYPE_REGISTRY, CONFORMANCE_EXTENSION_REGISTRY) + .isEqualTo(ExprValue.newBuilder().setValue(test.getTypedResult().getResult()).build()); + assertThat(resultType).isEqualTo(test.getTypedResult().getDeducedType()); + break; + default: + throw new IllegalStateException( + String.format("Unexpected matcher kind: %s", test.getResultMatcherCase())); + } + } +} diff --git a/conformance/src/test/java/dev/cel/conformance/ConformanceTestRunner.java b/conformance/src/test/java/dev/cel/conformance/ConformanceTestRunner.java new file mode 100644 index 000000000..4c3631d31 --- /dev/null +++ b/conformance/src/test/java/dev/cel/conformance/ConformanceTestRunner.java @@ -0,0 +1,132 @@ +// 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. + +package dev.cel.conformance; + +import static dev.cel.conformance.ConformanceTest.CONFORMANCE_EXTENSION_REGISTRY; + +import com.google.common.base.Preconditions; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSortedMap; +import com.google.protobuf.TextFormat; +import dev.cel.expr.conformance.test.SimpleTest; +import dev.cel.expr.conformance.test.SimpleTestFile; +import dev.cel.expr.conformance.test.SimpleTestSection; +import java.io.BufferedReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +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; +import org.junit.runners.model.TestClass; + +public final class ConformanceTestRunner extends ParentRunner { + + private static final Splitter SPLITTER = Splitter.on(",").omitEmptyStrings(); + + 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(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, CONFORMANCE_EXTENSION_REGISTRY, fileBuilder); + } + SimpleTestFile testFile = fileBuilder.build(); + testFiles.put(testFile.getName(), testFile); + } + return testFiles.buildOrThrow(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public ConformanceTestRunner(Class clazz) throws InitializationError { + super(new TestClass(clazz)); + Preconditions.checkArgument(ConformanceTests.class.equals(clazz)); + testFiles = loadTestFiles(); + testsToSkip = + 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) { + for (String testToSkip : testsToSkip) { + if (name.startsWith(testToSkip)) { + String consumedName = name.substring(testToSkip.length()); + if (consumedName.isEmpty() || consumedName.startsWith("/")) { + return true; + } + } + } + return false; + } + + @Override + protected List getChildren() { + ArrayList tests = new ArrayList<>(); + for (SimpleTestFile testFile : testFiles.values()) { + for (SimpleTestSection testSection : testFile.getSectionList()) { + for (SimpleTest test : testSection.getTestList()) { + String name = + String.format("%s/%s/%s", testFile.getName(), testSection.getName(), test.getName()); + tests.add(new ConformanceTest(name, test, shouldSkipTest(name), usePlanner)); + } + } + } + return tests; + } + + @Override + protected Description describeChild(ConformanceTest child) { + return Description.createTestDescription( + ConformanceTests.class, child.getName(), ConformanceTest.class.getAnnotations()); + } + + @Override + protected void runChild(ConformanceTest child, RunNotifier notifier) { + Description desc = describeChild(child); + if (isIgnored(child)) { + notifier.fireTestIgnored(desc); + } else { + runLeaf(child, desc, notifier); + } + } + + @Override + protected boolean isIgnored(ConformanceTest child) { + return child.shouldSkip(); + } +} diff --git a/conformance/src/test/java/dev/cel/conformance/ConformanceTests.java b/conformance/src/test/java/dev/cel/conformance/ConformanceTests.java new file mode 100644 index 000000000..58de114ba --- /dev/null +++ b/conformance/src/test/java/dev/cel/conformance/ConformanceTests.java @@ -0,0 +1,20 @@ +// 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. + +package dev.cel.conformance; + +import org.junit.runner.RunWith; + +@RunWith(ConformanceTestRunner.class) +public class ConformanceTests {} diff --git a/conformance/src/test/java/dev/cel/conformance/conformance_test.bzl b/conformance/src/test/java/dev/cel/conformance/conformance_test.bzl new file mode 100644 index 000000000..b91ce4a8b --- /dev/null +++ b/conformance/src/test/java/dev/cel/conformance/conformance_test.bzl @@ -0,0 +1,116 @@ +# 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. + +""" +This module contains build rules for generating the conformance test targets. +""" + +load("@rules_java//java:defs.bzl", "java_test") +load("@rules_shell//shell:sh_test.bzl", "sh_test") + +# Converts the list of tests to skip from the format used by the original Go test runner to a single +# flag value where each test is separated by a comma. It also performs expansion, for example +# `foo/bar,baz` becomes two entries which are `foo/bar` and `foo/baz`. +def _expand_tests_to_skip(tests_to_skip): + result = [] + for test_to_skip in tests_to_skip: + comma = test_to_skip.find(",") + if comma == -1: + result.append(test_to_skip) + continue + slash = test_to_skip.rfind("/", 0, comma) + if slash == -1: + slash = 0 + else: + slash = slash + 1 + for part in test_to_skip[slash:].split(","): + result.append(test_to_skip[0:slash] + part) + return result + +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 + TEST = "test", + # Executes conformance test against published jar in maven central + MAVEN_TEST = "maven_test", + # Dashboard mode + DASHBOARD = "dashboard", +) + +def conformance_test(name, data, mode = MODE.TEST, skip_tests = [], use_planner = False): + """Executes conformance tests + + Args: + name: unique label for the java_test + data: A list of test data files + mode: An enum that determines the test configuration. + - `MODE.TEST` (default): Runs the conformance tests + - `MODE.DASHBOARD`: Runs the conformance tests for displaying on dashboard. + - `MODE.MAVEN_TEST`: Runs the conformance tests against published JAR in maven central. + skip_tests: A list of strings, where each string is the name of a test file to + exclude from the run. + """ + if mode == MODE.DASHBOARD: + java_test( + name = "_" + name, + jvm_flags = _conformance_test_args(data, skip_tests, use_planner), + data = data, + size = "small", + test_class = "dev.cel.conformance.ConformanceTests", + runtime_deps = ["//conformance/src/test/java/dev/cel/conformance:run"], + tags = [ + "manual", + "notap", + ], + ) + + sh_test( + name = name, + size = "small", + srcs = ["//conformance/src/test/java/dev/cel/conformance:conformance_test.sh"], + args = ["$(location :_" + name + ")"], + data = [":_" + name], + tags = [ + "guitar", + "manual", + "notap", + ], + ) + elif mode == MODE.TEST: + java_test( + name = name, + jvm_flags = _conformance_test_args(data, skip_tests, use_planner), + data = data, + size = "small", + test_class = "dev.cel.conformance.ConformanceTests", + runtime_deps = ["//conformance/src/test/java/dev/cel/conformance:run"], + ) + elif mode == MODE.MAVEN_TEST: + java_test( + name = name, + jvm_flags = _conformance_test_args(data, skip_tests, use_planner), + data = data, + size = "small", + test_class = "dev.cel.conformance.ConformanceTests", + tags = ["conformance_maven"], + runtime_deps = ["//conformance/src/test/java/dev/cel/conformance:run_maven_jar"], + ) + else: + fail("Unknown mode specified: %s." % mode) diff --git a/conformance/src/test/java/dev/cel/conformance/conformance_test.sh b/conformance/src/test/java/dev/cel/conformance/conformance_test.sh new file mode 100755 index 000000000..c00f21c7a --- /dev/null +++ b/conformance/src/test/java/dev/cel/conformance/conformance_test.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +(exec "$@") +rc = $? +if [ $rc -eq 1 ]; then + rc = 0 +fi +exit $rc 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..d7851bb72 --- /dev/null +++ b/conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTest.java @@ -0,0 +1,114 @@ +// 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 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.fromOverloads( + "locationCode", + CelFunctionBinding.from( + "locationCode_string", + String.class, + (ip) -> { + switch (ip) { + case "10.0.0.1": + return "us"; + case "10.0.0.2": + return "de"; + default: + return "ir"; + } + }))) + .build(); + + private final String name; + private final CelTestCase testCase; + 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 new file mode 100644 index 000000000..895339521 --- /dev/null +++ b/conformance/src/test/java/dev/cel/maven/BUILD.bazel @@ -0,0 +1,47 @@ +load("@rules_java//java:defs.bzl", "java_test") + +package(default_applicable_licenses = [ + "//:license", +]) + +# keep sorted +MAVEN_COMPILER_JAR_DEPS = [ + "@maven_conformance//:dev_cel_common", + "@maven_conformance//:dev_cel_compiler", +] + +# keep sorted +MAVEN_RUNTIME_JAR_DEPS = [ + "@maven_conformance//:dev_cel_common", + "@maven_conformance//:dev_cel_protobuf", + "@maven_conformance//:dev_cel_runtime", +] + +java_test( + name = "compiler_artifact_test", + srcs = ["CompilerArtifactTest.java"], + test_class = "dev.cel.maven.CompilerArtifactTest", + deps = + MAVEN_COMPILER_JAR_DEPS + [ + "//:java_truth", + "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", + "@maven//:com_google_testparameterinjector_test_parameter_injector", + "@maven//:junit_junit", + ], +) + +java_test( + name = "runtime_artifact_test", + srcs = ["RuntimeArtifactTest.java"], + test_class = "dev.cel.maven.RuntimeArtifactTest", + deps = + MAVEN_RUNTIME_JAR_DEPS + [ + "//:java_truth", + "@cel_spec//proto/cel/expr:checked_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//:com_google_testparameterinjector_test_parameter_injector", + "@maven//:junit_junit", + ], +) diff --git a/conformance/src/test/java/dev/cel/maven/CompilerArtifactTest.java b/conformance/src/test/java/dev/cel/maven/CompilerArtifactTest.java new file mode 100644 index 000000000..dcdedb157 --- /dev/null +++ b/conformance/src/test/java/dev/cel/maven/CompilerArtifactTest.java @@ -0,0 +1,106 @@ +// 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.maven; + +import static com.google.common.truth.Truth.assertThat; +import static dev.cel.common.CelFunctionDecl.newFunctionDeclaration; +import static dev.cel.common.CelOverloadDecl.newGlobalOverload; +import static org.junit.Assert.assertThrows; + +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.checker.CelChecker; +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.ast.CelConstant; +import dev.cel.common.ast.CelExpr; +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.compiler.CelCompilerFactory; +import dev.cel.expr.conformance.proto3.TestAllTypes; +import dev.cel.parser.CelParser; +import dev.cel.parser.CelParserFactory; +import dev.cel.parser.CelStandardMacro; +import dev.cel.parser.CelUnparser; +import dev.cel.parser.CelUnparserFactory; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public class CompilerArtifactTest { + + @Test + public void parse() throws Exception { + CelParser parser = CelParserFactory.standardCelParserBuilder().build(); + CelUnparser unparser = CelUnparserFactory.newUnparser(); + + CelAbstractSyntaxTree ast = parser.parse("'Hello World'").getAst(); + + assertThat(ast.getExpr()).isEqualTo(CelExpr.ofConstant(1L, CelConstant.ofValue("Hello World"))); + assertThat(unparser.unparse(ast)).isEqualTo("\"Hello World\""); + } + + @Test + public void typeCheck() throws Exception { + CelParser parser = CelParserFactory.standardCelParserBuilder().build(); + CelChecker checker = CelCompilerFactory.standardCelCheckerBuilder().build(); + + CelAbstractSyntaxTree ast = checker.check(parser.parse("'Hello World'").getAst()).getAst(); + + assertThat(ast.getResultType()).isEqualTo(SimpleType.STRING); + } + + @Test + public void compile() throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addFunctionDeclarations( + newFunctionDeclaration( + "getThree", newGlobalOverload("getThree_overload", SimpleType.INT))) + .setOptions(CelOptions.DEFAULT) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .setTypeProvider( + ProtoMessageTypeProvider.newBuilder() + .addFileDescriptors(TestAllTypes.getDescriptor().getFile()) + .build()) + .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) + .addVar("msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())) + .build(); + CelUnparser unparser = CelUnparserFactory.newUnparser(); + + CelAbstractSyntaxTree ast = + compiler.compile("msg == TestAllTypes{} && 3 == getThree()").getAst(); + + assertThat(unparser.unparse(ast)) + .isEqualTo("msg == cel.expr.conformance.proto3.TestAllTypes{} && 3 == getThree()"); + assertThat(ast.getResultType()).isEqualTo(SimpleType.BOOL); + } + + @Test + public void compile_error() { + CelCompiler compiler = CelCompilerFactory.standardCelCompilerBuilder().build(); + + CelValidationResult result = compiler.compile("'foo' + 1"); + + assertThat(result.hasError()).isTrue(); + assertThat(assertThrows(CelValidationException.class, result::getAst)) + .hasMessageThat() + .contains("found no matching overload for '_+_' applied to '(string, int)'"); + } +} diff --git a/conformance/src/test/java/dev/cel/maven/RuntimeArtifactTest.java b/conformance/src/test/java/dev/cel/maven/RuntimeArtifactTest.java new file mode 100644 index 000000000..e129b85d3 --- /dev/null +++ b/conformance/src/test/java/dev/cel/maven/RuntimeArtifactTest.java @@ -0,0 +1,413 @@ +// 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.maven; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import dev.cel.expr.CheckedExpr; +import com.google.common.collect.ImmutableList; +import com.google.protobuf.TextFormat; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelOptions; +import dev.cel.common.CelProtoAbstractSyntaxTree; +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.runtime.CelStandardFunctions; +import dev.cel.runtime.CelStandardFunctions.StandardFunction; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public class RuntimeArtifactTest { + + // Serialized expr: [TestAllTypes{single_int64: 1}.single_int64, 2].exists(x, x == 2) + private static final String CHECKED_EXPR = + "reference_map {\n" + + " key: 2\n" + + " value {\n" + + " name: \"cel.expr.conformance.proto3.TestAllTypes\"\n" + + " }\n" + + "}\n" + + "reference_map {\n" + + " key: 7\n" + + " value {\n" + + " overload_id: \"getThree_overload\"\n" + + " }\n" + + "}\n" + + "reference_map {\n" + + " key: 10\n" + + " value {\n" + + " name: \"x\"\n" + + " }\n" + + "}\n" + + "reference_map {\n" + + " key: 11\n" + + " value {\n" + + " overload_id: \"greater_equals_int64\"\n" + + " }\n" + + "}\n" + + "reference_map {\n" + + " key: 14\n" + + " value {\n" + + " name: \"@result\"\n" + + " }\n" + + "}\n" + + "reference_map {\n" + + " key: 15\n" + + " value {\n" + + " overload_id: \"not_strictly_false\"\n" + + " }\n" + + "}\n" + + "reference_map {\n" + + " key: 16\n" + + " value {\n" + + " name: \"@result\"\n" + + " }\n" + + "}\n" + + "reference_map {\n" + + " key: 17\n" + + " value {\n" + + " overload_id: \"logical_and\"\n" + + " }\n" + + "}\n" + + "reference_map {\n" + + " key: 18\n" + + " value {\n" + + " name: \"@result\"\n" + + " }\n" + + "}\n" + + "type_map {\n" + + " key: 1\n" + + " value {\n" + + " list_type {\n" + + " elem_type {\n" + + " primitive: INT64\n" + + " }\n" + + " }\n" + + " }\n" + + "}\n" + + "type_map {\n" + + " key: 2\n" + + " value {\n" + + " message_type: \"cel.expr.conformance.proto3.TestAllTypes\"\n" + + " }\n" + + "}\n" + + "type_map {\n" + + " key: 4\n" + + " value {\n" + + " primitive: INT64\n" + + " }\n" + + "}\n" + + "type_map {\n" + + " key: 5\n" + + " value {\n" + + " primitive: INT64\n" + + " }\n" + + "}\n" + + "type_map {\n" + + " key: 6\n" + + " value {\n" + + " primitive: INT64\n" + + " }\n" + + "}\n" + + "type_map {\n" + + " key: 7\n" + + " value {\n" + + " primitive: INT64\n" + + " }\n" + + "}\n" + + "type_map {\n" + + " key: 10\n" + + " value {\n" + + " primitive: INT64\n" + + " }\n" + + "}\n" + + "type_map {\n" + + " key: 11\n" + + " value {\n" + + " primitive: BOOL\n" + + " }\n" + + "}\n" + + "type_map {\n" + + " key: 12\n" + + " value {\n" + + " primitive: INT64\n" + + " }\n" + + "}\n" + + "type_map {\n" + + " key: 13\n" + + " value {\n" + + " primitive: BOOL\n" + + " }\n" + + "}\n" + + "type_map {\n" + + " key: 14\n" + + " value {\n" + + " primitive: BOOL\n" + + " }\n" + + "}\n" + + "type_map {\n" + + " key: 15\n" + + " value {\n" + + " primitive: BOOL\n" + + " }\n" + + "}\n" + + "type_map {\n" + + " key: 16\n" + + " value {\n" + + " primitive: BOOL\n" + + " }\n" + + "}\n" + + "type_map {\n" + + " key: 17\n" + + " value {\n" + + " primitive: BOOL\n" + + " }\n" + + "}\n" + + "type_map {\n" + + " key: 18\n" + + " value {\n" + + " primitive: BOOL\n" + + " }\n" + + "}\n" + + "type_map {\n" + + " key: 19\n" + + " value {\n" + + " primitive: BOOL\n" + + " }\n" + + "}\n" + + "expr {\n" + + " id: 19\n" + + " comprehension_expr {\n" + + " iter_var: \"x\"\n" + + " iter_range {\n" + + " id: 1\n" + + " list_expr {\n" + + " elements {\n" + + " id: 5\n" + + " select_expr {\n" + + " operand {\n" + + " id: 2\n" + + " struct_expr {\n" + + " message_name: \"cel.expr.conformance.proto3.TestAllTypes\"\n" + + " entries {\n" + + " id: 3\n" + + " field_key: \"single_int64\"\n" + + " value {\n" + + " id: 4\n" + + " const_expr {\n" + + " int64_value: 1\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " field: \"single_int64\"\n" + + " }\n" + + " }\n" + + " elements {\n" + + " id: 6\n" + + " const_expr {\n" + + " int64_value: 2\n" + + " }\n" + + " }\n" + + " elements {\n" + + " id: 7\n" + + " call_expr {\n" + + " function: \"getThree\"\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " accu_var: \"@result\"\n" + + " accu_init {\n" + + " id: 13\n" + + " const_expr {\n" + + " bool_value: true\n" + + " }\n" + + " }\n" + + " loop_condition {\n" + + " id: 15\n" + + " call_expr {\n" + + " function: \"@not_strictly_false\"\n" + + " args {\n" + + " id: 14\n" + + " ident_expr {\n" + + " name: \"@result\"\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " loop_step {\n" + + " id: 17\n" + + " call_expr {\n" + + " function: \"_&&_\"\n" + + " args {\n" + + " id: 16\n" + + " ident_expr {\n" + + " name: \"@result\"\n" + + " }\n" + + " }\n" + + " args {\n" + + " id: 11\n" + + " call_expr {\n" + + " function: \"_>=_\"\n" + + " args {\n" + + " id: 10\n" + + " ident_expr {\n" + + " name: \"x\"\n" + + " }\n" + + " }\n" + + " args {\n" + + " id: 12\n" + + " const_expr {\n" + + " int64_value: 0\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " result {\n" + + " id: 18\n" + + " ident_expr {\n" + + " name: \"@result\"\n" + + " }\n" + + " }\n" + + " }\n" + + "}\n" + + "source_info {\n" + + " location: \"\"\n" + + " line_offsets: 75\n" + + " positions {\n" + + " key: 1\n" + + " value: 0\n" + + " }\n" + + " positions {\n" + + " key: 2\n" + + " value: 13\n" + + " }\n" + + " positions {\n" + + " key: 3\n" + + " value: 26\n" + + " }\n" + + " positions {\n" + + " key: 4\n" + + " value: 28\n" + + " }\n" + + " positions {\n" + + " key: 5\n" + + " value: 30\n" + + " }\n" + + " positions {\n" + + " key: 6\n" + + " value: 45\n" + + " }\n" + + " positions {\n" + + " key: 7\n" + + " value: 56\n" + + " }\n" + + " positions {\n" + + " key: 9\n" + + " value: 64\n" + + " }\n" + + " positions {\n" + + " key: 10\n" + + " value: 67\n" + + " }\n" + + " positions {\n" + + " key: 11\n" + + " value: 69\n" + + " }\n" + + " positions {\n" + + " key: 12\n" + + " value: 72\n" + + " }\n" + + " positions {\n" + + " key: 13\n" + + " value: 63\n" + + " }\n" + + " positions {\n" + + " key: 14\n" + + " value: 63\n" + + " }\n" + + " positions {\n" + + " key: 15\n" + + " value: 63\n" + + " }\n" + + " positions {\n" + + " key: 16\n" + + " value: 63\n" + + " }\n" + + " positions {\n" + + " key: 17\n" + + " value: 63\n" + + " }\n" + + " positions {\n" + + " key: 18\n" + + " value: 63\n" + + " }\n" + + " positions {\n" + + " key: 19\n" + + " value: 63\n" + + " }\n" + + "}\n"; + + @Test + public void eval() throws Exception { + CelRuntime runtime = + CelRuntimeFactory.standardCelRuntimeBuilder() + .setOptions(CelOptions.DEFAULT) + .setStandardEnvironmentEnabled(false) + .addFunctionBindings( + CelFunctionBinding.fromOverloads( + "getThree", + CelFunctionBinding.from("getThree_overload", ImmutableList.of(), arg -> 3L))) + .setStandardFunctions( + CelStandardFunctions.newBuilder() + .includeFunctions( + StandardFunction.GREATER_EQUALS, + StandardFunction.LOGICAL_NOT, + StandardFunction.NOT_STRICTLY_FALSE) + .build()) + .addMessageTypes(TestAllTypes.getDescriptor()) + .build(); + CheckedExpr checkedExpr = TextFormat.parse(CHECKED_EXPR, CheckedExpr.class); + CelAbstractSyntaxTree ast = CelProtoAbstractSyntaxTree.fromCheckedExpr(checkedExpr).getAst(); + + boolean result = (boolean) runtime.createProgram(ast).eval(); + + assertThat(result).isTrue(); + } + + @Test + public void eval_error() throws Exception { + CelRuntime runtime = + CelRuntimeFactory.standardCelRuntimeBuilder() + .addMessageTypes(TestAllTypes.getDescriptor()) + .build(); + CheckedExpr checkedExpr = TextFormat.parse(CHECKED_EXPR, CheckedExpr.class); + CelAbstractSyntaxTree ast = CelProtoAbstractSyntaxTree.fromCheckedExpr(checkedExpr).getAst(); + + assertThat(assertThrows(CelEvaluationException.class, () -> runtime.createProgram(ast).eval())) + .hasMessageThat() + .contains("No matching overload for function 'getThree'"); + } +} diff --git a/extensions/BUILD.bazel b/extensions/BUILD.bazel index ef0e9c9fd..dea4cd760 100644 --- a/extensions/BUILD.bazel +++ b/extensions/BUILD.bazel @@ -1,3 +1,6 @@ +load("@rules_java//java:defs.bzl", "java_library") +load("//:cel_android_rules.bzl", "cel_android_library") + package( default_applicable_licenses = ["//:license"], default_visibility = ["//visibility:public"], @@ -8,6 +11,22 @@ java_library( exports = ["//extensions/src/main/java/dev/cel/extensions"], ) +java_library( + name = "extension_library", + exports = ["//extensions/src/main/java/dev/cel/extensions:extension_library"], +) + +java_library( + name = "lite_extensions", + visibility = ["//:internal"], + exports = ["//extensions/src/main/java/dev/cel/extensions:lite_extensions"], +) + +cel_android_library( + name = "lite_extensions_android", + exports = ["//extensions/src/main/java/dev/cel/extensions:lite_extensions_android"], +) + java_library( name = "strings", exports = ["//extensions/src/main/java/dev/cel/extensions:strings"], @@ -22,3 +41,23 @@ java_library( name = "optional_library", exports = ["//extensions/src/main/java/dev/cel/extensions:optional_library"], ) + +java_library( + name = "sets", + exports = ["//extensions/src/main/java/dev/cel/extensions:sets"], +) + +java_library( + name = "sets_function", + exports = ["//extensions/src/main/java/dev/cel/extensions:sets_function"], +) + +java_library( + name = "comprehensions", + exports = ["//extensions/src/main/java/dev/cel/extensions:comprehensions"], +) + +java_library( + name = "native", + exports = ["//extensions/src/main/java/dev/cel/extensions:native"], +) diff --git a/extensions/src/main/java/dev/cel/extensions/BUILD.bazel b/extensions/src/main/java/dev/cel/extensions/BUILD.bazel index bf42c2ae8..b25fdf16d 100644 --- a/extensions/src/main/java/dev/cel/extensions/BUILD.bazel +++ b/extensions/src/main/java/dev/cel/extensions/BUILD.bazel @@ -1,3 +1,6 @@ +load("@rules_java//java:defs.bzl", "java_library") +load("//:cel_android_rules.bzl", "cel_android_library") + package( default_applicable_licenses = [ "//:license", @@ -8,6 +11,18 @@ package( ], ) +java_library( + name = "extension_library", + srcs = ["CelExtensionLibrary.java"], + tags = [ + ], + deps = [ + "//common:compiler_common", + "//parser:macro", + "@maven//:com_google_guava_guava", + ], +) + java_library( name = "extensions", srcs = ["CelExtensions.java"], @@ -15,15 +30,54 @@ java_library( ], deps = [ ":bindings", + ":comprehensions", ":encoders", + ":lists", ":math", + ":native", + ":optional_library", ":protos", + ":regex", + ":sets", + ":sets_function", ":strings", "//common:options", + "//extensions:extension_library", + "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", ], ) +java_library( + name = "lite_extensions", + srcs = ["CelLiteExtensions.java"], + tags = [ + ], + deps = [ + ":sets_function", + ":sets_runtime_impl", + "//common:options", + "//runtime:runtime_equality", + "//runtime:runtime_helpers", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "lite_extensions_android", + srcs = ["CelLiteExtensions.java"], + tags = [ + ], + deps = [ + ":sets_function", + ":sets_runtime_impl_android", + "//common:options", + "//runtime:runtime_equality_android", + "//runtime:runtime_helpers_android", + "@maven_android//:com_google_guava_guava", + ], +) + java_library( name = "strings", srcs = ["CelStringExtensions.java"], @@ -35,7 +89,10 @@ java_library( "//common/internal", "//common/types", "//compiler:compiler_builder", + "//extensions:extension_library", "//runtime", + "//runtime:evaluation_exception_builder", + "//runtime:function_binding", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", ], @@ -49,6 +106,7 @@ java_library( "//common/ast", "//common/internal", "//compiler:compiler_builder", + "//extensions:extension_library", "//parser:macro", "//parser:parser_builder", "@maven//:com_google_errorprone_error_prone_annotations", @@ -62,16 +120,18 @@ java_library( tags = [ ], deps = [ + ":extension_library", "//checker:checker_builder", "//common:compiler_common", - "//common:options", "//common/ast", + "//common/exceptions:numeric_overflow", "//common/internal:comparison_functions", "//common/types", "//compiler:compiler_builder", "//parser:macro", "//parser:parser_builder", "//runtime", + "//runtime:function_binding", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", ], @@ -83,7 +143,9 @@ java_library( deps = [ "//common:compiler_common", "//common/ast", + "//common/types", "//compiler:compiler_builder", + "//extensions:extension_library", "//parser:macro", "//parser:parser_builder", "@maven//:com_google_errorprone_error_prone_annotations", @@ -97,10 +159,15 @@ java_library( deps = [ "//checker:checker_builder", "//common:compiler_common", + "//common:options", "//common/types", + "//common/values:cel_byte_string", "//compiler:compiler_builder", + "//extensions:extension_library", "//runtime", + "//runtime:function_binding", "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", ], ) @@ -109,20 +176,171 @@ java_library( name = "optional_library", srcs = ["CelOptionalLibrary.java"], tags = [ - "alt_dep=//extensions:optional_library", - "avoid_dep", ], deps = [ "//checker:checker_builder", "//common:compiler_common", + "//common:operator", + "//common:options", "//common/ast", "//common/types", + "//common/values", + "//common/values:cel_byte_string", + "//common/values:cel_value", "//compiler:compiler_builder", + "//extensions:extension_library", "//parser:macro", - "//parser:operator", "//parser:parser_builder", "//runtime", + "//runtime:function_binding", + "//runtime:runtime_equality", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", ], ) + +java_library( + name = "sets", + srcs = ["CelSetsExtensions.java"], + tags = [ + ], + deps = [ + ":sets_function", + ":sets_runtime_impl", + "//checker:checker_builder", + "//common:compiler_common", + "//common:options", + "//common/internal:default_message_factory", + "//common/internal:dynamic_proto", + "//common/types", + "//compiler:compiler_builder", + "//extensions:extension_library", + "//runtime", + "//runtime:proto_message_runtime_equality", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "sets_function", + srcs = ["SetsFunction.java"], + # used_by_android + tags = [ + ], +) + +java_library( + name = "sets_runtime_impl", + srcs = ["SetsExtensionsRuntimeImpl.java"], + visibility = ["//visibility:private"], + deps = [ + ":sets_function", + "//runtime:function_binding", + "//runtime:lite_runtime", + "//runtime:runtime_equality", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "sets_runtime_impl_android", + srcs = ["SetsExtensionsRuntimeImpl.java"], + visibility = ["//visibility:private"], + deps = [ + ":sets_function", + "//runtime:function_binding_android", + "//runtime:lite_runtime_android", + "//runtime:runtime_equality_android", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", + ], +) + +java_library( + name = "lists", + srcs = ["CelListsExtensions.java"], + tags = [ + ], + deps = [ + "//checker:checker_builder", + "//common:compiler_common", + "//common:operator", + "//common:options", + "//common/ast", + "//common/internal:comparison_functions", + "//common/types", + "//compiler:compiler_builder", + "//extensions:extension_library", + "//parser:macro", + "//parser:parser_builder", + "//runtime", + "//runtime:function_binding", + "//runtime:runtime_equality", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "regex", + srcs = ["CelRegexExtensions.java"], + deps = [ + "//checker:checker_builder", + "//common:compiler_common", + "//common/types", + "//compiler:compiler_builder", + "//extensions:extension_library", + "//runtime", + "//runtime:function_binding", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven//:com_google_re2j_re2j", + ], +) + +java_library( + name = "comprehensions", + srcs = ["CelComprehensionsExtensions.java"], + deps = [ + "//checker:checker_builder", + "//common:compiler_common", + "//common:operator", + "//common:options", + "//common/ast", + "//common/types", + "//common/values:mutable_map_value", + "//compiler:compiler_builder", + "//extensions:extension_library", + "//parser:macro", + "//parser:parser_builder", + "//runtime", + "//runtime:function_binding", + "//runtime:runtime_equality", + "@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 f05c79c6c..0e6537334 100644 --- a/extensions/src/main/java/dev/cel/extensions/CelBindingsExtensions.java +++ b/extensions/src/main/java/dev/cel/extensions/CelBindingsExtensions.java @@ -18,9 +18,15 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import com.google.errorprone.annotations.Immutable; +import dev.cel.common.CelFunctionDecl; import dev.cel.common.CelIssue; +import dev.cel.common.CelOverloadDecl; import dev.cel.common.ast.CelExpr; +import dev.cel.common.types.ListType; +import dev.cel.common.types.SimpleType; +import dev.cel.common.types.TypeParamType; import dev.cel.compiler.CelCompilerLibrary; import dev.cel.parser.CelMacro; import dev.cel.parser.CelMacroExprFactory; @@ -29,15 +35,56 @@ /** Internal implementation of the CEL local binding extensions. */ @Immutable -final class CelBindingsExtensions implements CelCompilerLibrary { - +public final class CelBindingsExtensions + implements CelCompilerLibrary, CelExtensionLibrary.FeatureSet { private static final String CEL_NAMESPACE = "cel"; private static final String UNUSED_ITER_VAR = "#unused"; + private static final CelExtensionLibrary LIBRARY = + new CelExtensionLibrary() { + private final CelBindingsExtensions version0 = new CelBindingsExtensions(); + + @Override + public String name() { + return "bindings"; + } + + @Override + public ImmutableSet versions() { + return ImmutableSet.of(version0); + } + }; + + static CelExtensionLibrary library() { + return LIBRARY; + } + + @Override + public int version() { + return 0; + } + + @Override + public ImmutableSet functions() { + // TODO: Add bindings for block once decorator support is available. + return ImmutableSet.of( + CelFunctionDecl.newFunctionDeclaration( + "cel.@block", + CelOverloadDecl.newGlobalOverload( + "cel_block_list", + TypeParamType.create("T"), + ListType.create(SimpleType.DYN), + TypeParamType.create("T")))); + } + + @Override + public ImmutableSet macros() { + return ImmutableSet.of(CelMacro.newReceiverMacro("bind", 3, CelBindingsExtensions::expandBind)); + } + @Override public void setParserOptions(CelParserBuilder parserBuilder) { - parserBuilder.addMacros( - CelMacro.newReceiverMacro("bind", 3, CelBindingsExtensions::expandBind)); + parserBuilder.addMacros(macros()); } /** diff --git a/extensions/src/main/java/dev/cel/extensions/CelComprehensionsExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelComprehensionsExtensions.java new file mode 100644 index 000000000..14968099c --- /dev/null +++ b/extensions/src/main/java/dev/cel/extensions/CelComprehensionsExtensions.java @@ -0,0 +1,513 @@ +// 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.extensions; + +import static com.google.common.base.Preconditions.checkArgument; +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 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.Operator; +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; +import dev.cel.parser.CelParserBuilder; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.CelInternalRuntimeLibrary; +import dev.cel.runtime.CelRuntimeBuilder; +import dev.cel.runtime.RuntimeEquality; +import java.util.Map; +import java.util.Optional; + +/** Internal implementation of CEL two variable comprehensions extensions. */ +public final class CelComprehensionsExtensions + implements CelCompilerLibrary, CelInternalRuntimeLibrary, CelExtensionLibrary.FeatureSet { + + 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 { + 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), + 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))); + + private final CelFunctionDecl functionDecl; + + String getFunction() { + return functionDecl.name(); + } + + Function(CelFunctionDecl functionDecl) { + this.functionDecl = functionDecl; + } + } + + private static final CelExtensionLibrary LIBRARY = + new CelExtensionLibrary() { + private final CelComprehensionsExtensions version0 = new CelComprehensionsExtensions(); + + @Override + public String name() { + return "comprehensions"; + } + + @Override + public ImmutableSet versions() { + return ImmutableSet.of(version0); + } + }; + + static CelExtensionLibrary library() { + return LIBRARY; + } + + private final ImmutableSet functions; + + CelComprehensionsExtensions() { + this.functions = ImmutableSet.copyOf(Function.values()); + } + + @Override + public void setCheckerOptions(CelCheckerBuilder checkerBuilder) { + functions.forEach(function -> checkerBuilder.addFunctionDeclarations(function.functionDecl)); + } + + @Override + public void setRuntimeOptions(CelRuntimeBuilder runtimeBuilder) { + throw new UnsupportedOperationException("Unsupported"); + } + + @Override + public void setRuntimeOptions( + CelRuntimeBuilder runtimeBuilder, RuntimeEquality runtimeEquality, CelOptions celOptions) { + 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 + public int version() { + return 0; + } + + @Override + public ImmutableSet macros() { + return ImmutableSet.of( + CelMacro.newReceiverMacro( + Operator.ALL.getFunction(), 3, CelComprehensionsExtensions::expandAllMacro), + CelMacro.newReceiverMacro( + Operator.EXISTS.getFunction(), 3, CelComprehensionsExtensions::expandExistsMacro), + CelMacro.newReceiverMacro( + Operator.EXISTS_ONE.getFunction(), + 3, + CelComprehensionsExtensions::expandExistsOneMacro), + CelMacro.newReceiverMacro( + Operator.EXISTS_ONE_NEW.getFunction(), + 3, + CelComprehensionsExtensions::expandExistsOneMacro), + CelMacro.newReceiverMacro( + "transformList", 3, CelComprehensionsExtensions::transformListMacro), + CelMacro.newReceiverMacro( + "transformList", 4, CelComprehensionsExtensions::transformListMacro), + CelMacro.newReceiverMacro( + "transformMap", 3, CelComprehensionsExtensions::transformMapMacro), + CelMacro.newReceiverMacro( + "transformMap", 4, CelComprehensionsExtensions::transformMapMacro), + CelMacro.newReceiverMacro( + "transformMapEntry", 3, CelComprehensionsExtensions::transformMapEntryMacro), + CelMacro.newReceiverMacro( + "transformMapEntry", 4, CelComprehensionsExtensions::transformMapEntryMacro)); + } + + @Override + public void setParserOptions(CelParserBuilder parserBuilder) { + parserBuilder.addMacros(macros()); + } + + private static Map mapInsertMap( + Map targetMap, Map mapToMerge, RuntimeEquality equality) { + for (Object key : mapToMerge.keySet()) { + if (equality.findInMap(targetMap, key).isPresent()) { + throw new IllegalArgumentException( + String.format("insert failed: key '%s' already exists", key)); + } + } + + if (targetMap instanceof MutableMapValue) { + MutableMapValue wrapper = (MutableMapValue) targetMap; + wrapper.putAll(mapToMerge); + return wrapper; + } + + return ImmutableMap.builderWithExpectedSize(targetMap.size() + mapToMerge.size()) + .putAll(targetMap) + .putAll(mapToMerge) + .buildOrThrow(); + } + + private static Map mapInsertKeyValue(Object[] args, RuntimeEquality equality) { + Map mapArg = (Map) args[0]; + Object key = args[1]; + Object value = args[2]; + + if (equality.findInMap(mapArg, key).isPresent()) { + throw new IllegalArgumentException( + String.format("insert failed: key '%s' already exists", key)); + } + + if (mapArg instanceof MutableMapValue) { + MutableMapValue mutableMap = (MutableMapValue) mapArg; + mutableMap.put(key, value); + return mutableMap; + } + + ImmutableMap.Builder builder = + ImmutableMap.builderWithExpectedSize(mapArg.size() + 1); + return builder.put(key, value).putAll(mapArg).buildOrThrow(); + } + + private static Optional expandAllMacro( + CelMacroExprFactory exprFactory, CelExpr target, ImmutableList arguments) { + checkNotNull(exprFactory); + checkNotNull(target); + checkArgument(arguments.size() == 3); + CelExpr arg0 = validatedIterationVariable(exprFactory, arguments.get(0)); + if (arg0.exprKind().getKind() != CelExpr.ExprKind.Kind.IDENT) { + return Optional.of(arg0); + } + CelExpr arg1 = validatedIterationVariable(exprFactory, arguments.get(1)); + if (arg1.exprKind().getKind() != CelExpr.ExprKind.Kind.IDENT) { + return Optional.of(arg1); + } + CelExpr arg2 = checkNotNull(arguments.get(2)); + CelExpr accuInit = exprFactory.newBoolLiteral(true); + CelExpr condition = + exprFactory.newGlobalCall( + Operator.NOT_STRICTLY_FALSE.getFunction(), + exprFactory.newIdentifier(exprFactory.getAccumulatorVarName())); + CelExpr step = + exprFactory.newGlobalCall( + Operator.LOGICAL_AND.getFunction(), + exprFactory.newIdentifier(exprFactory.getAccumulatorVarName()), + arg2); + CelExpr result = exprFactory.newIdentifier(exprFactory.getAccumulatorVarName()); + return Optional.of( + exprFactory.fold( + arg0.ident().name(), + arg1.ident().name(), + target, + exprFactory.getAccumulatorVarName(), + accuInit, + condition, + step, + result)); + } + + private static Optional expandExistsMacro( + CelMacroExprFactory exprFactory, CelExpr target, ImmutableList arguments) { + checkNotNull(exprFactory); + checkNotNull(target); + checkArgument(arguments.size() == 3); + CelExpr arg0 = validatedIterationVariable(exprFactory, arguments.get(0)); + if (arg0.exprKind().getKind() != CelExpr.ExprKind.Kind.IDENT) { + return Optional.of(arg0); + } + CelExpr arg1 = validatedIterationVariable(exprFactory, arguments.get(1)); + if (arg1.exprKind().getKind() != CelExpr.ExprKind.Kind.IDENT) { + return Optional.of(arg1); + } + CelExpr arg2 = checkNotNull(arguments.get(2)); + CelExpr accuInit = exprFactory.newBoolLiteral(false); + CelExpr condition = + exprFactory.newGlobalCall( + Operator.NOT_STRICTLY_FALSE.getFunction(), + exprFactory.newGlobalCall( + Operator.LOGICAL_NOT.getFunction(), + exprFactory.newIdentifier(exprFactory.getAccumulatorVarName()))); + CelExpr step = + exprFactory.newGlobalCall( + Operator.LOGICAL_OR.getFunction(), + exprFactory.newIdentifier(exprFactory.getAccumulatorVarName()), + arg2); + CelExpr result = exprFactory.newIdentifier(exprFactory.getAccumulatorVarName()); + return Optional.of( + exprFactory.fold( + arg0.ident().name(), + arg1.ident().name(), + target, + exprFactory.getAccumulatorVarName(), + accuInit, + condition, + step, + result)); + } + + private static Optional expandExistsOneMacro( + CelMacroExprFactory exprFactory, CelExpr target, ImmutableList arguments) { + checkNotNull(exprFactory); + checkNotNull(target); + checkArgument(arguments.size() == 3); + CelExpr arg0 = validatedIterationVariable(exprFactory, arguments.get(0)); + if (arg0.exprKind().getKind() != CelExpr.ExprKind.Kind.IDENT) { + return Optional.of(arg0); + } + CelExpr arg1 = validatedIterationVariable(exprFactory, arguments.get(1)); + if (arg1.exprKind().getKind() != CelExpr.ExprKind.Kind.IDENT) { + return Optional.of(arg1); + } + CelExpr arg2 = checkNotNull(arguments.get(2)); + CelExpr accuInit = exprFactory.newIntLiteral(0); + CelExpr condition = exprFactory.newBoolLiteral(true); + CelExpr step = + exprFactory.newGlobalCall( + Operator.CONDITIONAL.getFunction(), + arg2, + exprFactory.newGlobalCall( + Operator.ADD.getFunction(), + exprFactory.newIdentifier(exprFactory.getAccumulatorVarName()), + exprFactory.newIntLiteral(1)), + exprFactory.newIdentifier(exprFactory.getAccumulatorVarName())); + CelExpr result = + exprFactory.newGlobalCall( + Operator.EQUALS.getFunction(), + exprFactory.newIdentifier(exprFactory.getAccumulatorVarName()), + exprFactory.newIntLiteral(1)); + return Optional.of( + exprFactory.fold( + arg0.ident().name(), + arg1.ident().name(), + target, + exprFactory.getAccumulatorVarName(), + accuInit, + condition, + step, + result)); + } + + private static Optional transformListMacro( + CelMacroExprFactory exprFactory, CelExpr target, ImmutableList arguments) { + checkNotNull(exprFactory); + checkNotNull(target); + checkArgument(arguments.size() == 3 || arguments.size() == 4); + CelExpr arg0 = validatedIterationVariable(exprFactory, arguments.get(0)); + if (arg0.exprKind().getKind() != CelExpr.ExprKind.Kind.IDENT) { + return Optional.of(arg0); + } + CelExpr arg1 = validatedIterationVariable(exprFactory, arguments.get(1)); + if (arg1.exprKind().getKind() != CelExpr.ExprKind.Kind.IDENT) { + return Optional.of(arg1); + } + CelExpr transform; + CelExpr filter = null; + if (arguments.size() == 4) { + filter = checkNotNull(arguments.get(2)); + transform = checkNotNull(arguments.get(3)); + } else { + transform = checkNotNull(arguments.get(2)); + } + CelExpr accuInit = exprFactory.newList(); + CelExpr condition = exprFactory.newBoolLiteral(true); + CelExpr step = + exprFactory.newGlobalCall( + Operator.ADD.getFunction(), + exprFactory.newIdentifier(exprFactory.getAccumulatorVarName()), + exprFactory.newList(transform)); + if (filter != null) { + step = + exprFactory.newGlobalCall( + Operator.CONDITIONAL.getFunction(), + filter, + step, + exprFactory.newIdentifier(exprFactory.getAccumulatorVarName())); + } + return Optional.of( + exprFactory.fold( + arg0.ident().name(), + arg1.ident().name(), + target, + exprFactory.getAccumulatorVarName(), + accuInit, + condition, + step, + exprFactory.newIdentifier(exprFactory.getAccumulatorVarName()))); + } + + private static Optional transformMapMacro( + CelMacroExprFactory exprFactory, CelExpr target, ImmutableList arguments) { + checkNotNull(exprFactory); + checkNotNull(target); + checkArgument(arguments.size() == 3 || arguments.size() == 4); + CelExpr arg0 = validatedIterationVariable(exprFactory, arguments.get(0)); + if (arg0.exprKind().getKind() != CelExpr.ExprKind.Kind.IDENT) { + return Optional.of(arg0); + } + CelExpr arg1 = validatedIterationVariable(exprFactory, arguments.get(1)); + if (arg1.exprKind().getKind() != CelExpr.ExprKind.Kind.IDENT) { + return Optional.of(arg1); + } + CelExpr transform; + CelExpr filter = null; + if (arguments.size() == 4) { + filter = checkNotNull(arguments.get(2)); + transform = checkNotNull(arguments.get(3)); + } else { + transform = checkNotNull(arguments.get(2)); + } + CelExpr accuInit = exprFactory.newMap(); + CelExpr condition = exprFactory.newBoolLiteral(true); + CelExpr step = + exprFactory.newGlobalCall( + MAP_INSERT_FUNCTION, + exprFactory.newIdentifier(exprFactory.getAccumulatorVarName()), + arg0, + transform); + if (filter != null) { + step = + exprFactory.newGlobalCall( + Operator.CONDITIONAL.getFunction(), + filter, + step, + exprFactory.newIdentifier(exprFactory.getAccumulatorVarName())); + } + return Optional.of( + exprFactory.fold( + arg0.ident().name(), + arg1.ident().name(), + target, + exprFactory.getAccumulatorVarName(), + accuInit, + condition, + step, + exprFactory.newIdentifier(exprFactory.getAccumulatorVarName()))); + } + + private static Optional transformMapEntryMacro( + CelMacroExprFactory exprFactory, CelExpr target, ImmutableList arguments) { + checkNotNull(exprFactory); + checkNotNull(target); + checkArgument(arguments.size() == 3 || arguments.size() == 4); + CelExpr arg0 = validatedIterationVariable(exprFactory, arguments.get(0)); + if (arg0.exprKind().getKind() != CelExpr.ExprKind.Kind.IDENT) { + return Optional.of(arg0); + } + CelExpr arg1 = validatedIterationVariable(exprFactory, arguments.get(1)); + if (arg1.exprKind().getKind() != CelExpr.ExprKind.Kind.IDENT) { + return Optional.of(arg1); + } + CelExpr transform; + CelExpr filter = null; + if (arguments.size() == 4) { + filter = checkNotNull(arguments.get(2)); + transform = checkNotNull(arguments.get(3)); + } else { + transform = checkNotNull(arguments.get(2)); + } + CelExpr accuInit = exprFactory.newMap(); + CelExpr condition = exprFactory.newBoolLiteral(true); + CelExpr step = + exprFactory.newGlobalCall( + MAP_INSERT_FUNCTION, + exprFactory.newIdentifier(exprFactory.getAccumulatorVarName()), + transform); + if (filter != null) { + step = + exprFactory.newGlobalCall( + Operator.CONDITIONAL.getFunction(), + filter, + step, + exprFactory.newIdentifier(exprFactory.getAccumulatorVarName())); + } + return Optional.of( + exprFactory.fold( + arg0.ident().name(), + arg1.ident().name(), + target, + exprFactory.getAccumulatorVarName(), + accuInit, + condition, + step, + 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/extensions/src/main/java/dev/cel/extensions/CelEncoderExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelEncoderExtensions.java index 46ad08262..498b8555e 100644 --- a/extensions/src/main/java/dev/cel/extensions/CelEncoderExtensions.java +++ b/extensions/src/main/java/dev/cel/extensions/CelEncoderExtensions.java @@ -14,14 +14,19 @@ package dev.cel.extensions; +import static com.google.common.collect.ImmutableSet.toImmutableSet; + +import com.google.common.collect.ImmutableSet; import com.google.errorprone.annotations.Immutable; import com.google.protobuf.ByteString; import dev.cel.checker.CelCheckerBuilder; 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.values.CelByteString; import dev.cel.compiler.CelCompilerLibrary; -import dev.cel.runtime.CelRuntime; +import dev.cel.runtime.CelFunctionBinding; import dev.cel.runtime.CelRuntimeBuilder; import dev.cel.runtime.CelRuntimeLibrary; import java.util.Base64; @@ -30,36 +35,119 @@ /** Internal implementation of Encoder Extensions. */ @Immutable -public class CelEncoderExtensions implements CelCompilerLibrary, CelRuntimeLibrary { +public class CelEncoderExtensions + implements CelCompilerLibrary, CelRuntimeLibrary, CelExtensionLibrary.FeatureSet { + private static final Encoder BASE64_ENCODER = Base64.getEncoder(); private static final Decoder BASE64_DECODER = Base64.getDecoder(); - @Override - public void setCheckerOptions(CelCheckerBuilder checkerBuilder) { - checkerBuilder.addFunctionDeclarations( + private final ImmutableSet functions; + private final CelOptions celOptions; + + enum Function { + DECODE( CelFunctionDecl.newFunctionDeclaration( "base64.decode", CelOverloadDecl.newGlobalOverload( "base64_decode_string", SimpleType.BYTES, SimpleType.STRING)), + CelFunctionBinding.from( + "base64_decode_string", + String.class, + str -> CelByteString.of(BASE64_DECODER.decode(str))), + CelFunctionBinding.from( + "base64_decode_string", + String.class, + str -> ByteString.copyFrom(BASE64_DECODER.decode(str)))), + ENCODE( CelFunctionDecl.newFunctionDeclaration( "base64.encode", CelOverloadDecl.newGlobalOverload( - "base64_encode_bytes", SimpleType.STRING, SimpleType.BYTES))); + "base64_encode_bytes", SimpleType.STRING, SimpleType.BYTES)), + CelFunctionBinding.from( + "base64_encode_bytes", + CelByteString.class, + bytes -> BASE64_ENCODER.encodeToString(bytes.toByteArray())), + CelFunctionBinding.from( + "base64_encode_bytes", + ByteString.class, + bytes -> BASE64_ENCODER.encodeToString(bytes.toByteArray()))), + ; + + private final CelFunctionDecl functionDecl; + private final CelFunctionBinding nativeBytesFunctionBinding; + private final CelFunctionBinding protoBytesFunctionBinding; + + String getFunction() { + return functionDecl.name(); + } + + Function( + CelFunctionDecl functionDecl, + CelFunctionBinding nativeBytesFunctionBinding, + CelFunctionBinding protoBytesFunctionBinding) { + this.functionDecl = functionDecl; + this.nativeBytesFunctionBinding = nativeBytesFunctionBinding; + this.protoBytesFunctionBinding = protoBytesFunctionBinding; + } + } + + private static final class Library implements CelExtensionLibrary { + private final CelEncoderExtensions version0; + + @Override + public String name() { + return "encoders"; + } + + @Override + public ImmutableSet versions() { + return ImmutableSet.of(version0); + } + + private Library(CelOptions celOptions) { + this.version0 = new CelEncoderExtensions(celOptions); + } + } + + static CelExtensionLibrary library(CelOptions celOptions) { + return new Library(celOptions); + } + + @Override + public int version() { + return 0; + } + + @Override + public ImmutableSet functions() { + return functions.stream().map(f -> f.functionDecl).collect(toImmutableSet()); + } + + @Override + public void setCheckerOptions(CelCheckerBuilder checkerBuilder) { + functions.forEach(function -> checkerBuilder.addFunctionDeclarations(function.functionDecl)); } @SuppressWarnings("Immutable") // Instances of java.util.Base64 are immutable @Override public void setRuntimeOptions(CelRuntimeBuilder runtimeBuilder) { - runtimeBuilder.addFunctionBindings( - CelRuntime.CelFunctionBinding.from( - "base64_decode_string", - String.class, - str -> ByteString.copyFrom(BASE64_DECODER.decode(str))), - CelRuntime.CelFunctionBinding.from( - "base64_encode_bytes", - ByteString.class, - bytes -> BASE64_ENCODER.encodeToString(bytes.toByteArray()))); + functions.forEach( + function -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + runtimeBuilder.addFunctionBindings( + CelFunctionBinding.fromOverloads( + function.getFunction(), function.nativeBytesFunctionBinding)); + } else { + runtimeBuilder.addFunctionBindings( + CelFunctionBinding.fromOverloads( + function.getFunction(), function.protoBytesFunctionBinding)); + } + }); } -} + CelEncoderExtensions(CelOptions celOptions) { + this.celOptions = celOptions; + this.functions = ImmutableSet.copyOf(Function.values()); + } +} diff --git a/extensions/src/main/java/dev/cel/extensions/CelExtensionLibrary.java b/extensions/src/main/java/dev/cel/extensions/CelExtensionLibrary.java new file mode 100644 index 000000000..b7cb94297 --- /dev/null +++ b/extensions/src/main/java/dev/cel/extensions/CelExtensionLibrary.java @@ -0,0 +1,75 @@ +// 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.extensions; + +import com.google.common.collect.ImmutableSet; +import dev.cel.common.CelFunctionDecl; +import dev.cel.common.CelVarDecl; +import dev.cel.parser.CelMacro; +import java.util.Comparator; + +/** + * Interface for defining CEL extension libraries. + * + *

An extension library is a collection of CEL functions, variables, and macros that can be added + * to a CEL environment to provide additional functionality. + */ +public interface CelExtensionLibrary { + + /** Returns the name of the extension library. */ + String name(); + + ImmutableSet versions(); + + default T latest() { + return versions().stream().max(Comparator.comparing(FeatureSet::version)).get(); + } + + default T version(int version) { + if (version == Integer.MAX_VALUE) { + return latest(); + } + + for (T v : versions()) { + if (v.version() == version) { + return v; + } + } + throw new IllegalArgumentException("Unsupported '" + name() + "' extension version " + version); + } + + /** + * Interface for defining a version of a CEL extension library. + */ + interface FeatureSet { + /** Returns the extension library version or -1 if unspecified. */ + int version(); + + /** Returns the set of function declarations defined by this extension library. */ + default ImmutableSet functions() { + return ImmutableSet.of(); + } + + /** Returns the set of macros defined by this extension library. */ + default ImmutableSet macros() { + return ImmutableSet.of(); + } + + /** Returns the set of variables defined by this extension library. */ + default ImmutableSet variables() { + return ImmutableSet.of(); + } + } +} diff --git a/extensions/src/main/java/dev/cel/extensions/CelExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelExtensions.java index c3d9fdbbf..446fa26e7 100644 --- a/extensions/src/main/java/dev/cel/extensions/CelExtensions.java +++ b/extensions/src/main/java/dev/cel/extensions/CelExtensions.java @@ -1,4 +1,4 @@ -// Copyright 2022 Google LLC +// 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. @@ -14,9 +14,16 @@ package dev.cel.extensions; +import static com.google.common.collect.ImmutableSet.toImmutableSet; + 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. @@ -29,7 +36,27 @@ public final class CelExtensions { private static final CelStringExtensions STRING_EXTENSIONS_ALL = new CelStringExtensions(); private static final CelProtoExtensions PROTO_EXTENSIONS = new CelProtoExtensions(); private static final CelBindingsExtensions BINDINGS_EXTENSIONS = new CelBindingsExtensions(); - private static final CelEncoderExtensions ENCODER_EXTENSIONS = new CelEncoderExtensions(); + private static final CelRegexExtensions REGEX_EXTENSIONS = new CelRegexExtensions(); + private static final CelComprehensionsExtensions COMPREHENSIONS_EXTENSIONS = + new CelComprehensionsExtensions(); + + /** + * Implementation of optional values. + * + *

Refer to README.md for available functions. + */ + public static CelOptionalLibrary optional() { + return CelOptionalLibrary.library().latest(); + } + + /** + * Implementation of optional values. + * + *

Refer to README.md for available functions for each supported version. + */ + public static CelOptionalLibrary optional(int version) { + return CelOptionalLibrary.library().version(version); + } /** * Extended functions for string manipulation. @@ -96,13 +123,19 @@ 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...)} instead. + * CelMathExtensions.Function...)} or {@link #math(CelOptions,int)} instead. + */ + public static CelMathExtensions math() { + return CelMathExtensions.library().latest(); + } + + /** + * Returns the specified version of the 'math' extension. * - * @param celOptions CelOptions to configure CelMathExtension with. This should be the same - * options object used to configure the compilation/runtime environments. + *

Refer to README.md for functions available in each version. */ - public static CelMathExtensions math(CelOptions celOptions) { - return new CelMathExtensions(celOptions); + public static CelMathExtensions math(int version) { + return CelMathExtensions.library().version(version); } /** @@ -117,13 +150,9 @@ public static CelMathExtensions math(CelOptions celOptions) { * 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)); } /** @@ -138,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); } /** @@ -160,14 +225,203 @@ public static CelBindingsExtensions bindings() { return BINDINGS_EXTENSIONS; } + /** + * @deprecated Use {@link #encoders(CelOptions) instead.} + */ + @Deprecated + public static CelEncoderExtensions encoders() { + return new CelEncoderExtensions(CelOptions.DEFAULT); + } + /** * Extended functions for string, byte and object encodings. * *

This adds {@code base64.encode} and {@code base64.decode} functions. See README.md for their * documentation. + * + * @param celOptions This should be the same {@link CelOptions} object used to configure + * compilation/runtime environments. */ - public static CelEncoderExtensions encoders() { - return ENCODER_EXTENSIONS; + public static CelEncoderExtensions encoders(CelOptions celOptions) { + return new CelEncoderExtensions(celOptions); + } + + /** + * Extended functions for Set manipulation. + * + *

Refer to README.md for available functions. + * + *

This will include all functions denoted in {@link SetsFunction}, including any future + * additions. To expose only a subset of functions, use {@link #sets(CelOptions, SetsFunction...)} + * instead. + */ + public static CelSetsExtensions sets(CelOptions celOptions) { + return new CelSetsExtensions(celOptions); + } + + /** + * Extended functions for Set manipulation. + * + *

Refer to README.md for available functions. + * + *

This will include only the specific functions denoted by {@link SetsFunction}. + */ + public static CelSetsExtensions sets(CelOptions celOptions, SetsFunction... functions) { + return sets(celOptions, ImmutableSet.copyOf(functions)); + } + + /** + * Extended functions for Set manipulation. + * + *

Refer to README.md for available functions. + * + *

This will include only the specific functions denoted by {@link SetsFunction}. + */ + public static CelSetsExtensions sets(CelOptions celOptions, Set functions) { + return new CelSetsExtensions(celOptions, functions); + } + + /** + * Extended functions for List manipulation. + * + *

Refer to README.md for available functions. + * + *

This will include only the specific functions denoted by {@link + * CelListsExtensions.Function}. + */ + public static CelListsExtensions lists() { + return CelListsExtensions.library().latest(); + } + + /** + * Extended functions for List manipulation. + * + *

Refer to README.md for functions available in each version. + */ + public static CelListsExtensions lists(int version) { + return CelListsExtensions.library().version(version); + } + + /** + * Extended functions for List manipulation. + * + *

Refer to README.md for available functions. + * + *

This will include only the specific functions denoted by {@link + * CelListsExtensions.Function}. + */ + public static CelListsExtensions lists(CelListsExtensions.Function... functions) { + return lists(ImmutableSet.copyOf(functions)); + } + + /** + * Extended functions for List manipulation. + * + *

Refer to README.md for available functions. + * + *

This will include all functions denoted in {@link CelListsExtensions.Function}, including + * any future additions. To expose only a subset of functions, use {@link + * #lists(CelListsExtensions.Function...)} instead. + */ + public static CelListsExtensions lists(Set functions) { + return new CelListsExtensions(functions); + } + + /** + * Extended functions for Regular Expressions. + * + *

Refer to README.md for available functions. + * + *

This will include all functions denoted in {@link CelRegexExtensions.Function}, including + * any future additions. + */ + public static CelRegexExtensions regex() { + return REGEX_EXTENSIONS; + } + + /** + * Extended functions for Two Variable Comprehensions Expressions. + * + *

Refer to README.md for available functions. + * + *

This will include all functions denoted in {@link CelComprehensionsExtensions.Function}, + * including any future additions. + */ + 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. + * + *

Note: Certain extensions such as {@link CelProtoExtensions} and {@link + * CelBindingsExtensions} are implemented via macros, not functions, and those are not included + * here. + */ + public static ImmutableSet getAllFunctionNames() { + return Streams.concat( + EnumSet.allOf(Function.class).stream().map(CelMathExtensions.Function::getFunction), + EnumSet.allOf(CelStringExtensions.Function.class).stream() + .map(CelStringExtensions.Function::getFunction), + EnumSet.allOf(SetsFunction.class).stream().map(SetsFunction::getFunction), + EnumSet.allOf(CelEncoderExtensions.Function.class).stream() + .map(CelEncoderExtensions.Function::getFunction), + EnumSet.allOf(CelListsExtensions.Function.class).stream() + .map(CelListsExtensions.Function::getFunction), + EnumSet.allOf(CelRegexExtensions.Function.class).stream() + .map(CelRegexExtensions.Function::getFunction), + Stream.of( + CelOptionalLibrary.Function.VALUE, + CelOptionalLibrary.Function.HAS_VALUE, + CelOptionalLibrary.Function.OPTIONAL_NONE, + CelOptionalLibrary.Function.OPTIONAL_OF, + CelOptionalLibrary.Function.OPTIONAL_UNWRAP, + CelOptionalLibrary.Function.OPTIONAL_OF_NON_ZERO_VALUE) + .map(CelOptionalLibrary.Function::getFunction), + EnumSet.allOf(CelComprehensionsExtensions.Function.class).stream() + .map(CelComprehensionsExtensions.Function::getFunction)) + .collect(toImmutableSet()); + } + + public static CelExtensionLibrary getExtensionLibrary( + String name, CelOptions options) { + switch (name) { + case "bindings": + return CelBindingsExtensions.library(); + case "encoders": + return CelEncoderExtensions.library(options); + case "lists": + return CelListsExtensions.library(); + case "math": + return CelMathExtensions.library(); + case "optional": + return CelOptionalLibrary.library(); + case "protos": + return CelProtoExtensions.library(); + case "regex": + return CelRegexExtensions.library(); + case "sets": + return CelSetsExtensions.library(options); + case "strings": + return CelStringExtensions.library(); + case "comprehensions": + return CelComprehensionsExtensions.library(); + // TODO: add support for remaining standard extensions + default: + throw new IllegalArgumentException("Unknown standard extension '" + name + "'"); + } } private CelExtensions() {} diff --git a/extensions/src/main/java/dev/cel/extensions/CelListsExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelListsExtensions.java new file mode 100644 index 000000000..79539b008 --- /dev/null +++ b/extensions/src/main/java/dev/cel/extensions/CelListsExtensions.java @@ -0,0 +1,455 @@ +// 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. + +package dev.cel.extensions; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.ImmutableSet.toImmutableSet; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +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.Operator; +import dev.cel.common.ast.CelExpr; +import dev.cel.common.internal.ComparisonFunctions; +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; +import dev.cel.parser.CelParserBuilder; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.CelInternalRuntimeLibrary; +import dev.cel.runtime.CelRuntimeBuilder; +import dev.cel.runtime.RuntimeEquality; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +/** Internal implementation of CEL lists extensions. */ +public final class CelListsExtensions + implements CelCompilerLibrary, CelInternalRuntimeLibrary, CelExtensionLibrary.FeatureSet { + + /** Supported functions for Lists extension library. */ + @SuppressWarnings({"unchecked"}) // Unchecked: Type-checker guarantees casting safety. + public enum Function { + // Note! Creating dependencies on the outer class may cause circular initialization issues. + SLICE( + CelFunctionDecl.newFunctionDeclaration( + "slice", + CelOverloadDecl.newMemberOverload( + "list_slice", + "Returns a new sub-list using the indices provided", + ListType.create(TypeParamType.create("T")), + ListType.create(TypeParamType.create("T")), + SimpleType.INT, + SimpleType.INT)), + CelFunctionBinding.from( + "list_slice", + ImmutableList.of(Collection.class, Long.class, Long.class), + (args) -> { + Collection target = (Collection) args[0]; + long from = (Long) args[1]; + long to = (Long) args[2]; + return CelListsExtensions.slice(target, from, to); + })), + FLATTEN( + CelFunctionDecl.newFunctionDeclaration( + "flatten", + CelOverloadDecl.newMemberOverload( + "list_flatten", + "Flattens a list by a single level", + ListType.create(TypeParamType.create("T")), + ListType.create(ListType.create(TypeParamType.create("T")))), + CelOverloadDecl.newMemberOverload( + "list_flatten_list_int", + "Flattens a list to the specified level. A negative depth value flattens the list" + + " recursively to its deepest level.", + ListType.create(SimpleType.DYN), + ListType.create(SimpleType.DYN), + SimpleType.INT)), + CelFunctionBinding.from("list_flatten", Collection.class, list -> flatten(list, 1)), + CelFunctionBinding.from( + "list_flatten_list_int", Collection.class, Long.class, CelListsExtensions::flatten)), + RANGE( + CelFunctionDecl.newFunctionDeclaration( + "lists.range", + CelOverloadDecl.newGlobalOverload( + "lists_range", + "Returns a list of integers from 0 to n-1.", + ListType.create(SimpleType.INT), + SimpleType.INT)), + CelFunctionBinding.from("lists_range", Long.class, CelListsExtensions::genRange)), + DISTINCT( + CelFunctionDecl.newFunctionDeclaration( + "distinct", + CelOverloadDecl.newMemberOverload( + "list_distinct", + "Returns the distinct elements of a list", + ListType.create(TypeParamType.create("T")), + ListType.create(TypeParamType.create("T"))))), + REVERSE( + CelFunctionDecl.newFunctionDeclaration( + "reverse", + CelOverloadDecl.newMemberOverload( + "list_reverse", + "Returns the elements of a list in reverse order", + ListType.create(TypeParamType.create("T")), + ListType.create(TypeParamType.create("T")))), + CelFunctionBinding.from("list_reverse", Collection.class, CelListsExtensions::reverse)), + SORT( + CelFunctionDecl.newFunctionDeclaration( + "sort", + CelOverloadDecl.newMemberOverload( + "list_sort", + "Sorts a list with comparable elements.", + 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", + ListType.create(TypeParamType.create("T")), + ListType.create(TypeParamType.create("T")))), + CelFunctionBinding.from( + "list_sortByAssociatedKeys", + Collection.class, + CelListsExtensions::sortByAssociatedKeys)); + + private final CelFunctionDecl functionDecl; + private final ImmutableSet functionBindings; + + String getFunction() { + return functionDecl.name(); + } + + Function(CelFunctionDecl functionDecl, CelFunctionBinding... functionBindings) { + this.functionDecl = functionDecl; + this.functionBindings = + functionBindings.length > 0 + ? CelFunctionBinding.fromOverloads(functionDecl.name(), functionBindings) + : ImmutableSet.of(); + } + } + + private static final CelExtensionLibrary LIBRARY = + new CelExtensionLibrary() { + private final CelListsExtensions version0 = + new CelListsExtensions(0, ImmutableSet.of(Function.SLICE)); + private final CelListsExtensions version1 = + new CelListsExtensions( + 1, + ImmutableSet.builder() + .addAll(version0.functions) + .add(Function.FLATTEN) + .build()); + private final CelListsExtensions version2 = + new CelListsExtensions( + 2, + ImmutableSet.builder() + .addAll(version1.functions) + .add( + Function.RANGE, + Function.DISTINCT, + Function.REVERSE, + Function.SORT, + Function.SORT_BY) + .build()); + + @Override + public String name() { + return "lists"; + } + + @Override + public ImmutableSet versions() { + return ImmutableSet.of(version0, version1, version2); + } + }; + + static CelExtensionLibrary library() { + return LIBRARY; + } + + private final int version; + private final ImmutableSet functions; + + CelListsExtensions(Set functions) { + this(-1, functions); + } + + private CelListsExtensions(int version, Set functions) { + this.version = version; + this.functions = ImmutableSet.copyOf(functions); + } + + @Override + public int version() { + return version; + } + + @Override + public ImmutableSet functions() { + return functions.stream().map(f -> f.functionDecl).collect(toImmutableSet()); + } + + @Override + public ImmutableSet macros() { + if (version >= 2) { + return ImmutableSet.of( + CelMacro.newReceiverMacro("sortBy", 2, CelListsExtensions::sortByMacro)); + } + return ImmutableSet.of(); + } + + @Override + public void setParserOptions(CelParserBuilder parserBuilder) { + parserBuilder.addMacros(macros()); + } + + @Override + public void setCheckerOptions(CelCheckerBuilder checkerBuilder) { + functions.forEach(function -> checkerBuilder.addFunctionDeclarations(function.functionDecl)); + } + + @Override + public void setRuntimeOptions(CelRuntimeBuilder runtimeBuilder) { + throw new UnsupportedOperationException("Unsupported"); + } + + @SuppressWarnings("unchecked") + @Override + public void setRuntimeOptions( + CelRuntimeBuilder runtimeBuilder, RuntimeEquality runtimeEquality, CelOptions celOptions) { + 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) { + Preconditions.checkArgument(from >= 0 && to >= 0, "Negative indexes not supported"); + Preconditions.checkArgument(to >= from, "Start index must be less than or equal to end index"); + Preconditions.checkArgument(to <= list.size(), "List is length %s", list.size()); + if (list instanceof List) { + List subList = ((List) list).subList((int) from, (int) to); + if (subList instanceof ImmutableList) { + return (ImmutableList) subList; + } + return ImmutableList.copyOf(subList); + } else { + ImmutableList.Builder builder = ImmutableList.builder(); + long index = 0; + for (Iterator iterator = list.iterator(); iterator.hasNext(); index++) { + Object element = iterator.next(); + if (index >= to) { + break; + } + if (index >= from) { + builder.add(element); + } + } + return builder.build(); + } + } + + @SuppressWarnings("unchecked") + private static ImmutableList flatten(Collection list, long depth) { + Preconditions.checkArgument(depth >= 0, "Level must be non-negative"); + ImmutableList.Builder builder = ImmutableList.builder(); + for (Object element : list) { + if (!(element instanceof Collection) || depth == 0) { + builder.add(element); + } else { + Collection listItem = (Collection) element; + builder.addAll(flatten(listItem, depth - 1)); + } + } + + return builder.build(); + } + + public static ImmutableList genRange(long end) { + ImmutableList.Builder builder = ImmutableList.builder(); + for (long i = 0; i < end; i++) { + builder.add(i); + } + return builder.build(); + } + + private static class RuntimeEqualityObjectWrapper { + private final Object object; + private final int hashCode; + private final RuntimeEquality runtimeEquality; + + RuntimeEqualityObjectWrapper(Object object, RuntimeEquality runtimeEquality) { + this.object = object; + this.runtimeEquality = runtimeEquality; + this.hashCode = runtimeEquality.hashCode(object); + } + + @Override + public int hashCode() { + return hashCode; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof RuntimeEqualityObjectWrapper)) { + return false; + } + return runtimeEquality.objectEquals(object, ((RuntimeEqualityObjectWrapper) obj).object); + } + } + + private static ImmutableList distinct( + Collection list, RuntimeEquality runtimeEquality) { + int size = list.size(); + ImmutableList.Builder builder = ImmutableList.builderWithExpectedSize(size); + Set distinctValues = Sets.newHashSetWithExpectedSize(size); + for (Object element : list) { + if (distinctValues.add(new RuntimeEqualityObjectWrapper(element, runtimeEquality))) { + builder.add(element); + } + } + return builder.build(); + } + + private static List reverse(Collection list) { + if (list instanceof List) { + return Lists.reverse((List) list); + } else { + ImmutableList.Builder builder = ImmutableList.builderWithExpectedSize(list.size()); + Object[] objects = list.toArray(); + for (int i = objects.length - 1; i >= 0; i--) { + builder.add(objects[i]); + } + return builder.build(); + } + } + + private static ImmutableList sort(Collection objects) { + return ImmutableList.sortedCopyOf(new CelObjectComparator(), objects); + } + + private static class CelObjectComparator implements Comparator { + + CelObjectComparator() {} + + @SuppressWarnings({"unchecked"}) + @Override + public int compare(Object o1, Object o2) { + if (o1 instanceof Number && o2 instanceof Number) { + return ComparisonFunctions.numericCompare((Number) o1, (Number) o2); + } + + if (!(o1 instanceof Comparable)) { + throw new IllegalArgumentException("List elements must be comparable"); + } + if (o1.getClass() != o2.getClass()) { + throw new IllegalArgumentException("List elements must have the same type"); + } + return ((Comparable) o1).compareTo(o2); + } + } + + private static Optional sortByMacro( + CelMacroExprFactory exprFactory, CelExpr target, ImmutableList arguments) { + checkNotNull(exprFactory); + checkNotNull(target); + checkArgument(arguments.size() == 2); + CelExpr varIdent = checkNotNull(arguments.get(0)); + if (varIdent.exprKind().getKind() != CelExpr.ExprKind.Kind.IDENT) { + return Optional.of( + exprFactory.reportError( + CelIssue.formatError( + exprFactory.getSourceLocation(varIdent), + "sortBy(var, ...) variable name must be a simple identifier"))); + } + + 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 = + exprFactory.newGlobalCall( + Operator.ADD.getFunction(), + exprFactory.newIdentifier(exprFactory.getAccumulatorVarName()), + step); + // Create an intermediate list and populate it with key-value pairs + step = + exprFactory.fold( + varName, + target, + exprFactory.getAccumulatorVarName(), + exprFactory.newList(), + exprFactory.newBoolLiteral(true), // Include all elements + step, + 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); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private static ImmutableList sortByAssociatedKeys( + Collection> keyValuePairs) { + List[] array = keyValuePairs.toArray(new List[0]); + Arrays.sort(array, new CelObjectByKeyComparator(new CelObjectComparator())); + ImmutableList.Builder builder = ImmutableList.builderWithExpectedSize(array.length); + for (List pair : array) { + builder.add(pair.get(1)); + } + return builder.build(); + } + + private static class CelObjectByKeyComparator implements Comparator { + private final CelObjectComparator keyComparator; + + CelObjectByKeyComparator(CelObjectComparator keyComparator) { + this.keyComparator = keyComparator; + } + + @SuppressWarnings({"unchecked"}) + @Override + public int compare(Object o1, Object o2) { + return keyComparator.compare(((List) o1).get(0), ((List) o2).get(0)); + } + } +} diff --git a/extensions/src/main/java/dev/cel/extensions/CelLiteExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelLiteExtensions.java new file mode 100644 index 000000000..b46b40756 --- /dev/null +++ b/extensions/src/main/java/dev/cel/extensions/CelLiteExtensions.java @@ -0,0 +1,67 @@ +// 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.extensions; + +import com.google.common.collect.ImmutableSet; +import dev.cel.common.CelOptions; +import dev.cel.runtime.RuntimeEquality; +import dev.cel.runtime.RuntimeHelpers; +import java.util.Set; + +/** + * Collections of supported CEL Extensions for the lite runtime. + * + *

To use, supply the desired extensions using : {@code CelLiteRuntimeBuilder#addLibraries}. + */ +public final class CelLiteExtensions { + + /** + * Extended functions for Set manipulation. + * + *

Refer to README.md for available functions. + * + *

This will include all functions denoted in {@link SetsFunction}, including any future + * additions. To expose only a subset of functions, use {@link #sets(CelOptions, SetsFunction...)} + * instead. + */ + public static SetsExtensionsRuntimeImpl sets(CelOptions celOptions) { + return sets(celOptions, SetsFunction.values()); + } + + /** + * Extended functions for Set manipulation. + * + *

Refer to README.md for available functions. + * + *

This will include only the specific functions denoted by {@link SetsFunction}. + */ + public static SetsExtensionsRuntimeImpl sets(CelOptions celOptions, SetsFunction... functions) { + return sets(celOptions, ImmutableSet.copyOf(functions)); + } + + /** + * Extended functions for Set manipulation. + * + *

Refer to README.md for available functions. + * + *

This will include only the specific functions denoted by {@link SetsFunction}. + */ + public static SetsExtensionsRuntimeImpl sets(CelOptions celOptions, Set functions) { + RuntimeEquality runtimeEquality = RuntimeEquality.create(RuntimeHelpers.create(), celOptions); + return new SetsExtensionsRuntimeImpl(runtimeEquality, functions); + } + + private CelLiteExtensions() {} +} diff --git a/extensions/src/main/java/dev/cel/extensions/CelMathExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelMathExtensions.java index 7326cabf9..63108aa0c 100644 --- a/extensions/src/main/java/dev/cel/extensions/CelMathExtensions.java +++ b/extensions/src/main/java/dev/cel/extensions/CelMathExtensions.java @@ -16,20 +16,22 @@ import static com.google.common.collect.Comparators.max; import static com.google.common.collect.Comparators.min; +import static com.google.common.collect.ImmutableSet.toImmutableSet; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableTable; +import com.google.common.math.DoubleMath; import com.google.common.primitives.UnsignedLong; import com.google.errorprone.annotations.Immutable; 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; import dev.cel.common.ast.CelExpr.ExprKind.Kind; +import dev.cel.common.exceptions.CelNumericOverflowException; import dev.cel.common.internal.ComparisonFunctions; import dev.cel.common.types.ListType; import dev.cel.common.types.SimpleType; @@ -37,9 +39,10 @@ import dev.cel.parser.CelMacro; import dev.cel.parser.CelMacroExprFactory; import dev.cel.parser.CelParserBuilder; -import dev.cel.runtime.CelRuntime; +import dev.cel.runtime.CelFunctionBinding; import dev.cel.runtime.CelRuntimeBuilder; import dev.cel.runtime.CelRuntimeLibrary; +import java.math.RoundingMode; import java.util.List; import java.util.Optional; import java.util.Set; @@ -53,7 +56,8 @@ */ @SuppressWarnings({"rawtypes", "unchecked"}) // Use of raw Comparables. @Immutable -final class CelMathExtensions implements CelCompilerLibrary, CelRuntimeLibrary { +public final class CelMathExtensions + implements CelCompilerLibrary, CelRuntimeLibrary, CelExtensionLibrary.FeatureSet { private static final String MATH_NAMESPACE = "math"; @@ -64,6 +68,33 @@ final class CelMathExtensions implements CelCompilerLibrary, CelRuntimeLibrary { private static final String MATH_MIN_OVERLOAD_DOC = "Returns the least valued number present in the arguments."; + // Rounding Functions + private static final String MATH_CEIL_FUNCTION = "math.ceil"; + private static final String MATH_FLOOR_FUNCTION = "math.floor"; + private static final String MATH_ROUND_FUNCTION = "math.round"; + private static final String MATH_TRUNC_FUNCTION = "math.trunc"; + + // Floating Point Functions + private static final String MATH_ISFINITE_FUNCTION = "math.isFinite"; + private static final String MATH_ISNAN_FUNCTION = "math.isNaN"; + private static final String MATH_ISINF_FUNCTION = "math.isInf"; + + // Signedness Functions + private static final String MATH_ABS_FUNCTION = "math.abs"; + private static final String MATH_SIGN_FUNCTION = "math.sign"; + + // Bitwise Functions + private static final String MATH_BIT_AND_FUNCTION = "math.bitAnd"; + private static final String MATH_BIT_OR_FUNCTION = "math.bitOr"; + private static final String MATH_BIT_XOR_FUNCTION = "math.bitXor"; + private static final String MATH_BIT_NOT_FUNCTION = "math.bitNot"; + private static final String MATH_BIT_LEFT_SHIFT_FUNCTION = "math.bitShiftLeft"; + private static final String MATH_BIT_RIGHT_SHIFT_FUNCTION = "math.bitShiftRight"; + + private static final String MATH_SQRT_FUNCTION = "math.sqrt"; + + private static final int MAX_BIT_SHIFT = 63; + /** * Returns the proper comparison function to use for a math function call involving different * argument types. @@ -104,6 +135,7 @@ final class CelMathExtensions implements CelCompilerLibrary, CelRuntimeLibrary { return builder.buildOrThrow(); } + /** Enumeration of functions for Math extension. */ public enum Function { MAX( CelFunctionDecl.newFunctionDeclaration( @@ -174,52 +206,59 @@ public enum Function { MATH_MAX_OVERLOAD_DOC, SimpleType.DYN, ListType.create(SimpleType.DYN))), - ImmutableSet.of( - CelRuntime.CelFunctionBinding.from("math_@max_double", Double.class, x -> x), - CelRuntime.CelFunctionBinding.from("math_@max_int", Long.class, x -> x), - CelRuntime.CelFunctionBinding.from( - "math_@max_double_double", Double.class, Double.class, CelMathExtensions::maxPair), - CelRuntime.CelFunctionBinding.from( - "math_@max_int_int", Long.class, Long.class, CelMathExtensions::maxPair), - CelRuntime.CelFunctionBinding.from( - "math_@max_int_double", Long.class, Double.class, CelMathExtensions::maxPair), - CelRuntime.CelFunctionBinding.from( - "math_@max_double_int", Double.class, Long.class, CelMathExtensions::maxPair), - CelRuntime.CelFunctionBinding.from( - "math_@max_list_dyn", List.class, CelMathExtensions::maxList)), - ImmutableSet.of( - CelRuntime.CelFunctionBinding.from("math_@max_uint", Long.class, x -> x), - CelRuntime.CelFunctionBinding.from( - "math_@max_uint_uint", Long.class, Long.class, CelMathExtensions::maxPair), - CelRuntime.CelFunctionBinding.from( - "math_@max_double_uint", Double.class, Long.class, CelMathExtensions::maxPair), - CelRuntime.CelFunctionBinding.from( - "math_@max_uint_int", Long.class, Long.class, CelMathExtensions::maxPair), - CelRuntime.CelFunctionBinding.from( - "math_@max_uint_double", Long.class, Double.class, CelMathExtensions::maxPair), - CelRuntime.CelFunctionBinding.from( - "math_@max_int_uint", Long.class, Long.class, CelMathExtensions::maxPair)), - ImmutableSet.of( - CelRuntime.CelFunctionBinding.from("math_@max_uint", UnsignedLong.class, x -> x), - CelRuntime.CelFunctionBinding.from( - "math_@max_uint_uint", - UnsignedLong.class, - UnsignedLong.class, - CelMathExtensions::maxPair), - CelRuntime.CelFunctionBinding.from( - "math_@max_double_uint", - Double.class, - UnsignedLong.class, - CelMathExtensions::maxPair), - CelRuntime.CelFunctionBinding.from( - "math_@max_uint_int", UnsignedLong.class, Long.class, CelMathExtensions::maxPair), - CelRuntime.CelFunctionBinding.from( - "math_@max_uint_double", - UnsignedLong.class, - Double.class, - CelMathExtensions::maxPair), - CelRuntime.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, @@ -289,89 +328,442 @@ public enum Function { MATH_MIN_OVERLOAD_DOC, SimpleType.DYN, ListType.create(SimpleType.DYN))), + 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, + CelOverloadDecl.newGlobalOverload( + "math_ceil_double", + "Compute the ceiling of a double value.", + SimpleType.DOUBLE, + SimpleType.DOUBLE)), + ImmutableSet.of(CelFunctionBinding.from("math_ceil_double", Double.class, Math::ceil))), + FLOOR( + CelFunctionDecl.newFunctionDeclaration( + MATH_FLOOR_FUNCTION, + CelOverloadDecl.newGlobalOverload( + "math_floor_double", + "Compute the floor of a double value.", + SimpleType.DOUBLE, + SimpleType.DOUBLE)), + ImmutableSet.of(CelFunctionBinding.from("math_floor_double", Double.class, Math::floor))), + ROUND( + CelFunctionDecl.newFunctionDeclaration( + MATH_ROUND_FUNCTION, + CelOverloadDecl.newGlobalOverload( + "math_round_double", + "Rounds the double value to the nearest whole number with ties rounding away from" + + " zero.", + SimpleType.DOUBLE, + SimpleType.DOUBLE)), ImmutableSet.of( - CelRuntime.CelFunctionBinding.from("math_@min_double", Double.class, x -> x), - CelRuntime.CelFunctionBinding.from("math_@min_int", Long.class, x -> x), - CelRuntime.CelFunctionBinding.from( - "math_@min_double_double", Double.class, Double.class, CelMathExtensions::minPair), - CelRuntime.CelFunctionBinding.from( - "math_@min_int_int", Long.class, Long.class, CelMathExtensions::minPair), - CelRuntime.CelFunctionBinding.from( - "math_@min_int_double", Long.class, Double.class, CelMathExtensions::minPair), - CelRuntime.CelFunctionBinding.from( - "math_@min_double_int", Double.class, Long.class, CelMathExtensions::minPair), - CelRuntime.CelFunctionBinding.from( - "math_@min_list_dyn", List.class, CelMathExtensions::minList)), + CelFunctionBinding.from("math_round_double", Double.class, CelMathExtensions::round))), + TRUNC( + CelFunctionDecl.newFunctionDeclaration( + MATH_TRUNC_FUNCTION, + CelOverloadDecl.newGlobalOverload( + "math_trunc_double", + "Truncates the fractional portion of the double value.", + SimpleType.DOUBLE, + SimpleType.DOUBLE)), ImmutableSet.of( - CelRuntime.CelFunctionBinding.from("math_@min_uint", Long.class, x -> x), - CelRuntime.CelFunctionBinding.from( - "math_@min_uint_uint", Long.class, Long.class, CelMathExtensions::minPair), - CelRuntime.CelFunctionBinding.from( - "math_@min_double_uint", Double.class, Long.class, CelMathExtensions::minPair), - CelRuntime.CelFunctionBinding.from( - "math_@min_uint_int", Long.class, Long.class, CelMathExtensions::minPair), - CelRuntime.CelFunctionBinding.from( - "math_@min_uint_double", Long.class, Double.class, CelMathExtensions::minPair), - CelRuntime.CelFunctionBinding.from( - "math_@min_int_uint", Long.class, Long.class, CelMathExtensions::minPair)), + CelFunctionBinding.from("math_trunc_double", Double.class, CelMathExtensions::trunc))), + ISFINITE( + CelFunctionDecl.newFunctionDeclaration( + MATH_ISFINITE_FUNCTION, + CelOverloadDecl.newGlobalOverload( + "math_isFinite_double", + "Returns true if the value is a finite number.", + SimpleType.BOOL, + SimpleType.DOUBLE)), ImmutableSet.of( - CelRuntime.CelFunctionBinding.from("math_@min_uint", UnsignedLong.class, x -> x), - CelRuntime.CelFunctionBinding.from( - "math_@min_uint_uint", + CelFunctionBinding.from("math_isFinite_double", Double.class, Double::isFinite))), + ISNAN( + CelFunctionDecl.newFunctionDeclaration( + MATH_ISNAN_FUNCTION, + CelOverloadDecl.newGlobalOverload( + "math_isNaN_double", + "Returns true if the input double value is NaN, false otherwise.", + SimpleType.BOOL, + SimpleType.DOUBLE)), + ImmutableSet.of( + CelFunctionBinding.from("math_isNaN_double", Double.class, CelMathExtensions::isNaN))), + ISINF( + CelFunctionDecl.newFunctionDeclaration( + MATH_ISINF_FUNCTION, + CelOverloadDecl.newGlobalOverload( + "math_isInf_double", + "Returns true if the input double value is -Inf or +Inf.", + SimpleType.BOOL, + SimpleType.DOUBLE)), + ImmutableSet.of( + CelFunctionBinding.from( + "math_isInf_double", Double.class, CelMathExtensions::isInfinite))), + ABS( + CelFunctionDecl.newFunctionDeclaration( + MATH_ABS_FUNCTION, + CelOverloadDecl.newGlobalOverload( + "math_abs_double", + "Compute the absolute value of a double value.", + SimpleType.DOUBLE, + SimpleType.DOUBLE), + CelOverloadDecl.newGlobalOverload( + "math_abs_int", + "Compute the absolute value of an int value.", + SimpleType.INT, + SimpleType.INT), + CelOverloadDecl.newGlobalOverload( + "math_abs_uint", + "Compute the absolute value of a uint value.", + SimpleType.UINT, + SimpleType.UINT)), + ImmutableSet.of( + CelFunctionBinding.from("math_abs_double", Double.class, Math::abs), + CelFunctionBinding.from("math_abs_int", Long.class, CelMathExtensions::absExact), + CelFunctionBinding.from("math_abs_uint", UnsignedLong.class, x -> x))), + SIGN( + CelFunctionDecl.newFunctionDeclaration( + MATH_SIGN_FUNCTION, + CelOverloadDecl.newGlobalOverload( + "math_sign_double", + "Returns the sign of the input numeric type, either -1, 0, 1 cast as double.", + SimpleType.DOUBLE, + SimpleType.DOUBLE), + CelOverloadDecl.newGlobalOverload( + "math_sign_uint", + "Returns the sign of the input numeric type, either -1, 0, 1 case as uint.", + SimpleType.UINT, + SimpleType.UINT), + CelOverloadDecl.newGlobalOverload( + "math_sign_int", + "Returns the sign of the input numeric type, either -1, 0, 1.", + SimpleType.INT, + SimpleType.INT)), + ImmutableSet.of( + CelFunctionBinding.from("math_sign_double", Double.class, CelMathExtensions::sign), + CelFunctionBinding.from("math_sign_int", Long.class, CelMathExtensions::sign), + CelFunctionBinding.from( + "math_sign_uint", UnsignedLong.class, CelMathExtensions::sign))), + BITAND( + CelFunctionDecl.newFunctionDeclaration( + MATH_BIT_AND_FUNCTION, + CelOverloadDecl.newGlobalOverload( + "math_bitAnd_int_int", + "Performs a bitwise-AND operation over two int values.", + SimpleType.INT, + SimpleType.INT, + SimpleType.INT), + CelOverloadDecl.newGlobalOverload( + "math_bitAnd_uint_uint", + "Performs a bitwise-AND operation over two uint values.", + SimpleType.UINT, + SimpleType.UINT, + SimpleType.UINT)), + ImmutableSet.of( + CelFunctionBinding.from( + "math_bitAnd_int_int", Long.class, Long.class, CelMathExtensions::intBitAnd), + CelFunctionBinding.from( + "math_bitAnd_uint_uint", UnsignedLong.class, UnsignedLong.class, - CelMathExtensions::minPair), - CelRuntime.CelFunctionBinding.from( - "math_@min_double_uint", - Double.class, + CelMathExtensions::uintBitAnd))), + BITOR( + CelFunctionDecl.newFunctionDeclaration( + MATH_BIT_OR_FUNCTION, + CelOverloadDecl.newGlobalOverload( + "math_bitOr_int_int", + "Performs a bitwise-OR operation over two int values.", + SimpleType.INT, + SimpleType.INT, + SimpleType.INT), + CelOverloadDecl.newGlobalOverload( + "math_bitOr_uint_uint", + "Performs a bitwise-OR operation over two uint values.", + SimpleType.UINT, + SimpleType.UINT, + SimpleType.UINT)), + ImmutableSet.of( + CelFunctionBinding.from( + "math_bitOr_int_int", Long.class, Long.class, CelMathExtensions::intBitOr), + CelFunctionBinding.from( + "math_bitOr_uint_uint", UnsignedLong.class, - CelMathExtensions::minPair), - CelRuntime.CelFunctionBinding.from( - "math_@min_uint_int", UnsignedLong.class, Long.class, CelMathExtensions::minPair), - CelRuntime.CelFunctionBinding.from( - "math_@min_uint_double", UnsignedLong.class, - Double.class, - CelMathExtensions::minPair), - CelRuntime.CelFunctionBinding.from( - "math_@min_int_uint", Long.class, UnsignedLong.class, CelMathExtensions::minPair))); + CelMathExtensions::uintBitOr))), + BITXOR( + CelFunctionDecl.newFunctionDeclaration( + MATH_BIT_XOR_FUNCTION, + CelOverloadDecl.newGlobalOverload( + "math_bitXor_int_int", + "Performs a bitwise-XOR operation over two int values.", + SimpleType.INT, + SimpleType.INT, + SimpleType.INT), + CelOverloadDecl.newGlobalOverload( + "math_bitXor_uint_uint", + "Performs a bitwise-XOR operation over two uint values.", + SimpleType.UINT, + SimpleType.UINT, + SimpleType.UINT)), + ImmutableSet.of( + CelFunctionBinding.from( + "math_bitXor_int_int", Long.class, Long.class, CelMathExtensions::intBitXor), + CelFunctionBinding.from( + "math_bitXor_uint_uint", + UnsignedLong.class, + UnsignedLong.class, + CelMathExtensions::uintBitXor))), + BITNOT( + CelFunctionDecl.newFunctionDeclaration( + MATH_BIT_NOT_FUNCTION, + CelOverloadDecl.newGlobalOverload( + "math_bitNot_int_int", + "Performs a bitwise-NOT operation over two int values.", + SimpleType.INT, + SimpleType.INT), + CelOverloadDecl.newGlobalOverload( + "math_bitNot_uint_uint", + "Performs a bitwise-NOT operation over two uint values.", + SimpleType.UINT, + SimpleType.UINT)), + ImmutableSet.of( + CelFunctionBinding.from( + "math_bitNot_int_int", Long.class, CelMathExtensions::intBitNot), + CelFunctionBinding.from( + "math_bitNot_uint_uint", UnsignedLong.class, CelMathExtensions::uintBitNot))), + BITSHIFTLEFT( + CelFunctionDecl.newFunctionDeclaration( + MATH_BIT_LEFT_SHIFT_FUNCTION, + CelOverloadDecl.newGlobalOverload( + "math_bitShiftLeft_int_int", + "Performs a bitwise-SHIFTLEFT operation over two int values.", + SimpleType.INT, + SimpleType.INT, + SimpleType.INT), + CelOverloadDecl.newGlobalOverload( + "math_bitShiftLeft_uint_int", + "Performs a bitwise-SHIFTLEFT operation over two uint values.", + SimpleType.UINT, + SimpleType.UINT, + SimpleType.INT)), + ImmutableSet.of( + CelFunctionBinding.from( + "math_bitShiftLeft_int_int", + Long.class, + Long.class, + CelMathExtensions::intBitShiftLeft), + CelFunctionBinding.from( + "math_bitShiftLeft_uint_int", + UnsignedLong.class, + Long.class, + CelMathExtensions::uintBitShiftLeft))), + BITSHIFTRIGHT( + CelFunctionDecl.newFunctionDeclaration( + MATH_BIT_RIGHT_SHIFT_FUNCTION, + CelOverloadDecl.newGlobalOverload( + "math_bitShiftRight_int_int", + "Performs a bitwise-SHIFTRIGHT operation over two int values.", + SimpleType.INT, + SimpleType.INT, + SimpleType.INT), + CelOverloadDecl.newGlobalOverload( + "math_bitShiftRight_uint_int", + "Performs a bitwise-SHIFTRIGHT operation over two uint values.", + SimpleType.UINT, + SimpleType.UINT, + SimpleType.INT)), + ImmutableSet.of( + CelFunctionBinding.from( + "math_bitShiftRight_int_int", + Long.class, + Long.class, + CelMathExtensions::intBitShiftRight), + CelFunctionBinding.from( + "math_bitShiftRight_uint_int", + UnsignedLong.class, + Long.class, + CelMathExtensions::uintBitShiftRight))), + SQRT( + CelFunctionDecl.newFunctionDeclaration( + MATH_SQRT_FUNCTION, + CelOverloadDecl.newGlobalOverload( + "math_sqrt_double", + "Computes square root of the double value.", + SimpleType.DOUBLE, + SimpleType.DOUBLE), + CelOverloadDecl.newGlobalOverload( + "math_sqrt_int", + "Computes square root of the int value.", + SimpleType.DOUBLE, + SimpleType.INT), + CelOverloadDecl.newGlobalOverload( + "math_sqrt_uint", + "Computes square root of the unsigned value.", + SimpleType.DOUBLE, + SimpleType.UINT)), + ImmutableSet.of( + CelFunctionBinding.from( + "math_sqrt_double", Double.class, CelMathExtensions::sqrtDouble), + CelFunctionBinding.from("math_sqrt_int", Long.class, CelMathExtensions::sqrtInt), + CelFunctionBinding.from( + "math_sqrt_uint", UnsignedLong.class, CelMathExtensions::sqrtUint))); private final CelFunctionDecl functionDecl; - private final ImmutableSet functionBindings; - private final ImmutableSet functionBindingsULongSigned; - private final ImmutableSet functionBindingsULongUnsigned; - - Function( - CelFunctionDecl functionDecl, - ImmutableSet functionBindings, - ImmutableSet functionBindingsULongSigned, - ImmutableSet functionBindingsULongUnsigned) { + private final ImmutableSet functionBindings; + + String getFunction() { + return functionDecl.name(); + } + + Function(CelFunctionDecl functionDecl, ImmutableSet bindings) { this.functionDecl = functionDecl; - this.functionBindings = functionBindings; - this.functionBindingsULongSigned = functionBindingsULongSigned; - this.functionBindingsULongUnsigned = functionBindingsULongUnsigned; + this.functionBindings = bindings; + } + } + + private static final class Library implements CelExtensionLibrary { + private final CelMathExtensions version0; + private final CelMathExtensions version1; + private final CelMathExtensions version2; + + Library() { + version0 = new CelMathExtensions(0, ImmutableSet.of(Function.MIN, Function.MAX)); + + version1 = + new CelMathExtensions( + 1, + ImmutableSet.builder() + .addAll(version0.functions) + .add( + Function.CEIL, + Function.FLOOR, + Function.ROUND, + Function.TRUNC, + Function.ISINF, + Function.ISNAN, + Function.ISFINITE, + Function.ABS, + Function.SIGN, + Function.BITAND, + Function.BITOR, + Function.BITXOR, + Function.BITNOT, + Function.BITSHIFTLEFT, + Function.BITSHIFTRIGHT) + .build()); + + version2 = + new CelMathExtensions( + 2, + ImmutableSet.builder() + .addAll(version1.functions) + .add(Function.SQRT) + .build()); + } + + @Override + public String name() { + return "math"; + } + + @Override + public ImmutableSet versions() { + return ImmutableSet.of(version0, version1, version2); } } - private final boolean enableUnsignedLongs; + private static final Library LIBRARY = new Library(); + + static CelExtensionLibrary library() { + return LIBRARY; + } + private final ImmutableSet functions; + private final int version; - CelMathExtensions(CelOptions celOptions) { - this(celOptions, ImmutableSet.copyOf(Function.values())); + CelMathExtensions(Set functions) { + this(-1, functions); } - CelMathExtensions(CelOptions celOptions, Set functions) { - this.enableUnsignedLongs = celOptions.enableUnsignedLongs(); + private CelMathExtensions(int version, Set functions) { + this.version = version; this.functions = ImmutableSet.copyOf(functions); } @Override - public void setParserOptions(CelParserBuilder parserBuilder) { - parserBuilder.addMacros( + public int version() { + return version; + } + + @Override + public ImmutableSet functions() { + return functions.stream().map(f -> f.functionDecl).collect(toImmutableSet()); + } + + @Override + public ImmutableSet macros() { + return ImmutableSet.of( CelMacro.newReceiverVarArgMacro("greatest", CelMathExtensions::expandGreatestMacro), CelMacro.newReceiverVarArgMacro("least", CelMathExtensions::expandLeastMacro)); } + @Override + public void setParserOptions(CelParserBuilder parserBuilder) { + parserBuilder.addMacros(macros()); + } + @Override public void setCheckerOptions(CelCheckerBuilder checkerBuilder) { functions.forEach(function -> checkerBuilder.addFunctionDeclarations(function.functionDecl)); @@ -381,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)); + } }); } @@ -455,6 +847,155 @@ private static Comparable minPair(Comparable x, Comparable y) { return CLASSES_TO_COMPARATORS.get(x.getClass(), y.getClass()).apply(x, y) <= 0 ? x : y; } + private static long absExact(long x) { + if (x == Long.MIN_VALUE) { + // The only case where standard Math.abs overflows silently + throw new CelNumericOverflowException("integer overflow"); + } + return Math.abs(x); + } + + private static boolean isNaN(double x) { + return Double.isNaN(x); + } + + private static Double trunc(Double x) { + if (isNaN(x) || isInfinite(x)) { + return x; + } + return (double) x.longValue(); + } + + private static boolean isInfinite(double x) { + return Double.isInfinite(x); + } + + private static double round(double x) { + if (isNaN(x) || isInfinite(x)) { + return x; + } + return DoubleMath.roundToLong(x, RoundingMode.HALF_UP); + } + + private static Number sign(Number x) { + if (x instanceof Double) { + double val = x.doubleValue(); + if (isNaN(val)) { + return val; + } + if (val == 0) { + return 0.0; + } + return val > 0 ? 1.0 : -1.0; + } + + if (x instanceof Long) { + long val = x.longValue(); + if (val == 0) { + return 0L; + } + return val > 0 ? 1L : -1L; + } + + if (x instanceof UnsignedLong) { + UnsignedLong val = (UnsignedLong) x; + if (val.equals(UnsignedLong.ZERO)) { + return val; + } + return UnsignedLong.ONE; + } + + throw new IllegalArgumentException("Unsupported type: " + x.getClass()); + } + + private static Long intBitAnd(long x, long y) { + return x & y; + } + + private static UnsignedLong uintBitAnd(UnsignedLong x, UnsignedLong y) { + return UnsignedLong.fromLongBits(x.longValue() & y.longValue()); + } + + private static Long intBitOr(long x, long y) { + return x | y; + } + + private static UnsignedLong uintBitOr(UnsignedLong x, UnsignedLong y) { + return UnsignedLong.fromLongBits(x.longValue() | y.longValue()); + } + + private static Long intBitXor(long x, long y) { + return x ^ y; + } + + private static UnsignedLong uintBitXor(UnsignedLong x, UnsignedLong y) { + return UnsignedLong.fromLongBits(x.longValue() ^ y.longValue()); + } + + private static Long intBitNot(long x) { + return ~x; + } + + private static UnsignedLong uintBitNot(UnsignedLong x) { + return UnsignedLong.fromLongBits(~x.longValue()); + } + + private static Long intBitShiftLeft(long value, long shiftAmount) { + if (shiftAmount < 0) { + throw new IllegalArgumentException("math.bitShiftLeft() negative offset:" + shiftAmount); + } + + if (shiftAmount > MAX_BIT_SHIFT) { + return 0L; + } + return value << shiftAmount; + } + + private static UnsignedLong uintBitShiftLeft(UnsignedLong value, long shiftAmount) { + if (shiftAmount < 0) { + throw new IllegalArgumentException("math.bitShiftLeft() negative offset:" + shiftAmount); + } + + if (shiftAmount > MAX_BIT_SHIFT) { + return UnsignedLong.ZERO; + } + return UnsignedLong.fromLongBits(value.longValue() << shiftAmount); + } + + private static Long intBitShiftRight(long value, long shiftAmount) { + if (shiftAmount < 0) { + throw new IllegalArgumentException("math.bitShiftRight() negative offset:" + shiftAmount); + } + + if (shiftAmount > MAX_BIT_SHIFT) { + return 0L; + } + return value >>> shiftAmount; + } + + private static UnsignedLong uintBitShiftRight(UnsignedLong value, long shiftAmount) { + if (shiftAmount < 0) { + throw new IllegalArgumentException("math.bitShiftRight() negative offset:" + shiftAmount); + } + + if (shiftAmount > MAX_BIT_SHIFT) { + return UnsignedLong.ZERO; + } + return UnsignedLong.fromLongBits(value.longValue() >>> shiftAmount); + } + + private static Double sqrtDouble(double x) { + return Math.sqrt(x); + } + + private static Double sqrtInt(Long x) { + return sqrtDouble(x.doubleValue()); + } + + private static Double sqrtUint(UnsignedLong x) { + return sqrtDouble(x.doubleValue()); + } + private static Comparable minList(List list) { if (list.isEmpty()) { throw new IllegalStateException("math.@min(list) argument must not be empty"); @@ -526,13 +1067,13 @@ private static Optional checkInvalidArgument( private static Optional checkInvalidArgumentSingleArg( CelMacroExprFactory exprFactory, String functionName, CelExpr argument) { - if (argument.exprKind().getKind() == Kind.CREATE_LIST) { - if (argument.createList().elements().isEmpty()) { + if (argument.exprKind().getKind() == Kind.LIST) { + if (argument.list().elements().isEmpty()) { return newError( exprFactory, String.format("%s invalid single argument value", functionName), argument); } - return checkInvalidArgument(exprFactory, functionName, argument.createList().elements()); + return checkInvalidArgument(exprFactory, functionName, argument.list().elements()); } if (isArgumentValidType(argument)) { return Optional.empty(); @@ -548,9 +1089,9 @@ private static boolean isArgumentValidType(CelExpr argument) { return constant.getKind() == CelConstant.Kind.INT64_VALUE || constant.getKind() == CelConstant.Kind.UINT64_VALUE || constant.getKind() == CelConstant.Kind.DOUBLE_VALUE; - } else if (argument.exprKind().getKind().equals(Kind.CREATE_LIST) - || argument.exprKind().getKind().equals(Kind.CREATE_STRUCT) - || argument.exprKind().getKind().equals(Kind.CREATE_MAP)) { + } else if (argument.exprKind().getKind().equals(Kind.LIST) + || argument.exprKind().getKind().equals(Kind.STRUCT) + || argument.exprKind().getKind().equals(Kind.MAP)) { return false; } 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 1da3e57d1..87a31341f 100644 --- a/extensions/src/main/java/dev/cel/extensions/CelOptionalLibrary.java +++ b/extensions/src/main/java/dev/cel/extensions/CelOptionalLibrary.java @@ -16,39 +16,62 @@ 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; +import static dev.cel.extensions.CelOptionalLibrary.Function.OPTIONAL_NONE; +import static dev.cel.extensions.CelOptionalLibrary.Function.OPTIONAL_OF; +import static dev.cel.extensions.CelOptionalLibrary.Function.OPTIONAL_OF_NON_ZERO_VALUE; +import static dev.cel.extensions.CelOptionalLibrary.Function.OPTIONAL_UNWRAP; +import static dev.cel.extensions.CelOptionalLibrary.Function.VALUE; +import static dev.cel.runtime.CelFunctionBinding.fromOverloads; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.primitives.Ints; import com.google.common.primitives.UnsignedLong; -import com.google.protobuf.ByteString; import com.google.protobuf.Duration; import com.google.protobuf.Message; -import com.google.protobuf.NullValue; import com.google.protobuf.Timestamp; 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.CelVarDecl; +import dev.cel.common.Operator; import dev.cel.common.ast.CelExpr; 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.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; import dev.cel.parser.CelMacroExprFactory; import dev.cel.parser.CelParserBuilder; -import dev.cel.parser.Operator; -import dev.cel.runtime.CelRuntime; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.CelInternalRuntimeLibrary; import dev.cel.runtime.CelRuntimeBuilder; -import dev.cel.runtime.CelRuntimeLibrary; +import dev.cel.runtime.RuntimeEquality; +import java.time.Instant; import java.util.Collection; +import java.util.List; import java.util.Map; import java.util.Optional; /** Internal implementation of CEL optional values. */ -public final class CelOptionalLibrary implements CelCompilerLibrary, CelRuntimeLibrary { - public static final CelOptionalLibrary INSTANCE = new CelOptionalLibrary(); +public final class CelOptionalLibrary + implements CelCompilerLibrary, CelInternalRuntimeLibrary, CelExtensionLibrary.FeatureSet { /** Enumerations of function names used for supporting optionals. */ public enum Function { @@ -56,9 +79,13 @@ public enum Function { HAS_VALUE("hasValue"), OPTIONAL_NONE("optional.none"), OPTIONAL_OF("optional.of"), + OPTIONAL_UNWRAP("optional.unwrap"), OPTIONAL_OF_NON_ZERO_VALUE("optional.ofNonZeroValue"), OR("or"), - OR_VALUE("orValue"); + OR_VALUE("orValue"), + FIRST("first"), + LAST("last"); + private final String functionName; public String getFunction() { @@ -70,8 +97,190 @@ public String getFunction() { } } + 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))), + // 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)), + // 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)), + // 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))), + 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)))); + + private final CelOptionalLibrary version1 = + new CelOptionalLibrary( + 1, + version0.functions, + ImmutableSet.builder() + .addAll(version0.macros) + .add( + CelMacro.newReceiverMacro( + "optFlatMap", 2, CelOptionalLibrary::expandOptFlatMap)) + .build(), + version0.variables); + + private final CelOptionalLibrary version2 = + new CelOptionalLibrary( + 2, + ImmutableSet.builder() + .addAll(version1.functions) + .add( + CelFunctionDecl.newFunctionDeclaration( + FIRST.functionName, + CelOverloadDecl.newMemberOverload( + "optional_list_first", + "Return the first value in a list if present, otherwise" + + " optional.none()", + optionalTypeV, + listTypeV)), + CelFunctionDecl.newFunctionDeclaration( + LAST.functionName, + CelOverloadDecl.newMemberOverload( + "optional_list_last", + "Return the last value in a list if present, otherwise" + + " optional.none()", + optionalTypeV, + listTypeV))) + .build(), + version1.macros, + version1.variables); + + @Override + public String name() { + return "optional"; + } + + @Override + public ImmutableSet versions() { + return ImmutableSet.of(version0, version1, version2); + } + }; + + static CelExtensionLibrary library() { + return LIBRARY; + } + + // TODO migrate from this constant to the CelExtensions.optional() + public static final CelOptionalLibrary INSTANCE = CelOptionalLibrary.library().latest(); + private static final String UNUSED_ITER_VAR = "#unused"; + private final int version; + private final ImmutableSet functions; + private final ImmutableSet macros; + private final ImmutableSet variables; + + CelOptionalLibrary( + int version, + ImmutableSet functions, + ImmutableSet macros, + ImmutableSet variables) { + this.version = version; + this.functions = functions; + this.macros = macros; + this.variables = variables; + } + + @Override + public int version() { + return version; + } + + @Override + public ImmutableSet functions() { + return functions; + } + + @Override + public ImmutableSet variables() { + return variables; + } + + @Override + public ImmutableSet macros() { + return macros; + } + @Override public void setParserOptions(CelParserBuilder parserBuilder) { if (!parserBuilder.getOptions().enableOptionalSyntax()) { @@ -80,107 +289,133 @@ public void setParserOptions(CelParserBuilder parserBuilder) { parserBuilder.setOptions( parserBuilder.getOptions().toBuilder().enableOptionalSyntax(true).build()); } - parserBuilder.addMacros( - CelMacro.newReceiverMacro("optMap", 2, CelOptionalLibrary::expandOptMap)); - parserBuilder.addMacros( - CelMacro.newReceiverMacro("optFlatMap", 2, CelOptionalLibrary::expandOptFlatMap)); + parserBuilder.addMacros(macros()); } @Override public void setCheckerOptions(CelCheckerBuilder checkerBuilder) { - TypeParamType paramTypeK = TypeParamType.create("K"); - TypeParamType paramTypeV = TypeParamType.create("V"); - OptionalType optionalTypeV = OptionalType.create(paramTypeV); - ListType listTypeV = ListType.create(paramTypeV); - MapType mapTypeKv = MapType.create(paramTypeK, paramTypeV); - checkerBuilder.addFunctionDeclarations( - CelFunctionDecl.newFunctionDeclaration( - Function.OPTIONAL_OF.getFunction(), - CelOverloadDecl.newGlobalOverload("optional_of", optionalTypeV, paramTypeV)), - CelFunctionDecl.newFunctionDeclaration( - Function.OPTIONAL_OF_NON_ZERO_VALUE.getFunction(), - CelOverloadDecl.newGlobalOverload( - "optional_ofNonZeroValue", optionalTypeV, paramTypeV)), - CelFunctionDecl.newFunctionDeclaration( - Function.OPTIONAL_NONE.getFunction(), - CelOverloadDecl.newGlobalOverload("optional_none", optionalTypeV)), - CelFunctionDecl.newFunctionDeclaration( - Function.VALUE.getFunction(), - CelOverloadDecl.newMemberOverload("optional_value", paramTypeV, optionalTypeV)), - CelFunctionDecl.newFunctionDeclaration( - Function.HAS_VALUE.getFunction(), - CelOverloadDecl.newMemberOverload("optional_hasValue", SimpleType.BOOL, optionalTypeV)), - // 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)), - // 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( + checkerBuilder.addVarDeclarations(variables()); + checkerBuilder.addFunctionDeclarations(functions()); + } + + @Override + public void setRuntimeOptions(CelRuntimeBuilder runtimeBuilder) { + throw new UnsupportedOperationException("Unsupported"); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + @Override + public void setRuntimeOptions( + CelRuntimeBuilder runtimeBuilder, RuntimeEquality runtimeEquality, CelOptions celOptions) { + runtimeBuilder.addFunctionBindings( + fromOverloads( + OPTIONAL_OF.getFunction(), + CelFunctionBinding.from("optional_of", Object.class, Optional::of))); + runtimeBuilder.addFunctionBindings( + fromOverloads( + OPTIONAL_OF_NON_ZERO_VALUE.getFunction(), + CelFunctionBinding.from( + "optional_ofNonZeroValue", + Object.class, + val -> { + if (isZeroValue(val)) { + return Optional.empty(); + } + return Optional.of(val); + }))); + runtimeBuilder.addFunctionBindings( + fromOverloads( + OPTIONAL_UNWRAP.getFunction(), + CelFunctionBinding.from( + "optional_unwrap_list", + Collection.class, + CelOptionalLibrary::elideOptionalCollection))); + runtimeBuilder.addFunctionBindings( + fromOverloads( + OPTIONAL_NONE.getFunction(), + CelFunctionBinding.from("optional_none", ImmutableList.of(), val -> Optional.empty()))); + runtimeBuilder.addFunctionBindings( + fromOverloads( + VALUE.getFunction(), + CelFunctionBinding.from( + "optional_value", Object.class, val -> ((Optional) val).get()))); + runtimeBuilder.addFunctionBindings( + fromOverloads( + HAS_VALUE.getFunction(), + CelFunctionBinding.from( + "optional_hasValue", Object.class, val -> ((Optional) val).isPresent()))); + + runtimeBuilder.addFunctionBindings( + 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", - optionalTypeV, - OptionalType.create(listTypeV), - SimpleType.INT), - CelOverloadDecl.newGlobalOverload( - "map_optindex_optional_value", optionalTypeV, mapTypeKv, paramTypeK), - CelOverloadDecl.newGlobalOverload( + 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", - optionalTypeV, - OptionalType.create(mapTypeKv), - paramTypeK)), - // Index overloads to accommodate using an optional value as the operand - CelFunctionDecl.newFunctionDeclaration( - Operator.INDEX.getFunction(), - CelOverloadDecl.newGlobalOverload( + Optional.class, + Object.class, + (Optional optionalMap, Object key) -> + indexOptionalMap(optionalMap, key, runtimeEquality)))); + + runtimeBuilder.addFunctionBindings( + fromOverloads( + INDEX.getFunction(), + CelFunctionBinding.from( "optional_list_index_int", - optionalTypeV, - OptionalType.create(listTypeV), - SimpleType.INT), - CelOverloadDecl.newGlobalOverload( + Optional.class, + Long.class, + CelOptionalLibrary::indexOptionalList), + CelFunctionBinding.from( "optional_map_index_value", - optionalTypeV, - OptionalType.create(mapTypeKv), - paramTypeK))); + Optional.class, + Object.class, + (Optional optionalMap, Object key) -> + indexOptionalMap(optionalMap, key, runtimeEquality)))); + + if (version >= 2) { + runtimeBuilder.addFunctionBindings( + fromOverloads( + "first", + CelFunctionBinding.from( + "optional_list_first", Collection.class, CelOptionalLibrary::listOptionalFirst))); + runtimeBuilder.addFunctionBindings( + fromOverloads( + "last", + CelFunctionBinding.from( + "optional_list_last", Collection.class, CelOptionalLibrary::listOptionalLast))); + } } - @Override - public void setRuntimeOptions(CelRuntimeBuilder runtimeBuilder) { - runtimeBuilder.addFunctionBindings( - CelRuntime.CelFunctionBinding.from("optional_of", Object.class, Optional::of), - CelRuntime.CelFunctionBinding.from( - "optional_ofNonZeroValue", - Object.class, - val -> { - if (isZeroValue(val)) { - return Optional.empty(); - } - return Optional.of(val); - }), - CelRuntime.CelFunctionBinding.from( - "optional_none", ImmutableList.of(), val -> Optional.empty()), - CelRuntime.CelFunctionBinding.from( - "optional_value", Object.class, val -> ((Optional) val).get()), - CelRuntime.CelFunctionBinding.from( - "optional_hasValue", Object.class, val -> ((Optional) val).isPresent())); + private static ImmutableList elideOptionalCollection(Collection> list) { + return list.stream().filter(Optional::isPresent).map(Optional::get).collect(toImmutableList()); } - // TODO: This will need to be adapted to handle an intermediate CelValue instead, - // akin to Zeroer interface in Go. Currently, it is unable to handle zero-values for a - // user-defined custom type. private static boolean isZeroValue(Object val) { if (val instanceof Boolean) { return !((Boolean) val); @@ -196,8 +431,8 @@ private static boolean isZeroValue(Object val) { return ((Collection) val).isEmpty(); } else if (val instanceof Map) { return ((Map) val).isEmpty(); - } else if (val instanceof ByteString) { - return ((ByteString) val).size() == 0; + } else if (val instanceof CelByteString) { + return ((CelByteString) val).isEmpty(); } else if (val instanceof Duration) { return val.equals(((Duration) val).getDefaultInstanceForType()); } else if (val instanceof Timestamp) { @@ -207,6 +442,12 @@ private static boolean isZeroValue(Object val) { } else if (val instanceof NullValue) { // A null value always represents an absent value return true; + } else if (val instanceof Instant) { + return val.equals(Instant.EPOCH); + } else if (val instanceof java.time.Duration) { + return val.equals(java.time.Duration.ZERO); + } else if (val instanceof CelValue) { + return ((CelValue) val).isZeroValue(); } // Unknown. Assume that it is non-zero. @@ -233,18 +474,18 @@ private static Optional expandOptMap( return Optional.of( exprFactory.newGlobalCall( Operator.CONDITIONAL.getFunction(), - exprFactory.newReceiverCall(Function.HAS_VALUE.getFunction(), target), + exprFactory.newReceiverCall(HAS_VALUE.getFunction(), target), exprFactory.newGlobalCall( - Function.OPTIONAL_OF.getFunction(), + OPTIONAL_OF.getFunction(), exprFactory.fold( UNUSED_ITER_VAR, exprFactory.newList(), varName, - exprFactory.newReceiverCall(Function.VALUE.getFunction(), target), + exprFactory.newReceiverCall(VALUE.getFunction(), exprFactory.copy(target)), exprFactory.newBoolLiteral(true), exprFactory.newIdentifier(varName), mapExpr)), - exprFactory.newGlobalCall(Function.OPTIONAL_NONE.getFunction()))); + exprFactory.newGlobalCall(OPTIONAL_NONE.getFunction()))); } private static Optional expandOptFlatMap( @@ -267,17 +508,53 @@ private static Optional expandOptFlatMap( return Optional.of( exprFactory.newGlobalCall( Operator.CONDITIONAL.getFunction(), - exprFactory.newReceiverCall(Function.HAS_VALUE.getFunction(), target), + exprFactory.newReceiverCall(HAS_VALUE.getFunction(), target), exprFactory.fold( UNUSED_ITER_VAR, exprFactory.newList(), varName, - exprFactory.newReceiverCall(Function.VALUE.getFunction(), target), + exprFactory.newReceiverCall(VALUE.getFunction(), exprFactory.copy(target)), exprFactory.newBoolLiteral(true), exprFactory.newIdentifier(varName), mapExpr), - exprFactory.newGlobalCall(Function.OPTIONAL_NONE.getFunction()))); + exprFactory.newGlobalCall(OPTIONAL_NONE.getFunction()))); + } + + private static Object indexOptionalMap( + Optional optionalMap, Object key, RuntimeEquality runtimeEquality) { + if (!optionalMap.isPresent()) { + return Optional.empty(); + } + + Map map = (Map) optionalMap.get(); + + return runtimeEquality.findInMap(map, key); } - private CelOptionalLibrary() {} + private static Object indexOptionalList(Optional optionalList, long index) { + if (!optionalList.isPresent()) { + return Optional.empty(); + } + List list = (List) optionalList.get(); + int castIndex = Ints.checkedCast(index); + if (castIndex < 0 || castIndex >= list.size()) { + return Optional.empty(); + } + return Optional.of(list.get(castIndex)); + } + + @SuppressWarnings("rawtypes") + private static Object listOptionalFirst(Collection list) { + if (list.isEmpty()) { + return Optional.empty(); + } + if (list instanceof List) { + return Optional.ofNullable(((List) list).get(0)); + } + return Optional.ofNullable(Iterables.getFirst(list, null)); + } + + private static Object listOptionalLast(Collection list) { + return Optional.ofNullable(Iterables.getLast(list, null)); + } } diff --git a/extensions/src/main/java/dev/cel/extensions/CelProtoExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelProtoExtensions.java index bb932bc41..6fe3c4c0c 100644 --- a/extensions/src/main/java/dev/cel/extensions/CelProtoExtensions.java +++ b/extensions/src/main/java/dev/cel/extensions/CelProtoExtensions.java @@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import com.google.errorprone.annotations.Immutable; import dev.cel.common.CelIssue; import dev.cel.common.ast.CelExpr; @@ -30,18 +31,48 @@ /** Internal implementation of CEL proto extensions. */ @Immutable -final class CelProtoExtensions implements CelCompilerLibrary { +public final class CelProtoExtensions + implements CelCompilerLibrary, CelExtensionLibrary.FeatureSet { private static final String PROTO_NAMESPACE = "proto"; private static final CelExpr ERROR = CelExpr.newBuilder().setConstant(Constants.ERROR).build(); + private static final CelExtensionLibrary LIBRARY = + new CelExtensionLibrary() { + private final CelProtoExtensions version0 = new CelProtoExtensions(); + + @Override + public String name() { + return "protos"; + } + + @Override + public ImmutableSet versions() { + return ImmutableSet.of(version0); + } + }; + + static CelExtensionLibrary library() { + return LIBRARY; + } + @Override - public void setParserOptions(CelParserBuilder parserBuilder) { - parserBuilder.addMacros( + public int version() { + return 0; + } + + @Override + public ImmutableSet macros() { + return ImmutableSet.of( CelMacro.newReceiverMacro("hasExt", 2, CelProtoExtensions::expandHasProtoExt), CelMacro.newReceiverMacro("getExt", 2, CelProtoExtensions::expandGetProtoExt)); } + @Override + public void setParserOptions(CelParserBuilder parserBuilder) { + parserBuilder.addMacros(macros()); + } + private static Optional expandHasProtoExt( CelMacroExprFactory exprFactory, CelExpr target, ImmutableList arguments) { return expandProtoExt(exprFactory, target, arguments, true); @@ -118,4 +149,6 @@ private static boolean isTargetInNamespace(CelExpr target) { return target.exprKind().getKind().equals(CelExpr.ExprKind.Kind.IDENT) && target.ident().name().equals(PROTO_NAMESPACE); } + + CelProtoExtensions() {} } diff --git a/extensions/src/main/java/dev/cel/extensions/CelRegexExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelRegexExtensions.java new file mode 100644 index 000000000..564422cd4 --- /dev/null +++ b/extensions/src/main/java/dev/cel/extensions/CelRegexExtensions.java @@ -0,0 +1,310 @@ +// 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.extensions; + +import static com.google.common.collect.ImmutableSet.toImmutableSet; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.errorprone.annotations.Immutable; +import com.google.re2j.Matcher; +import com.google.re2j.Pattern; +import com.google.re2j.PatternSyntaxException; +import dev.cel.checker.CelCheckerBuilder; +import dev.cel.common.CelFunctionDecl; +import dev.cel.common.CelOverloadDecl; +import dev.cel.common.types.ListType; +import dev.cel.common.types.OptionalType; +import dev.cel.common.types.SimpleType; +import dev.cel.compiler.CelCompilerLibrary; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.CelRuntimeBuilder; +import dev.cel.runtime.CelRuntimeLibrary; +import java.util.Optional; +import java.util.Set; + +/** Internal implementation of CEL regex extensions. */ +@Immutable +public final class CelRegexExtensions + implements CelCompilerLibrary, CelRuntimeLibrary, CelExtensionLibrary.FeatureSet { + + private static final String REGEX_REPLACE_FUNCTION = "regex.replace"; + private static final String REGEX_EXTRACT_FUNCTION = "regex.extract"; + private static final String REGEX_EXTRACT_ALL_FUNCTION = "regex.extractAll"; + + enum Function { + REPLACE( + CelFunctionDecl.newFunctionDeclaration( + REGEX_REPLACE_FUNCTION, + CelOverloadDecl.newGlobalOverload( + "regex_replaceAll_string_string_string", + "Replaces all the matched values using the given replace string.", + SimpleType.STRING, + SimpleType.STRING, + SimpleType.STRING, + SimpleType.STRING), + CelOverloadDecl.newGlobalOverload( + "regex_replaceCount_string_string_string_int", + "Replaces the given number of matched values using the given replace string.", + SimpleType.STRING, + SimpleType.STRING, + SimpleType.STRING, + SimpleType.STRING, + SimpleType.INT)), + ImmutableSet.of( + CelFunctionBinding.from( + "regex_replaceAll_string_string_string", + ImmutableList.of(String.class, String.class, String.class), + (args) -> { + String target = (String) args[0]; + String pattern = (String) args[1]; + String replaceStr = (String) args[2]; + return CelRegexExtensions.replace(target, pattern, replaceStr); + }), + CelFunctionBinding.from( + "regex_replaceCount_string_string_string_int", + ImmutableList.of(String.class, String.class, String.class, Long.class), + (args) -> { + String target = (String) args[0]; + String pattern = (String) args[1]; + String replaceStr = (String) args[2]; + long count = (long) args[3]; + return CelRegexExtensions.replaceN(target, pattern, replaceStr, count); + }))), + EXTRACT( + CelFunctionDecl.newFunctionDeclaration( + REGEX_EXTRACT_FUNCTION, + CelOverloadDecl.newGlobalOverload( + "regex_extract_string_string", + "Returns the first substring that matches the regex.", + OptionalType.create(SimpleType.STRING), + SimpleType.STRING, + SimpleType.STRING)), + ImmutableSet.of( + CelFunctionBinding.from( + "regex_extract_string_string", + String.class, + String.class, + CelRegexExtensions::extract))), + EXTRACTALL( + CelFunctionDecl.newFunctionDeclaration( + REGEX_EXTRACT_ALL_FUNCTION, + CelOverloadDecl.newGlobalOverload( + "regex_extractAll_string_string", + "Returns an array of all substrings that match the regex.", + ListType.create(SimpleType.STRING), + SimpleType.STRING, + SimpleType.STRING)), + ImmutableSet.of( + CelFunctionBinding.from( + "regex_extractAll_string_string", + String.class, + String.class, + CelRegexExtensions::extractAll))); + + private final CelFunctionDecl functionDecl; + private final ImmutableSet functionBindings; + + String getFunction() { + return functionDecl.name(); + } + + Function(CelFunctionDecl functionDecl, ImmutableSet functionBindings) { + this.functionDecl = functionDecl; + this.functionBindings = + CelFunctionBinding.fromOverloads(functionDecl.name(), functionBindings); + } + } + + private static final CelExtensionLibrary LIBRARY = + new CelExtensionLibrary() { + private final CelRegexExtensions version0 = new CelRegexExtensions(); + + @Override + public String name() { + return "regex"; + } + + @Override + public ImmutableSet versions() { + return ImmutableSet.of(version0); + } + }; + + static CelExtensionLibrary library() { + return LIBRARY; + } + + private final ImmutableSet functions; + + CelRegexExtensions() { + this.functions = ImmutableSet.copyOf(Function.values()); + } + + CelRegexExtensions(Set functions) { + this.functions = ImmutableSet.copyOf(functions); + } + + @Override + public int version() { + return 0; + } + + @Override + public ImmutableSet functions() { + return functions.stream().map(f -> f.functionDecl).collect(toImmutableSet()); + } + + @Override + public void setCheckerOptions(CelCheckerBuilder checkerBuilder) { + functions.forEach(function -> checkerBuilder.addFunctionDeclarations(function.functionDecl)); + } + + @Override + public void setRuntimeOptions(CelRuntimeBuilder runtimeBuilder) { + functions.forEach(function -> runtimeBuilder.addFunctionBindings(function.functionBindings)); + } + + private static Pattern compileRegexPattern(String regex) { + try { + return Pattern.compile(regex); + } catch (PatternSyntaxException e) { + throw new IllegalArgumentException("Failed to compile regex: " + regex, e); + } + } + + private static String replace(String target, String regex, String replaceStr) { + return replaceN(target, regex, replaceStr, -1); + } + + private static String replaceN( + String target, String regex, String replaceStr, long replaceCount) { + if (replaceCount == 0) { + return target; + } + // For all negative replaceCount, do a replaceAll + if (replaceCount < 0) { + replaceCount = -1; + } + + Pattern pattern = compileRegexPattern(regex); + Matcher matcher = pattern.matcher(target); + StringBuffer sb = new StringBuffer(); + int counter = 0; + + while (matcher.find()) { + if (replaceCount != -1 && counter >= replaceCount) { + break; + } + + String processedReplacement = replaceStrValidator(matcher, replaceStr); + matcher.appendReplacement(sb, Matcher.quoteReplacement(processedReplacement)); + counter++; + } + matcher.appendTail(sb); + + return sb.toString(); + } + + private static String replaceStrValidator(Matcher matcher, String replacement) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < replacement.length(); i++) { + char c = replacement.charAt(i); + + if (c != '\\') { + sb.append(c); + continue; + } + + if (i + 1 >= replacement.length()) { + throw new IllegalArgumentException("Invalid replacement string: \\ not allowed at end"); + } + + char nextChar = replacement.charAt(++i); + + if (Character.isDigit(nextChar)) { + int groupNum = Character.digit(nextChar, 10); + int groupCount = matcher.groupCount(); + + if (groupNum > groupCount) { + throw new IllegalArgumentException( + "Replacement string references group " + + groupNum + + " but regex has only " + + groupCount + + " group(s)"); + } + + String groupValue = matcher.group(groupNum); + if (groupValue != null) { + sb.append(groupValue); + } + } else if (nextChar == '\\') { + sb.append('\\'); + } else { + throw new IllegalArgumentException( + "Invalid replacement string: \\ must be followed by a digit"); + } + } + return sb.toString(); + } + + private static Optional extract(String target, String regex) { + Pattern pattern = compileRegexPattern(regex); + Matcher matcher = pattern.matcher(target); + + if (!matcher.find()) { + return Optional.empty(); + } + + int groupCount = matcher.groupCount(); + if (groupCount > 1) { + throw new IllegalArgumentException( + "Regular expression has more than one capturing group: " + regex); + } + + String result = (groupCount == 1) ? matcher.group(1) : matcher.group(0); + + return Optional.ofNullable(result); + } + + private static ImmutableList extractAll(String target, String regex) { + Pattern pattern = compileRegexPattern(regex); + Matcher matcher = pattern.matcher(target); + + if (matcher.groupCount() > 1) { + throw new IllegalArgumentException( + "Regular expression has more than one capturing group: " + regex); + } + + ImmutableList.Builder builder = ImmutableList.builder(); + boolean hasOneGroup = matcher.groupCount() == 1; + + while (matcher.find()) { + if (hasOneGroup) { + String group = matcher.group(1); + // Add the captured group's content only if it's not null + if (group != null) { + builder.add(group); + } + } else { + // No capturing groups + builder.add(matcher.group(0)); + } + } + + return builder.build(); + } +} diff --git a/extensions/src/main/java/dev/cel/extensions/CelSetsExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelSetsExtensions.java new file mode 100644 index 000000000..324528b05 --- /dev/null +++ b/extensions/src/main/java/dev/cel/extensions/CelSetsExtensions.java @@ -0,0 +1,151 @@ +// 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. + +package dev.cel.extensions; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.errorprone.annotations.Immutable; +import dev.cel.checker.CelCheckerBuilder; +import dev.cel.common.CelFunctionDecl; +import dev.cel.common.CelOptions; +import dev.cel.common.CelOverloadDecl; +import dev.cel.common.internal.DefaultMessageFactory; +import dev.cel.common.internal.DynamicProto; +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.runtime.CelRuntimeBuilder; +import dev.cel.runtime.CelRuntimeLibrary; +import dev.cel.runtime.ProtoMessageRuntimeEquality; +import java.util.Set; + +/** + * Internal implementation of CEL Set extensions. + * + *

TODO: https://github.com/google/cel-go/blob/master/ext/sets.go#L127 + * + *

Invoking in operator will result in O(n) complexity. We need to wire in the CEL optimizers to + * rewrite the AST into a map to achieve a O(1) lookup. + */ +@Immutable +public final class CelSetsExtensions + implements CelCompilerLibrary, CelRuntimeLibrary, CelExtensionLibrary.FeatureSet { + + private static final String SET_CONTAINS_OVERLOAD_DOC = + "Returns whether the first list argument contains all elements in the second list" + + " argument. The list may contain elements of any type and standard CEL" + + " equality is used to determine whether a value exists in both lists. If the" + + " second list is empty, the result will always return true."; + private static final String SET_EQUIVALENT_OVERLOAD_DOC = + "Returns whether the first and second list are set equivalent. Lists are set equivalent if" + + " for every item in the first list, there is an element in the second which is equal." + + " The lists may not be of the same size as they do not guarantee the elements within" + + " them are unique, so size does not factor into the computation."; + private static final String SET_INTERSECTS_OVERLOAD_DOC = + "Returns whether the first and second list intersect. Lists intersect if there is at least" + + " one element in the first list which is equal to an element in the second list. The" + + " lists may not be of the same size as they do not guarantee the elements within them" + + " are unique, so size does not factor into the computation. If either list is empty," + + " the result will be false."; + + private static final ImmutableMap FUNCTION_DECL_MAP = + ImmutableMap.of( + SetsFunction.CONTAINS, + CelFunctionDecl.newFunctionDeclaration( + SetsFunction.CONTAINS.getFunction(), + CelOverloadDecl.newGlobalOverload( + "list_sets_contains_list", + SET_CONTAINS_OVERLOAD_DOC, + SimpleType.BOOL, + ListType.create(TypeParamType.create("T")), + ListType.create(TypeParamType.create("T")))), + SetsFunction.EQUIVALENT, + CelFunctionDecl.newFunctionDeclaration( + SetsFunction.EQUIVALENT.getFunction(), + CelOverloadDecl.newGlobalOverload( + "list_sets_equivalent_list", + SET_EQUIVALENT_OVERLOAD_DOC, + SimpleType.BOOL, + ListType.create(TypeParamType.create("T")), + ListType.create(TypeParamType.create("T")))), + SetsFunction.INTERSECTS, + CelFunctionDecl.newFunctionDeclaration( + SetsFunction.INTERSECTS.getFunction(), + CelOverloadDecl.newGlobalOverload( + "list_sets_intersects_list", + SET_INTERSECTS_OVERLOAD_DOC, + SimpleType.BOOL, + ListType.create(TypeParamType.create("T")), + ListType.create(TypeParamType.create("T"))))); + + private static final class Library implements CelExtensionLibrary { + private final CelSetsExtensions version0; + + Library(CelOptions celOptions) { + version0 = new CelSetsExtensions(celOptions); + } + + @Override + public String name() { + return "sets"; + } + + @Override + public ImmutableSet versions() { + return ImmutableSet.of(version0); + } + } + + static CelExtensionLibrary library(CelOptions options) { + return new Library(options); + } + + private final ImmutableSet functions; + private final SetsExtensionsRuntimeImpl setsExtensionsRuntime; + + CelSetsExtensions(CelOptions celOptions) { + this(celOptions, ImmutableSet.copyOf(SetsFunction.values())); + } + + CelSetsExtensions(CelOptions celOptions, Set functions) { + this.functions = ImmutableSet.copyOf(functions); + ProtoMessageRuntimeEquality runtimeEquality = + ProtoMessageRuntimeEquality.create( + DynamicProto.create(DefaultMessageFactory.INSTANCE), celOptions); + this.setsExtensionsRuntime = new SetsExtensionsRuntimeImpl(runtimeEquality, functions); + } + + @Override + public int version() { + return 0; + } + + @Override + public ImmutableSet functions() { + return ImmutableSet.copyOf(FUNCTION_DECL_MAP.values()); + } + + @Override + public void setCheckerOptions(CelCheckerBuilder checkerBuilder) { + functions.forEach( + function -> checkerBuilder.addFunctionDeclarations(FUNCTION_DECL_MAP.get(function))); + } + + @Override + public void setRuntimeOptions(CelRuntimeBuilder runtimeBuilder) { + runtimeBuilder.addFunctionBindings(setsExtensionsRuntime.newFunctionBindings()); + } +} diff --git a/extensions/src/main/java/dev/cel/extensions/CelStringExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelStringExtensions.java index 69fbad8d1..2bb477b82 100644 --- a/extensions/src/main/java/dev/cel/extensions/CelStringExtensions.java +++ b/extensions/src/main/java/dev/cel/extensions/CelStringExtensions.java @@ -14,6 +14,7 @@ package dev.cel.extensions; +import static com.google.common.collect.ImmutableSet.toImmutableSet; import static java.lang.Math.max; import static java.lang.Math.min; @@ -22,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; @@ -32,16 +32,17 @@ import dev.cel.common.types.SimpleType; import dev.cel.compiler.CelCompilerLibrary; import dev.cel.runtime.CelEvaluationException; -import dev.cel.runtime.CelRuntime; +import dev.cel.runtime.CelEvaluationExceptionBuilder; +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; /** Internal implementation of CEL string extensions. */ @Immutable -public final class CelStringExtensions implements CelCompilerLibrary, CelRuntimeLibrary { +public final class CelStringExtensions + implements CelCompilerLibrary, CelRuntimeLibrary, CelExtensionLibrary.FeatureSet { /** Denotes the string extension function */ @SuppressWarnings({"unchecked"}) // Unchecked: Type-checker guarantees casting safety. @@ -55,7 +56,7 @@ public enum Function { + " greater than the length of the string, the function will produce an error.", SimpleType.STRING, ImmutableList.of(SimpleType.STRING, SimpleType.INT))), - CelRuntime.CelFunctionBinding.from( + CelFunctionBinding.from( "string_char_at_int", String.class, Long.class, CelStringExtensions::charAt)), INDEX_OF( CelFunctionDecl.newFunctionDeclaration( @@ -74,9 +75,9 @@ public enum Function { + " is returned (zero or custom).", SimpleType.INT, ImmutableList.of(SimpleType.STRING, SimpleType.STRING, SimpleType.INT))), - CelRuntime.CelFunctionBinding.from( + CelFunctionBinding.from( "string_index_of_string", String.class, String.class, CelStringExtensions::indexOf), - CelRuntime.CelFunctionBinding.from( + CelFunctionBinding.from( "string_index_of_string_int", ImmutableList.of(String.class, String.class, Long.class), CelStringExtensions::indexOf)), @@ -94,8 +95,8 @@ public enum Function { + " separator.", SimpleType.STRING, ImmutableList.of(ListType.create(SimpleType.STRING), SimpleType.STRING))), - CelRuntime.CelFunctionBinding.from("list_join", List.class, CelStringExtensions::join), - CelRuntime.CelFunctionBinding.from( + CelFunctionBinding.from("list_join", List.class, CelStringExtensions::join), + CelFunctionBinding.from( "list_join_string", List.class, String.class, CelStringExtensions::join)), LAST_INDEX_OF( CelFunctionDecl.newFunctionDeclaration( @@ -114,12 +115,12 @@ public enum Function { + " returned (string length or custom).", SimpleType.INT, ImmutableList.of(SimpleType.STRING, SimpleType.STRING, SimpleType.INT))), - CelRuntime.CelFunctionBinding.from( + CelFunctionBinding.from( "string_last_index_of_string", String.class, String.class, CelStringExtensions::lastIndexOf), - CelRuntime.CelFunctionBinding.from( + CelFunctionBinding.from( "string_last_index_of_string_int", ImmutableList.of(String.class, String.class, Long.class), CelStringExtensions::lastIndexOf)), @@ -133,7 +134,18 @@ public enum Function { + " range.", SimpleType.STRING, SimpleType.STRING)), - CelRuntime.CelFunctionBinding.from("string_lower_ascii", String.class, Ascii::toLowerCase)), + 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", @@ -153,14 +165,24 @@ public enum Function { SimpleType.STRING, ImmutableList.of( SimpleType.STRING, SimpleType.STRING, SimpleType.STRING, SimpleType.INT))), - CelRuntime.CelFunctionBinding.from( + CelFunctionBinding.from( "string_replace_string_string", ImmutableList.of(String.class, String.class, String.class), CelStringExtensions::replaceAll), - CelRuntime.CelFunctionBinding.from( + CelFunctionBinding.from( "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", @@ -175,9 +197,9 @@ public enum Function { + " the specified limit on the number of substrings produced by the split.", ListType.create(SimpleType.STRING), ImmutableList.of(SimpleType.STRING, SimpleType.STRING, SimpleType.INT))), - CelRuntime.CelFunctionBinding.from( + CelFunctionBinding.from( "string_split_string", String.class, String.class, CelStringExtensions::split), - CelRuntime.CelFunctionBinding.from( + CelFunctionBinding.from( "string_split_string_int", ImmutableList.of(String.class, String.class, Long.class), CelStringExtensions::split)), @@ -197,9 +219,9 @@ public enum Function { + " Thus the length of the substring is {@code endIndex-beginIndex}.", SimpleType.STRING, ImmutableList.of(SimpleType.STRING, SimpleType.INT, SimpleType.INT))), - CelRuntime.CelFunctionBinding.from( + CelFunctionBinding.from( "string_substring_int", String.class, Long.class, CelStringExtensions::substring), - CelRuntime.CelFunctionBinding.from( + CelFunctionBinding.from( "string_substring_int_int", ImmutableList.of(String.class, Long.class, Long.class), CelStringExtensions::substring)), @@ -213,7 +235,7 @@ public enum Function { + " which does not include the zero-width spaces. ", SimpleType.STRING, SimpleType.STRING)), - CelRuntime.CelFunctionBinding.from("string_trim", String.class, CelStringExtensions::trim)), + CelFunctionBinding.from("string_trim", String.class, CelStringExtensions::trim)), UPPER_ASCII( CelFunctionDecl.newFunctionDeclaration( "upperAscii", @@ -224,14 +246,19 @@ public enum Function { + " range.", SimpleType.STRING, SimpleType.STRING)), - CelRuntime.CelFunctionBinding.from("string_upper_ascii", String.class, Ascii::toUpperCase)); + CelFunctionBinding.from("string_upper_ascii", String.class, Ascii::toUpperCase)); private final CelFunctionDecl functionDecl; - private final ImmutableSet functionBindings; + private final ImmutableSet functionBindings; - Function(CelFunctionDecl functionDecl, CelRuntime.CelFunctionBinding... functionBindings) { + String getFunction() { + return functionDecl.name(); + } + + Function(CelFunctionDecl functionDecl, CelFunctionBinding... functionBindings) { this.functionDecl = functionDecl; - this.functionBindings = ImmutableSet.copyOf(functionBindings); + this.functionBindings = + CelFunctionBinding.fromOverloads(functionDecl.name(), functionBindings); } } @@ -245,6 +272,35 @@ public enum Function { this.functions = ImmutableSet.copyOf(functions); } + private static final CelExtensionLibrary LIBRARY = + new CelExtensionLibrary() { + private final CelStringExtensions version0 = new CelStringExtensions(); + + @Override + public String name() { + return "strings"; + } + + @Override + public ImmutableSet versions() { + return ImmutableSet.of(version0); + } + }; + + static CelExtensionLibrary library() { + return LIBRARY; + } + + @Override + public int version() { + return 0; + } + + @Override + public ImmutableSet functions() { + return functions.stream().map(f -> f.functionDecl).collect(toImmutableSet()); + } + @Override public void setCheckerOptions(CelCheckerBuilder checkerBuilder) { functions.forEach(function -> checkerBuilder.addFunctionDeclarations(function.functionDecl)); @@ -260,8 +316,10 @@ private static String charAt(String s, long i) throws CelEvaluationException { try { index = Math.toIntExact(i); } catch (ArithmeticException e) { - throw new CelEvaluationException( - String.format("charAt failure: Index must not exceed the int32 range: %d", i), e); + throw CelEvaluationExceptionBuilder.newBuilder( + "charAt failure: Index must not exceed the int32 range: %d", i) + .setCause(e) + .build(); } CelCodePointArray codePointArray = CelCodePointArray.fromString(s); @@ -269,8 +327,9 @@ private static String charAt(String s, long i) throws CelEvaluationException { return ""; } if (index < 0 || index > codePointArray.length()) { - throw new CelEvaluationException( - String.format("charAt failure: Index out of range: %d", index)); + throw CelEvaluationExceptionBuilder.newBuilder( + "charAt failure: Index out of range: %d", index) + .build(); } return codePointArray.slice(index, index + 1).toString(); @@ -292,10 +351,10 @@ private static Long indexOf(Object[] args) throws CelEvaluationException { try { offset = Math.toIntExact(offsetInLong); } catch (ArithmeticException e) { - throw new CelEvaluationException( - String.format( - "indexOf failure: Offset must not exceed the int32 range: %d", offsetInLong), - e); + throw CelEvaluationExceptionBuilder.newBuilder( + "indexOf failure: Offset must not exceed the int32 range: %d", offsetInLong) + .setCause(e) + .build(); } return indexOf(str, substr, offset); @@ -310,8 +369,9 @@ private static Long indexOf(String str, String substr, int offset) throws CelEva CelCodePointArray substrCpa = CelCodePointArray.fromString(substr); if (offset < 0 || offset >= strCpa.length()) { - throw new CelEvaluationException( - String.format("indexOf failure: Offset out of range: %d", offset)); + throw CelEvaluationExceptionBuilder.newBuilder( + "indexOf failure: Offset out of range: %d", offset) + .build(); } return safeIndexOf(strCpa, substrCpa, offset); @@ -351,6 +411,10 @@ private static Long lastIndexOf(String str, String substr) throws CelEvaluationE return (long) strCpa.length(); } + if (strCpa.length() < substrCpa.length()) { + return -1L; + } + return lastIndexOf(strCpa, substrCpa, (long) strCpa.length() - 1); } @@ -372,14 +436,16 @@ private static Long lastIndexOf(CelCodePointArray str, CelCodePointArray substr, try { off = Math.toIntExact(offset); } catch (ArithmeticException e) { - throw new CelEvaluationException( - String.format("lastIndexOf failure: Offset must not exceed the int32 range: %d", offset), - e); + throw CelEvaluationExceptionBuilder.newBuilder( + "lastIndexOf failure: Offset must not exceed the int32 range: %d", offset) + .setCause(e) + .build(); } if (off < 0 || off >= str.length()) { - throw new CelEvaluationException( - String.format("lastIndexOf failure: Offset out of range: %d", offset)); + throw CelEvaluationExceptionBuilder.newBuilder( + "lastIndexOf failure: Offset out of range: %d", offset) + .build(); } if (off > str.length() - substr.length()) { @@ -402,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); } @@ -412,9 +536,10 @@ private static String replace(Object[] objects) throws CelEvaluationException { try { index = Math.toIntExact(indexInLong); } catch (ArithmeticException e) { - throw new CelEvaluationException( - String.format("replace failure: Index must not exceed the int32 range: %d", indexInLong), - e); + throw CelEvaluationExceptionBuilder.newBuilder( + "replace failure: Index must not exceed the int32 range: %d", indexInLong) + .setCause(e) + .build(); } return replace((String) objects[0], (String) objects[1], (String) objects[2], index); @@ -456,37 +581,40 @@ 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 { limit = Math.toIntExact(limitInLong); } catch (ArithmeticException e) { - throw new CelEvaluationException( - String.format("split failure: Limit must not exceed the int32 range: %d", limitInLong), - e); + throw CelEvaluationExceptionBuilder.newBuilder( + "split failure: Limit must not exceed the int32 range: %d", limitInLong) + .setCause(e) + .build(); } 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) { @@ -498,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); } /** @@ -511,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; @@ -524,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 { @@ -532,18 +660,20 @@ private static Object substring(String s, long i) throws CelEvaluationException try { beginIndex = Math.toIntExact(i); } catch (ArithmeticException e) { - throw new CelEvaluationException( - String.format("substring failure: Index must not exceed the int32 range: %d", i), e); + throw CelEvaluationExceptionBuilder.newBuilder( + "substring failure: Index must not exceed the int32 range: %d", i) + .setCause(e) + .build(); } CelCodePointArray codePointArray = CelCodePointArray.fromString(s); boolean indexIsInRange = beginIndex <= codePointArray.length() && beginIndex >= 0; if (!indexIsInRange) { - throw new CelEvaluationException( - String.format( + throw CelEvaluationExceptionBuilder.newBuilder( "substring failure: Range [%d, %d) out of bounds", - beginIndex, codePointArray.length())); + beginIndex, codePointArray.length()) + .build(); } if (beginIndex == codePointArray.length()) { @@ -565,11 +695,11 @@ private static String substring(Object[] args) throws CelEvaluationException { beginIndex = Math.toIntExact(beginIndexInLong); endIndex = Math.toIntExact(endIndexInLong); } catch (ArithmeticException e) { - throw new CelEvaluationException( - String.format( + throw CelEvaluationExceptionBuilder.newBuilder( "substring failure: Indices must not exceed the int32 range: [%d, %d)", - beginIndexInLong, endIndexInLong), - e); + beginIndexInLong, endIndexInLong) + .setCause(e) + .build(); } String s = (String) args[0]; @@ -581,8 +711,9 @@ private static String substring(Object[] args) throws CelEvaluationException { && beginIndex <= codePointArray.length() && endIndex <= codePointArray.length(); if (!indicesIsInRange) { - throw new CelEvaluationException( - String.format("substring failure: Range [%d, %d) out of bounds", beginIndex, endIndex)); + throw CelEvaluationExceptionBuilder.newBuilder( + "substring failure: Range [%d, %d) out of bounds", beginIndex, endIndex) + .build(); } if (beginIndex == endIndex) { diff --git a/extensions/src/main/java/dev/cel/extensions/README.md b/extensions/src/main/java/dev/cel/extensions/README.md index 473ba634b..e6ee73aba 100644 --- a/extensions/src/main/java/dev/cel/extensions/README.md +++ b/extensions/src/main/java/dev/cel/extensions/README.md @@ -96,6 +96,261 @@ Examples: math.least(a, b) // check-time error if a or b is non-numeric math.least(dyn('string')) // runtime error +### Math.BitOr + +Introduced at version: 1 + +Performs a bitwise-OR operation over two int or uint values. + + math.bitOr(, ) -> + math.bitOr(, ) -> + +Examples: + + math.bitOr(1u, 2u) // returns 3u + math.bitOr(-2, -4) // returns -2 + +### Math.BitAnd + +Introduced at version: 1 + +Performs a bitwise-AND operation over two int or uint values. + + math.bitAnd(, ) -> + math.bitAnd(, ) -> + +Examples: + + math.bitAnd(3u, 2u) // return 2u + math.bitAnd(3, 5) // returns 3 + math.bitAnd(-3, -5) // returns -7 + +### Math.BitXor + +Introduced at version: 1 + + math.bitXor(, ) -> + math.bitXor(, ) -> + +Performs a bitwise-XOR operation over two int or uint values. + +Examples: + + math.bitXor(3u, 5u) // returns 6u + math.bitXor(1, 3) // returns 2 + +### Math.BitNot + +Introduced at version: 1 + +Function which accepts a single int or uint and performs a bitwise-NOT +ones-complement of the given binary value. + + math.bitNot() -> + math.bitNot() -> + +Examples + + math.bitNot(1) // returns -1 + math.bitNot(-1) // return 0 + math.bitNot(0u) // returns 18446744073709551615u + +### Math.BitShiftLeft + +Introduced at version: 1 + +Perform a left shift of bits on the first parameter, by the amount of bits +specified in the second parameter. The first parameter is either a uint or +an int. The second parameter must be an int. + +When the second parameter is 64 or greater, 0 will be always be returned +since the number of bits shifted is greater than or equal to the total bit +length of the number being shifted. Negative valued bit shifts will result +in a runtime error. + + math.bitShiftLeft(, ) -> + math.bitShiftLeft(, ) -> + +Examples + + math.bitShiftLeft(1, 2) // returns 4 + math.bitShiftLeft(-1, 2) // returns -4 + math.bitShiftLeft(1u, 2) // return 4u + math.bitShiftLeft(1u, 200) // returns 0u + +### Math.BitShiftRight + +Introduced at version: 1 + +Perform a right shift of bits on the first parameter, by the amount of bits +specified in the second parameter. The first parameter is either a uint or +an int. The second parameter must be an int. + +When the second parameter is 64 or greater, 0 will always be returned since +the number of bits shifted is greater than or equal to the total bit length +of the number being shifted. Negative valued bit shifts will result in a +runtime error. + +The sign bit extension will not be preserved for this operation: vacant bits +on the left are filled with 0. + + math.bitShiftRight(, ) -> + math.bitShiftRight(, ) -> + +Examples + + math.bitShiftRight(1024, 2) // returns 256 + math.bitShiftRight(1024u, 2) // returns 256u + math.bitShiftRight(1024u, 64) // returns 0u + +### Math.Ceil + +Introduced at version: 1 + +Compute the ceiling of a double value. + + math.ceil() -> + +Examples: + + math.ceil(1.2) // returns 2.0 + math.ceil(-1.2) // returns -1.0 + +### Math.Floor + +Introduced at version: 1 + +Compute the floor of a double value. + + math.floor() -> + +Examples: + + math.floor(1.2) // returns 1.0 + math.floor(-1.2) // returns -2.0 + +### Math.Round + +Introduced at version: 1 + +Rounds the double value to the nearest whole number with ties rounding away +from zero, e.g. 1.5 -> 2.0, -1.5 -> -2.0. + + math.round() -> + +Examples: + + math.round(1.2) // returns 1.0 + math.round(1.5) // returns 2.0 + math.round(-1.5) // returns -2.0 + +### Math.Trunc + +Introduced at version: 1 + +Truncates the fractional portion of the double value. + + math.trunc() -> + +Examples: + + math.trunc(-1.3) // returns -1.0 + math.trunc(1.3) // returns 1.0 + +### Math.Abs + +Introduced at version: 1 + +Returns the absolute value of the numeric type provided as input. If the +value is NaN, the output is NaN. If the input is int64 min, the function +will result in an overflow error. + + math.abs() -> + math.abs() -> + math.abs() -> + +Examples: + + math.abs(-1) // returns 1 + math.abs(1) // returns 1 + math.abs(-9223372036854775808) // overlflow error + +### Math.Sign + +Introduced at version: 1 + +Returns the sign of the numeric type, either -1, 0, 1 as an int, double, or +uint depending on the overload. For floating point values, if NaN is +provided as input, the output is also NaN. The implementation does not +differentiate between positive and negative zero. + + math.sign() -> + math.sign() -> + math.sign() -> + +Examples: + + math.sign(-42) // returns -1 + math.sign(0) // returns 0 + math.sign(42) // returns 1 + +### Math.IsInf + +Introduced at version: 1 + +Returns true if the input double value is -Inf or +Inf. + + math.isInf() -> + +Examples: + + math.isInf(1.0/0.0) // returns true + math.isInf(1.2) // returns false + +### Math.IsNaN + +Introduced at version: 1 + +Returns true if the input double value is NaN, false otherwise. + + math.isNaN() -> + +Examples: + + math.isNaN(0.0/0.0) // returns true + math.isNaN(1.2) // returns false + +### Math.IsFinite + +Introduced at version: 1 + +Returns true if the value is a finite number. Equivalent in behavior to: +!math.isNaN(double) && !math.isInf(double) + + math.isFinite() -> + +Examples: + + math.isFinite(0.0/0.0) // returns false + math.isFinite(1.2) // returns true + +### Math.sqrt + +Introduced at version: 2 + +Returns the square root of the numeric type provided as input. If the value is +NaN, the output is NaN. If the input is negative, the output is NaN. + + math.sqrt() -> + math.sqrt() -> + math.sqrt() -> + +Examples: + + math.sqrt(81.0) // returns 9.0 + math.sqrt(4) // returns 2.0 + math.sqrt(-4) // returns NaN + ## Protos Extended macros and functions for proto manipulation. @@ -140,13 +395,13 @@ zero-based. Returns the character at the given position. If the position is negative, or greater than the length of the string, the function will produce an error. -.charAt() -> + .charAt() -> Examples: -'hello'.charAt(4) // return 'o' -'hello'.charAt(5) // return '' -'hello'.charAt(-1) // error + 'hello'.charAt(4) // return 'o' + 'hello'.charAt(5) // return '' + 'hello'.charAt(-1) // error ### IndexOf @@ -156,17 +411,17 @@ not found the function returns -1. The function also accepts an optional offset from which to begin the substring search. If the substring is the empty string, the index where the search starts is returned (zero or custom). -.indexOf() -> -.indexOf(, ) -> + .indexOf() -> + .indexOf(, ) -> Examples: -'hello mellow'.indexOf('') // returns 0 -'hello mellow'.indexOf('ello') // returns 1 -'hello mellow'.indexOf('jello') // returns -1 -'hello mellow'.indexOf('', 2) // returns 2 -'hello mellow'.indexOf('ello', 2) // returns 7 -'hello mellow'.indexOf('ello', 20) // error + 'hello mellow'.indexOf('') // returns 0 + 'hello mellow'.indexOf('ello') // returns 1 + 'hello mellow'.indexOf('jello') // returns -1 + 'hello mellow'.indexOf('', 2) // returns 2 + 'hello mellow'.indexOf('ello', 2) // returns 7 + 'hello mellow'.indexOf('ello', 20) // error ### Join @@ -174,15 +429,15 @@ Returns a new string where the elements of string list are concatenated. The function also accepts an optional separator which is placed between elements in the resulting string. ->.join() -> ->.join() -> + >.join() -> + >.join() -> Examples: -['hello', 'mellow'].join() // returns 'hellomellow' -['hello', 'mellow'].join(' ') // returns 'hello mellow' -[].join() // returns '' -[].join('/') // returns '' + ['hello', 'mellow'].join() // returns 'hellomellow' + ['hello', 'mellow'].join(' ') // returns 'hello mellow' + [].join() // returns '' + [].join('/') // returns '' ### LastIndexOf @@ -219,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 @@ -238,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. @@ -304,8 +586,8 @@ ASCII range. Examples: - 'TacoCat'.upperAscii() // returns 'TACOCAT' - 'TacoCÆt Xii'.upperAscii() // returns 'TACOCÆT XII' + 'TacoCat'.upperAscii() // returns 'TACOCAT' + 'TacoCÆt Xii'.upperAscii() // returns 'TACOCÆT XII' ## Encoders @@ -338,3 +620,509 @@ Example: base64.encode(b'hello') // return 'aGVsbG8=' +## Sets + +Sets provides set relationship tests. + +There is no set type within CEL, and while one may be introduced in the future, +there are cases where a `list` type is known to behave like a set. For such +cases, this library provides some basic functionality for determining set +containment, equivalence, and intersection. + +### Sets.Contains + +Returns whether the first list argument contains all elements in the second list +argument. The list may contain elements of any type and standard CEL equality is +used to determine whether a value exists in both lists. If the second list is +empty, the result will always return true. + +``` +sets.contains(list(T), list(T)) -> bool +``` + +Examples: + +``` +sets.contains([], []) // true +sets.contains([], [1]) // false +sets.contains([1, 2, 3, 4], [2, 3]) // true +sets.contains([1, 2.0, 3u], [1.0, 2u, 3]) // true +``` + +### Sets.Equivalent + +Returns whether the first and second list are set equivalent. Lists are set +equivalent if for every item in the first list, there is an element in the +second which is equal. The lists may not be of the same size as they do not +guarantee the elements within them are unique, so size does not factor into the +computation. + +``` +sets.equivalent(list(T), list(T)) -> bool +``` + +Examples: + +``` +sets.equivalent([], []) // true +sets.equivalent([1], [1, 1]) // true +sets.equivalent([1], [1u, 1.0]) // true +sets.equivalent([1, 2, 3], [3u, 2.0, 1]) // true +``` + +### Sets.Intersects + +Returns whether the first list has at least one element whose value is equal to +an element in the second list. If either list is empty, the result will be +false. + +``` +sets.intersects(list(T), list(T)) -> bool +``` + +Examples: + +``` +sets.intersects([1], []) // false +sets.intersects([1], [1, 2]) // true +sets.intersects([[1], [2, 3]], [[1, 2], [2, 3.0]]) // true +``` + +## Lists + +Extended functions for list manipulation. As a general note, all indices are +zero-based. + +### Slice + +Returns a new sub-list using the indexes provided. The `from` index is +inclusive, the `to` index is exclusive. + + .slice(, ) -> + +Examples: + + [1,2,3,4].slice(1, 3) // return [2, 3] + [1,2,3,4].slice(2, 4) // return [3, 4] + +### Flatten + +Introduced at version: 1 + +Flattens a list by one level, or to the specified level. Providing a negative level will error. + +Examples: + +``` +// Single-level flatten: + +[].flatten() // [] +[1,[2,3],[4]].flatten() // [1, 2, 3, 4] +[1,[2,[3,4]]].flatten() // [1, 2, [3, 4]] +[1,2,[],[],[3,4]].flatten() // [1, 2, 3, 4] + +// Recursive flatten +[1,[2,[3,[4]]]].flatten(2) // return [1, 2, 3, [4]] +[1,[2,[3,[4]]]].flatten(3) // return [1, 2, 3, 4] + +// Error +[1,[2,[3,[4]]]].flatten(-1) +``` + +Note that due to the current limitations of type-checker, a compilation error +will occur if an already flat list is populated to the argument-less flatten +function. + +For time being, you must explicitly provide 1 as the depth level, or wrap the +list in dyn if you anticipate having to deal with a flat list: + +``` +[1,2,3].flatten() // error + +// But the following will work: +[1,2,3].flatten(1) // [1,2,3] +dyn([1,2,3]).flatten() // [1,2,3] +``` + +This will be addressed once we add the appropriate capabilities in the +type-checker to handle type-reductions, or union types. + +### Range + +Introduced at version: 2 + +Given integer size n returns a list of integers from 0 to n-1. If size <= 0 +then return empty list. + +``` +lists.range(int) -> list(int) +``` + +Examples: + +``` +lists.range(5) -> [0, 1, 2, 3, 4] +lists.range(0) -> [] +``` + +### Distinct + +Introduced at version: 2 + +Returns the distinct elements of a list. + + .distinct() -> + +Examples: + + [1, 2, 2, 3, 3, 3].distinct() // return [1, 2, 3] + ["b", "b", "c", "a", "c"].distinct() // return ["b", "c", "a"] + [1, "b", 2, "b"].distinct() // return [1, "b", 2] + [1, 1.0, 2, 2u].distinct() // return [1, 2] + +### Reverse + +Introduced in version 2 + +Returns the elements of a list in reverse order. + + .reverse() -> + +Examples: + + [5, 3, 1, 2].reverse() // return [2, 1, 3, 5] + +### Sort + +Introduced in version 2 + +Sorts a list with comparable elements. If the element type is not comparable +or the element types are not the same, the function will produce an error. + + .sort() -> + // T in {int, uint, double, bool, duration, timestamp, string, bytes} + +Examples: + + [3, 2, 1].sort() // return [1, 2, 3] + ["b", "c", "a"].sort() // return ["a", "b", "c"] + [1, "b"].sort() // error + [[1, 2, 3]].sort() // error + +### SortBy + +Introduced in version 2 + +Sorts a list by a key value, i.e., the order is determined by the result of +an expression applied to each element of the list. + + .sortBy(, ) -> + keyExpr returns a value in {int, uint, double, bool, duration, timestamp, string, bytes} + +Examples: + + [ + Player { name: "foo", score: 0 }, + Player { name: "bar", score: -10 }, + Player { name: "baz", score: 1000 }, + ].sortBy(e, e.score).map(e, e.name) + == ["bar", "foo", "baz"] + +### Last + +Introduced in the 'optional' extension version 2 + +Returns an optional with the last value from the list or `optional.None` if the +list is empty. + + .last() -> + +Examples: + + [1, 2, 3].last().value() == 3 + [].last().orValue('test') == 'test' + +This is syntactic sugar for list[list.size()-1]. + +### First + +Introduced in the 'optional' extension version 2 + +Returns an optional with the first value from the list or `optional.None` if the +list is empty. + + .first() -> + +Examples: + + [1, 2, 3].first().value() == 1 + [].first().orValue('test') == 'test' + +## Regex + +Regex introduces support for regular expressions in CEL. + +This library provides functions for capturing groups, replacing strings using +regex patterns, Regex configures namespaced regex helper functions. Note, all +functions use the 'regex' namespace. If you are currently using a variable named +'regex', the macro will likely work just as intended; however, there is some +chance for collision. + +### Replace + +The `regex.replace` function replaces all non-overlapping substring of a regex +pattern in the target string with a replacement string. Optionally, you can +limit the number of replacements by providing a count argument. When the count +is a negative number, the function acts as replace all. Only numeric (\N) +capture group references are supported in the replacement string, with +validation for correctness. Backslashed-escaped digits (\1 to \9) within the +replacement argument can be used to insert text matching the corresponding +parenthesized group in the regexp pattern. An error will be thrown for invalid +regex or replace string. + +``` +regex.replace(target: string, pattern: string, replacement: string) -> string +regex.replace(target: string, pattern: string, replacement: string, count: int) -> string +``` + +Examples: + +``` +regex.replace('hello world hello', 'hello', 'hi') == 'hi world hi' +regex.replace('banana', 'a', 'x', 0) == 'banana' +regex.replace('banana', 'a', 'x', 1) == 'bxnana' +regex.replace('banana', 'a', 'x', 2) == 'bxnxna' +regex.replace('banana', 'a', 'x', -12) == 'bxnxnx' +regex.replace('foo bar', '(fo)o (ba)r', '\\2 \\1') == 'ba fo' + +regex.replace('test', '(.)', '$2') \\ Runtime Error invalid replace string +regex.replace('foo bar', '(', '$2 $1') \\ Runtime Error invalid regex string +regex.replace('id=123', 'id=(?P\\\\d+)', 'value: \\values') \\ Runtime Error invalid replace string + +``` + +### Extract + +The `regex.extract` function returns the first match of a regex pattern in a +string. If no match is found, it returns an optional none value. An error will +be thrown for invalid regex or for multiple capture groups. + +``` +regex.extract(target: string, pattern: string) -> optional +``` + +Examples: + +``` +regex.extract('hello world', 'hello(.*)') == optional.of(' world') +regex.extract('item-A, item-B', 'item-(\\w+)') == optional.of('A') +regex.extract('HELLO', 'hello') == optional.empty() + +regex.extract('testuser@testdomain', '(.*)@([^.]*)')) \\ Runtime Error multiple extract group +``` + +### Extract All + +The `regex.extractAll` function returns a list of all matches of a regex +pattern in a target string. If no matches are found, it returns an empty list. +An error will be thrown for invalid regex or for multiple capture groups. + +``` +regex.extractAll(target: string, pattern: string) -> list +``` + +Examples: + +``` +regex.extractAll('id:123, id:456', 'id:\\d+') == ['id:123', 'id:456'] +regex.extractAll('id:123, id:456', 'assa') == [] + +regex.extractAll('testuser@testdomain', '(.*)@([^.]*)') \\ Runtime Error multiple capture group +``` + +## Comprehensions + +TwoVarComprehensions introduces support for two-variable comprehensions. + +The two-variable form of comprehensions looks similar to the one-variable +counterparts. Where possible, the same macro names were used and additional +macro signatures added. The notable distinction for two-variable comprehensions +is the introduction of `transformList`, `transformMap`, and `transformMapEntry` +support for list and map types rather than the more traditional `map` and +`filter` macros. + +### All + +Comprehension which tests whether all elements in the list or map satisfy a +given predicate. The `all` macro evaluates in a manner consistent with logical +AND and will short-circuit when encountering a `false` value. + + .all(indexVar, valueVar, ) -> bool + .all(keyVar, valueVar, ) -> bool + +Examples: + + [1, 2, 3].all(i, j, i < j) // returns true + {'hello': 'world', 'taco': 'taco'}.all(k, v, k != v) // returns false + + // Combines two-variable comprehension with single variable + {'h': ['hello', 'hi'], 'j': ['joke', 'jog']} + .all(k, vals, vals.all(v, v.startsWith(k))) // returns true + +### Exists + +Comprehension which tests whether any element in a list or map exists which +satisfies a given predicate. The `exists` macro evaluates in a manner consistent +with logical OR and will short-circuit when encountering a `true` value. + + .exists(indexVar, valueVar, ) -> bool + .exists(keyVar, valueVar, ) -> bool + +Examples: + + {'greeting': 'hello', 'farewell': 'goodbye'} + .exists(k, v, k.startsWith('good') || v.endsWith('bye')) // returns true + [1, 2, 4, 8, 16].exists(i, v, v == 1024 && i == 10) // returns false + +### Exists_One + +Comprehension which tests whether exactly one element in a list or map exists +which satisfies a given predicate expression. The `exists_one` macro +comprehension does not short-circuit in keeping with the one-variable semantics. + + .existsOne(indexVar, valueVar, ) + .existsOne(keyVar, valueVar, ) + +Examples: + + [1, 2, 1, 3, 1, 4].existsOne(i, v, i == 1 || v == 1) // returns false + [1, 1, 2, 2, 3, 3].existsOne(i, v, i == 2 && v == 2) // returns true + {'i': 0, 'j': 1, 'k': 2}.existsOne(i, v, i == 'l' || v == 1) // returns true + +### TransformList + +Comprehension which converts a map or a list into a list value. The output +expression of the comprehension determines the contents of the output list. +Elements in the list may optionally be filtered according to a predicate +expression, where elements that satisfy the predicate are transformed. + + .transformList(indexVar, valueVar, ) + .transformList(indexVar, valueVar, , ) + .transformList(keyVar, valueVar, ) + .transformList(keyVar, valueVar, , ) + +Examples: + + [1, 2, 3].transformList(indexVar, valueVar, + (indexVar * valueVar) + valueVar) // returns [1, 4, 9] + [1, 2, 3].transformList(indexVar, valueVar, indexVar % 2 == 0 + (indexVar * valueVar) + valueVar) // returns [1, 9] + {'greeting': 'hello', 'farewell': 'goodbye'} + .transformList(k, _, k) // returns ['greeting', 'farewell'] + {'greeting': 'hello', 'farewell': 'goodbye'} + .transformList(_, v, v) // returns ['hello', 'goodbye'] + +### TransformMap + +Comprehension which converts a map or a list into a map value. The output +expression of the comprehension determines the value of the output map entry; +however, the key remains fixed. Elements in the map may optionally be filtered +according to a predicate expression, where elements that satisfy the predicate +are transformed. + + .transformMap(indexVar, valueVar, ) + .transformMap(indexVar, valueVar, , ) + .transformMap(keyVar, valueVar, ) + .transformMap(keyVar, valueVar, , ) + +Examples: + + [1, 2, 3].transformMap(indexVar, valueVar, + (indexVar * valueVar) + valueVar) // returns {0: 1, 1: 4, 2: 9} + [1, 2, 3].transformMap(indexVar, valueVar, indexVar % 2 == 0 + (indexVar * valueVar) + valueVar) // returns {0: 1, 2: 9} + {'greeting': 'hi'}.transformMap(k, v, v + '!') // returns {'greeting': 'hi!'} + +### TransformMapEntry + +Comprehension which converts a map or a list into a map value; however, this +transform expects the entry expression be a map literal. If the transform +produces an entry which duplicates a key in the target map, the comprehension +will error. Note, that key equality is determined using CEL equality which +asserts that numeric values which are equal, even if they don't have the same +type will cause a key collision. + +Elements in the map may optionally be filtered according to a predicate +expression, where elements that satisfy the predicate are transformed. + + .transformMapEntry(indexVar, valueVar, ) + .transformMapEntry(indexVar, valueVar, , ) + .transformMapEntry(keyVar, valueVar, ) + .transformMapEntry(keyVar, valueVar, , ) + +Examples: + + {'greeting': 'hello'}.transformMapEntry(keyVar, valueVar, + {valueVar: keyVar}) // returns {'hello': 'greeting'} + // reverse lookup, require all values in list be unique + [1, 2, 3].transformMapEntry(indexVar, valueVar, + {valueVar: indexVar}) // returns {1:0, 2:1, 3:2} + + {'greeting': 'aloha', 'farewell': 'aloha'} + .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 new file mode 100644 index 000000000..a02fdba8a --- /dev/null +++ b/extensions/src/main/java/dev/cel/extensions/SetsExtensionsRuntimeImpl.java @@ -0,0 +1,147 @@ +// 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.extensions; + +import com.google.common.collect.ImmutableSet; +import com.google.errorprone.annotations.Immutable; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.CelLiteRuntimeBuilder; +import dev.cel.runtime.CelLiteRuntimeLibrary; +import dev.cel.runtime.RuntimeEquality; +import java.util.Collection; +import java.util.Iterator; +import java.util.Set; + +@Immutable +final class SetsExtensionsRuntimeImpl implements CelLiteRuntimeLibrary { + private final RuntimeEquality runtimeEquality; + + private final ImmutableSet functions; + + SetsExtensionsRuntimeImpl(RuntimeEquality runtimeEquality, Set functions) { + this.runtimeEquality = runtimeEquality; + this.functions = ImmutableSet.copyOf(functions); + } + + @Override + public void setRuntimeOptions(CelLiteRuntimeBuilder runtimeBuilder) { + runtimeBuilder.addFunctionBindings(newFunctionBindings()); + } + + ImmutableSet newFunctionBindings() { + ImmutableSet.Builder bindingBuilder = ImmutableSet.builder(); + for (SetsFunction function : functions) { + switch (function) { + case CONTAINS: + bindingBuilder.addAll( + CelFunctionBinding.fromOverloads( + function.getFunction(), + CelFunctionBinding.from( + "list_sets_contains_list", + Collection.class, + Collection.class, + this::containsAll))); + break; + case EQUIVALENT: + 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.addAll( + CelFunctionBinding.fromOverloads( + function.getFunction(), + CelFunctionBinding.from( + "list_sets_intersects_list", + Collection.class, + Collection.class, + this::setIntersects))); + break; + } + } + + return bindingBuilder.build(); + } + + /** + * This implementation iterates over the specified collection, checking each element returned by + * the iterator in turn to see if it's contained in this collection. If all elements are so + * contained true is returned, otherwise false. + * + *

This is picked verbatim as implemented in the Java standard library + * Collections.containsAll() method. + * + * @see #contains(Object, Collection) + */ + private boolean containsAll(Collection list, Collection subList) { + for (Object e : subList) { + if (!contains(e, list)) { + return false; + } + } + return true; + } + + /** + * This implementation iterates over the elements in the collection, checking each element in turn + * for equality with the specified element. + * + *

This is picked verbatim as implemented in the Java standard library Collections.contains() + * method. + * + *

Source: OpenJDK + * AbstractCollection + */ + private boolean contains(Object o, Collection list) { + Iterator it = list.iterator(); + if (o == null) { + while (it.hasNext()) { + if (it.next() == null) { + return true; + } + } + } else { + while (it.hasNext()) { + Object item = it.next(); + if (objectsEquals(item, o)) { + return true; + } + } + } + return false; + } + + private boolean objectsEquals(Object o1, Object o2) { + return runtimeEquality.objectEquals(o1, o2); + } + + private boolean setIntersects(Collection listA, Collection listB) { + if (listA.isEmpty() || listB.isEmpty()) { + return false; + } + for (Object element : listB) { + if (contains(element, listA)) { + return true; + } + } + return false; + } +} diff --git a/extensions/src/main/java/dev/cel/extensions/SetsFunction.java b/extensions/src/main/java/dev/cel/extensions/SetsFunction.java new file mode 100644 index 000000000..ad67f861d --- /dev/null +++ b/extensions/src/main/java/dev/cel/extensions/SetsFunction.java @@ -0,0 +1,32 @@ +// 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.extensions; + +/** Denotes the extension function used in {@code CelSetsExtension}. */ +public enum SetsFunction { + CONTAINS("sets.contains"), + EQUIVALENT("sets.equivalent"), + INTERSECTS("sets.intersects"); + + private final String functionName; + + String getFunction() { + return functionName; + } + + SetsFunction(String functionName) { + this.functionName = functionName; + } +} diff --git a/extensions/src/test/java/dev/cel/extensions/BUILD.bazel b/extensions/src/test/java/dev/cel/extensions/BUILD.bazel index c71df2d8a..9fda186cf 100644 --- a/extensions/src/test/java/dev/cel/extensions/BUILD.bazel +++ b/extensions/src/test/java/dev/cel/extensions/BUILD.bazel @@ -1,6 +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,26 +12,46 @@ java_library( deps = [ "//:java_truth", "//bundle:cel", - "//common", + "//common:cel_ast", + "//common:cel_exception", "//common:compiler_common", + "//common:container", "//common:options", - "//common/resources/testdata/proto2:messages_extensions_proto2_java_proto", - "//common/resources/testdata/proto2:messages_proto2_java_proto", - "//common/resources/testdata/proto2:test_all_types_java_proto", - "//common/resources/testdata/proto3:test_all_types_java_proto", + "//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", "//extensions:strings", "//parser:macro", + "//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", + "@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", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", - "@maven//:com_google_protobuf_protobuf_java_util", "@maven//:com_google_testparameterinjector_test_parameter_injector", "@maven//:junit_junit", ], diff --git a/extensions/src/test/java/dev/cel/extensions/CelBindingsExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelBindingsExtensionsTest.java index 218435d98..00fcad473 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelBindingsExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelBindingsExtensionsTest.java @@ -22,34 +22,50 @@ 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.CelRuntime; -import dev.cel.runtime.CelRuntime.CelFunctionBinding; -import dev.cel.runtime.CelRuntimeFactory; -import dev.cel.testing.testdata.proto3.TestAllTypesProto.TestAllTypes; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.CelFunctionBinding; import java.util.Arrays; +import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(TestParameterInjector.class) -public final class CelBindingsExtensionsTest { +public final class CelBindingsExtensionsTest extends CelExtensionTestBase { - private static final CelCompiler COMPILER = - CelCompilerFactory.standardCelCompilerBuilder() - .setStandardMacros(CelStandardMacro.STANDARD_MACROS) - .addLibraries(CelExtensions.bindings()) - .build(); + @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(); + } - private static final CelRuntime RUNTIME = CelRuntimeFactory.standardCelRuntimeBuilder().build(); + @Test + public void library() { + CelExtensionLibrary library = + CelExtensions.getExtensionLibrary("bindings", CelOptions.DEFAULT); + assertThat(library.name()).isEqualTo("bindings"); + assertThat(library.latest().version()).isEqualTo(0); + assertThat(library.version(0).functions().stream().map(CelFunctionDecl::name)) + .containsExactly("cel.@block"); + assertThat(library.version(0).macros().stream().map(CelMacro::getFunction)) + .containsExactly("bind"); + } private enum BindingTestCase { BOOL_LITERAL("cel.bind(t, true, t)"), @@ -63,7 +79,8 @@ private enum BindingTestCase { BIND_WITH_EXISTS_TRUE( "cel.bind(valid_elems, [1, 2, 3], [3, 4, 5].exists(e, e in valid_elems))"), BIND_WITH_EXISTS_FALSE("cel.bind(valid_elems, [1, 2, 3], ![4, 5].exists(e, e in valid_elems))"), - BIND_WITH_MAP("[1,2,3].map(x, cel.bind(y, x + x, [y, y])) == [[2, 2], [4, 4], [6, 6]]"); + BIND_WITH_MAP("[1,2,3].map(x, cel.bind(y, x + x, [y, y])) == [[2, 2], [4, 4], [6, 6]]"), + BIND_OPTIONAL_LIST("cel.bind(r0, optional.none(), [?r0, ?r0]) == []"); private final String source; @@ -74,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(); } @@ -84,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", @@ -97,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(); } @@ -116,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"); } @@ -124,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); @@ -196,34 +217,106 @@ 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))) + .addFunctionBindings( + CelFunctionBinding.fromOverloads( + "get_true", + CelFunctionBinding.from( + "get_true_overload", + ImmutableList.of(), + arg -> { + invocation.getAndIncrement(); + return true; + }))) + .build(); + 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); + } + + @Test + @SuppressWarnings({"Immutable", "unchecked"}) // Test only + public void lazyBinding_boundAttributeInComprehension() throws Exception { + AtomicInteger invocation = new AtomicInteger(); + Cel customCel = + runtimeFlavor + .builder() + .setOptions(CelOptions.current().enableHeterogeneousNumericComparisons(true).build()) + .setStandardMacros(CelStandardMacro.MAP) + .addCompilerLibraries(CelExtensions.bindings()) .addFunctionDeclarations( CelFunctionDecl.newFunctionDeclaration( "get_true", CelOverloadDecl.newGlobalOverload("get_true_overload", SimpleType.BOOL))) + .addFunctionBindings( + CelFunctionBinding.fromOverloads( + "get_true", + CelFunctionBinding.from( + "get_true_overload", + ImmutableList.of(), + arg -> { + invocation.getAndIncrement(); + return true; + }))) .build(); + + 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); + } + + @Test + @SuppressWarnings({"Immutable"}) // Test only + public void lazyBinding_boundAttributeInNestedComprehension() throws Exception { AtomicInteger invocation = new AtomicInteger(); - CelRuntime celRuntime = - CelRuntimeFactory.standardCelRuntimeBuilder() + Cel customCel = + runtimeFlavor + .builder() + .setOptions(CelOptions.current().enableHeterogeneousNumericComparisons(true).build()) + .setStandardMacros(CelStandardMacro.EXISTS) + .addCompilerLibraries(CelExtensions.bindings()) + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "get_true", + CelOverloadDecl.newGlobalOverload("get_true_overload", SimpleType.BOOL))) .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(x, get_true(), [1,2,3].exists(unused, x && " + + "['a','b','c'].exists(unused_2, x)))"); assertThat(result).isTrue(); - assertThat(invocation.get()).isEqualTo(2); + 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 new file mode 100644 index 000000000..42dc3e07d --- /dev/null +++ b/extensions/src/test/java/dev/cel/extensions/CelComprehensionsExtensionsTest.java @@ -0,0 +1,369 @@ +// 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.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.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.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 extends CelExtensionTestBase { + + private static final CelOptions CEL_OPTIONS = + CelOptions.current() + .enableHeterogeneousNumericComparisons(true) + // Enable macro call population for unparsing + .populateMacroCalls(true) + .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(); + + @Test + public void library() { + CelExtensionLibrary library = + CelExtensions.getExtensionLibrary("comprehensions", CelOptions.DEFAULT); + assertThat(library.name()).isEqualTo("comprehensions"); + assertThat(library.latest().version()).isEqualTo(0); + assertThat(library.version(0).functions().stream().map(CelFunctionDecl::name)).isEmpty(); + assertThat(library.version(0).macros().stream().map(CelMacro::getFunction)) + .containsAtLeast( + "all", "exists", "exists_one", "transformList", "transformMap", "transformMapEntry"); + } + + @Test + public void allMacro_twoVarComprehension_success( + @TestParameter({ + // list.all() + "[1, 2, 3, 4].all(i, v, i < 5 && v > 0)", + "[1, 2, 3, 4].all(i, v, i < v)", + "[1, 2, 3, 4].all(i, v, i > v) == false", + "cel.bind(listA, [1, 2, 3, 4], cel.bind(listB, [1, 2, 3, 4, 5], listA.all(i, v," + + " listB[?i].hasValue() && listB[i] == v)))", + "cel.bind(listA, [1, 2, 3, 4, 5, 6], cel.bind(listB, [1, 2, 3, 4, 5], listA.all(i, v," + + " listB[?i].hasValue() && listB[i] == v))) == false", + // map.all() + "{'hello': 'world', 'hello!': 'world'}.all(k, v, k.startsWith('hello') && v ==" + + " 'world')", + "{'hello': 'world', 'hello!': 'worlds'}.all(k, v, k.startsWith('hello') &&" + + " v.endsWith('world')) == false", + "{'a': 1, 'b': 2}.all(k, v, k.startsWith('a') && v == 1) == false", + }) + String expr) + throws Exception { + assertThat(eval(expr)).isEqualTo(true); + } + + @Test + public void existsMacro_twoVarComprehension_success( + @TestParameter({ + // list.exists() + "[1, 2, 3, 4].exists(i, v, i > 2 && v < 5)", + "[10, 1, 30].exists(i, v, i == v)", + "[].exists(i, v, true) == false", + "cel.bind(l, ['hello', 'world', 'hello!', 'worlds'], l.exists(i, v," + + " v.startsWith('hello') && l[?(i+1)].optMap(next," + + " next.endsWith('world')).orValue(false)))", + // map.exists() + "{'hello': 'world', 'hello!': 'worlds'}.exists(k, v, k.startsWith('hello') &&" + + " v.endsWith('world'))", + "{}.exists(k, v, true) == false", + "{'a': 1, 'b': 2}.exists(k, v, v == 3) == false", + "{'a': 'b', 'c': 'c'}.exists(k, v, k == v)" + }) + String expr) + throws Exception { + assertThat(eval(expr)).isEqualTo(true); + } + + @Test + public void exists_oneMacro_twoVarComprehension_success( + @TestParameter({ + // list.exists_one() + "[0, 5, 6].exists_one(i, v, i == v)", + "[0, 1, 5].exists_one(i, v, i == v) == false", + "[10, 11, 12].exists_one(i, v, i == v) == false", + "cel.bind(l, ['hello', 'world', 'hello!', 'worlds'], l.exists_one(i, v," + + " v.startsWith('hello') && l[?(i+1)].optMap(next," + + " next.endsWith('world')).orValue(false)))", + "cel.bind(l, ['hello', 'goodbye', 'hello!', 'goodbye'], l.exists_one(i, v," + + " v.startsWith('hello') && l[?(i+1)].optMap(next, next ==" + + " 'goodbye').orValue(false))) == false", + // map.exists_one() + "{'hello': 'world', 'hello!': 'worlds'}.exists_one(k, v, k.startsWith('hello') &&" + + " v.endsWith('world'))", + "{'hello': 'world', 'hello!': 'wow, world'}.exists_one(k, v, k.startsWith('hello') &&" + + " v.endsWith('world')) == false", + "{'a': 1, 'b': 1}.exists_one(k, v, v == 2) == false" + }) + String expr) + throws Exception { + assertThat(eval(expr)).isEqualTo(true); + } + + @Test + public void transformListMacro_twoVarComprehension_success( + @TestParameter({ + // list.transformList() + "[1, 2, 3].transformList(i, v, (i * v) + v) == [1, 4, 9]", + "[1, 2, 3].transformList(i, v, i % 2 == 0, (i * v) + v) == [1, 9]", + "[1, 2, 3].transformList(i, v, i > 0 && v < 3, (i * v) + v) == [4]", + "[1, 2, 3].transformList(i, v, i % 2 == 0, (i * v) + v) == [1, 9]", + "[1, 2, 3].transformList(i, v, (i * v) + v) == [1, 4, 9]", + "[-1, -2, -3].transformList(i, v, [1, 2].transformList(i, v, i + v)) == [[1, 3], [1," + + " 3], [1, 3]]", + // map.transformList() + "{'greeting': 'hello', 'farewell': 'goodbye'}.transformList(k, _, k).sort() ==" + + " ['farewell', 'greeting']", + "{'greeting': 'hello', 'farewell': 'goodbye'}.transformList(_, v, v).sort() ==" + + " ['goodbye', 'hello']" + }) + String expr) + throws Exception { + assertThat(eval(expr)).isEqualTo(true); + } + + @Test + public void transformMapMacro_twoVarComprehension_success( + @TestParameter({ + // list.transformMap() + "['Hello', 'world'].transformMap(i, v, [v.lowerAscii()]) == {0: ['hello'], 1:" + + " ['world']}", + "['world', 'Hello'].transformMap(i, v, [v.lowerAscii()]).transformList(k, v," + + " v).flatten().sort() == ['hello', 'world']", + "[1, 2, 3].transformMap(indexVar, valueVar, (indexVar * valueVar) + valueVar) == {0:" + + " 1, 1: 4, 2: 9}", + "[1, 2, 3].transformMap(indexVar, valueVar, indexVar % 2 == 0, (indexVar * valueVar)" + + " + valueVar) == {0: 1, 2: 9}", + // map.transformMap() + "{'greeting': 'hello'}.transformMap(k, v, v + '!') == {'greeting': 'hello!'}", + "dyn({'greeting': 'hello'}).transformMap(k, v, v + '!') == {'greeting': 'hello!'}", + "{'hello': 'world', 'goodbye': 'cruel world'}.transformMap(k, v, v.startsWith('world')," + + " v + '!') == {'hello': 'world!'}", + "{}.transformMap(k, v, v + '!') == {}" + }) + String expr) + throws Exception { + assertThat(eval(expr)).isEqualTo(true); + } + + @Test + public void transformMapEntryMacro_twoVarComprehension_success( + @TestParameter({ + // list.transformMapEntry() + "'key1:value1 key2:value2 key3:value3'.split(' ').transformMapEntry(i, v," + + " cel.bind(entry, v.split(':'),entry.size() == 2 ? {entry[0]: entry[1]} : {})) ==" + + " {'key1': 'value1', 'key2': 'value2', 'key3': 'value3'}", + "'key1:value1:extra key2:value2 key3'.split(' ').transformMapEntry(i, v," + + " cel.bind(entry, v.split(':'), {?entry[0]: entry[?1]})) == {'key1': 'value1'," + + " '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," + + " 'c': 30}", + "{'a': 1, 'b': 2}.transformMapEntry(k, v, k == 'a', {k + '_filtered': v}) ==" + + " {'a_filtered': 1}", + }) + String expr) + throws Exception { + assertThat(eval(expr)).isEqualTo(true); + } + + @Test + public void comprehension_onTypeParam_success() throws Exception { + Assume.assumeFalse(isParseOnly); + Cel customCel = + runtimeFlavor + .builder() + .setOptions(CEL_OPTIONS) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addCompilerLibraries(CelExtensions.comprehensions()) + .addVar("items", TypeParamType.create("T")) + .build(); + + CelAbstractSyntaxTree ast = customCel.compile("items.all(i, v, v > 0)").getAst(); + + assertThat(ast.getResultType()).isEqualTo(SimpleType.BOOL); + } + + @Test + public void unparseAST_twoVarComprehension( + @TestParameter({ + "cel.bind(listA, [1, 2, 3, 4], cel.bind(listB, [1, 2, 3, 4, 5], listA.all(i, v," + + " listB[?i].hasValue() && listB[i] == v)))", + "[1, 2, 3, 4].exists(i, v, i > 2 && v < 5)", + "{\"a\": 1, \"b\": 1}.exists_one(k, v, v == 2) == false", + "[1, 2, 3].transformList(i, v, i > 0 && v < 3, i * v + v) == [4]", + "[1, 2, 2].transformList(i, v, i / 2 == 1)", + "{\"a\": \"b\", \"c\": \"d\"}.exists_one(k, v, k == \"b\" || v == \"b\")", + "{\"a\": \"b\", \"c\": \"d\"}.exists(k, v, k == \"b\" || v == \"b\")", + "[null, null, \"hello\", string].all(i, v, i == 0 || type(v) != int)" + }) + String expr) + throws Exception { + CelAbstractSyntaxTree ast = isParseOnly ? cel.parse(expr).getAst() : cel.compile(expr).getAst(); + String unparsed = UNPARSER.unparse(ast); + assertThat(unparsed).isEqualTo(expr); + } + + @Test + @TestParameters( + "{expr: '[].exists_one(i.j, k, i.j < k)', err: 'The argument must be a simple name'}") + @TestParameters( + "{expr: '[].exists_one(i, [k], i < [k])', err: 'The argument must be a simple name'}") + @TestParameters( + "{expr: '1.exists_one(j, j < 5)', err: 'cannot be range of a comprehension (must be list," + + " map, or dynamic)'}") + @TestParameters( + "{expr: '1.exists_one(j, k, j < k)', err: 'cannot be range of a comprehension (must be list," + + " map, or dynamic)'}") + @TestParameters( + "{expr: '[].transformList(__result__, i, __result__ < i)', err: 'The iteration variable" + + " __result__ overwrites accumulator variable'}") + @TestParameters( + "{expr: '[].exists(__result__, i, __result__ < i)', err: 'The iteration variable __result__" + + " overwrites accumulator variable'}") + @TestParameters( + "{expr: '[].exists(j, __result__, __result__ < j)', err: 'The iteration variable __result__" + + " overwrites accumulator variable'}") + @TestParameters( + "{expr: 'no_such_var.all(i, v, v > 0)', err: \"undeclared reference to 'no_such_var'\"}") + @TestParameters( + "{expr: '{}.transformMap(i.j, k, i.j + k)', err: 'argument must be a simple name'}") + @TestParameters( + "{expr: '{}.transformMap(i, k.j, i + k.j)', err: 'argument must be a simple name'}") + @TestParameters( + "{expr: '{}.transformMapEntry(j, i.k, {j: i.k})', err: 'argument must be a simple name'}") + @TestParameters( + "{expr: '{}.transformMapEntry(i.j, k, {k: i.j})', err: 'argument must be a simple name'}") + @TestParameters( + "{expr: '{}.transformMapEntry(j, k, \"bad filter\", {k: j})', err: 'no matching overload'}") + @TestParameters( + "{expr: '[1, 2].transformList(i, v, v % 2 == 0 ? [v] : v)', err: 'no matching overload'}") + @TestParameters( + "{expr: \"{'hello': 'world', 'greetings': 'tacocat'}.transformMapEntry(k, v, []) == {}\"," + + " err: 'no matching overload'}") + public void twoVarComprehension_compilerErrors(String expr, String err) throws Exception { + Assume.assumeFalse(isParseOnly); + CelValidationResult result = cel.compile(expr); + CelValidationException e = assertThrows(CelValidationException.class, () -> result.getAst()); + + assertThat(e).hasMessageThat().contains(err); + } + + @Test + @TestParameters( + "{expr: \"['a:1', 'b:2', 'a:3'].transformMapEntry(i, v, cel.bind(p, v.split(':'), {p[0]:" + + " p[1]})) == {'a': '3', 'b': '2'}\", err: \"insert failed: key 'a' already exists\"}") + @TestParameters( + "{expr: '[1, 1].transformMapEntry(i, v, {v: i})', err: \"insert failed: key '1' already" + + " exists\"}") + @TestParameters( + "{expr: \"{'a': 65, 'b': 65u}.transformMapEntry(i, v, {v: i})\", err: \"insert failed: key" + + " '65' already exists\"}") + @TestParameters( + "{expr: \"{'a': 2, 'b': 2.0}.transformMapEntry(i, v, {v: i})\", err: \"insert failed: key" + + " '2.0' already exists\"}") + public void twoVarComprehension_keyCollision_runtimeError(String expr, String err) + throws Exception { + // 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 { + CelEvaluationException e = + 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 { + CelEvaluationException e = + 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 deee407aa..afeaa9105 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelEncoderExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelEncoderExtensionsTest.java @@ -19,113 +19,106 @@ import static org.junit.Assert.assertThrows; import com.google.common.collect.ImmutableMap; -import com.google.protobuf.ByteString; 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.compiler.CelCompiler; -import dev.cel.compiler.CelCompilerFactory; +import dev.cel.common.values.CelByteString; 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().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(); + } - private static final CelCompiler CEL_COMPILER = - CelCompilerFactory.standardCelCompilerBuilder() - .addVar("stringVar", SimpleType.STRING) - .addLibraries(CelExtensions.encoders()) - .build(); - private static final CelRuntime CEL_RUNTIME = - CelRuntimeFactory.standardCelRuntimeBuilder().addLibraries(CelExtensions.encoders()).build(); + @Test + public void library() { + CelExtensionLibrary library = + CelExtensions.getExtensionLibrary("encoders", CelOptions.DEFAULT); + assertThat(library.name()).isEqualTo("encoders"); + assertThat(library.latest().version()).isEqualTo(0); + assertThat(library.version(0).functions().stream().map(CelFunctionDecl::name)) + .containsExactly("base64.decode", "base64.encode"); + assertThat(library.version(0).macros()).isEmpty(); + } @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 { - ByteString decodedBytes = - (ByteString) - CEL_RUNTIME - .createProgram(CEL_COMPILER.compile("base64.decode('aGVsbG8=')").getAst()) - .eval(); + CelByteString decodedBytes = (CelByteString) eval("base64.decode('aGVsbG8=')"); assertThat(decodedBytes.size()).isEqualTo(5); - assertThat(decodedBytes.toString(ISO_8859_1)).isEqualTo("hello"); + assertThat(new String(decodedBytes.toByteArray(), ISO_8859_1)).isEqualTo("hello"); } @Test public void decode_withoutPadding_success() throws Exception { - ByteString decodedBytes = - (ByteString) - 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(decodedBytes.toString(ISO_8859_1)).isEqualTo("hello"); + assertThat(new String(decodedBytes.toByteArray(), ISO_8859_1)).isEqualTo("hello"); } @Test public void roundTrip_success() throws Exception { - String encodedString = - (String) - CEL_RUNTIME - .createProgram(CEL_COMPILER.compile("base64.encode(b'Hello World!')").getAst()) - .eval(); - ByteString decodedBytes = - (ByteString) - CEL_RUNTIME - .createProgram(CEL_COMPILER.compile("base64.decode(stringVar)").getAst()) - .eval(ImmutableMap.of("stringVar", encodedString)); - - assertThat(decodedBytes.toString(ISO_8859_1)).isEqualTo("Hello World!"); + String encodedString = (String) eval("base64.encode(b'Hello World!')"); + CelByteString decodedBytes = + (CelByteString) + 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 0b2e7b2cd..31c7d65c8 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelExtensionsTest.java @@ -89,7 +89,9 @@ public void addStringExtensionsForCompilerOnly_throwsEvaluationException() throw assertThat(exception) .hasMessageThat() - .contains("Unknown overload id 'string_substring_int_int' for function 'substring'"); + .contains( + "No matching overload for function 'substring'. Overload candidates:" + + " string_substring_int_int"); } @Test @@ -138,5 +140,61 @@ public void addEncoderExtension_success() throws Exception { assertThat(evaluatedResult).isTrue(); } -} + @Test + public void getAllFunctionNames() { + assertThat(CelExtensions.getAllFunctionNames()) + .containsExactly( + "math.@max", + "math.@min", + "math.ceil", + "math.floor", + "math.round", + "math.trunc", + "math.isFinite", + "math.isNaN", + "math.isInf", + "math.abs", + "math.sign", + "math.bitAnd", + "math.bitOr", + "math.bitXor", + "math.bitNot", + "math.bitShiftLeft", + "math.bitShiftRight", + "math.sqrt", + "charAt", + "indexOf", + "join", + "lastIndexOf", + "lowerAscii", + "strings.quote", + "replace", + "split", + "substring", + "trim", + "upperAscii", + "sets.contains", + "sets.equivalent", + "sets.intersects", + "base64.decode", + "base64.encode", + "slice", + "flatten", + "lists.range", + "distinct", + "reverse", + "sort", + "lists.@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 new file mode 100644 index 000000000..4520f81ba --- /dev/null +++ b/extensions/src/test/java/dev/cel/extensions/CelListsExtensionsTest.java @@ -0,0 +1,323 @@ +// 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. +package dev.cel.extensions; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSortedMultiset; +import com.google.common.collect.ImmutableSortedSet; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import com.google.testing.junit.testparameterinjector.TestParameters; +import dev.cel.bundle.Cel; +import dev.cel.common.CelContainer; +import dev.cel.common.CelValidationException; +import dev.cel.common.CelValidationResult; +import dev.cel.common.types.SimpleType; +import dev.cel.expr.conformance.test.SimpleTest; +import dev.cel.parser.CelStandardMacro; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.testing.CelRuntimeFlavor; +import org.junit.Assume; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +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() { + assertThat(CelExtensions.lists(0).functions().stream().map(f -> f.name())) + .containsExactly("slice"); + assertThat(CelExtensions.lists(1).functions().stream().map(f -> f.name())) + .containsExactly("slice", "flatten"); + assertThat(CelExtensions.lists(2).functions().stream().map(f -> f.name())) + .containsExactly( + "slice", + "flatten", + "lists.range", + "distinct", + "reverse", + "sort", + "lists.@sortByAssociatedKeys"); + } + + @Test + public void macroList_byVersion() { + assertThat(CelExtensions.lists(0).macros().stream().map(f -> f.getFunction())).isEmpty(); + assertThat(CelExtensions.lists(1).macros().stream().map(f -> f.getFunction())).isEmpty(); + assertThat(CelExtensions.lists(2).macros().stream().map(f -> f.getFunction())) + .containsExactly("sortBy"); + } + + @Test + @TestParameters("{expression: '[1,2,3,4].slice(0, 4)', expected: '[1,2,3,4]'}") + @TestParameters("{expression: '[1,2,3,4].slice(0, 0)', expected: '[]'}") + @TestParameters("{expression: '[1,2,3,4].slice(1, 1)', expected: '[]'}") + @TestParameters("{expression: '[1,2,3,4].slice(4, 4)', expected: '[]'}") + @TestParameters("{expression: '[1,2,3,4].slice(1, 3)', expected: '[2, 3]'}") + @TestParameters("{expression: 'non_list.slice(1, 3)', expected: '[2, 3]'}") + public void slice_success(String expression, String expected) throws Exception { + Object result = + eval(cel, expression, ImmutableMap.of("non_list", ImmutableSortedSet.of(4L, 1L, 3L, 2L))); + + assertThat(result).isEqualTo(eval(cel, expected)); + } + + @Test + @TestParameters( + "{expression: '[1,2,3,4].slice(3, 0)', " + + "expectedError: 'Start index must be less than or equal to end index'}") + @TestParameters("{expression: '[1,2,3,4].slice(0, 10)', expectedError: 'List is length 4'}") + @TestParameters( + "{expression: '[1,2,3,4].slice(-5, 10)', " + + "expectedError: 'Negative indexes not supported'}") + @TestParameters( + "{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, () -> eval(cel, expression))) + .hasCauseThat() + .hasMessageThat() + .contains(expectedError); + } + + @Test + @TestParameters("{expression: '[].flatten() == []'}") + @TestParameters("{expression: '[[1, 2]].flatten().exists(i, i == 1)'}") + @TestParameters("{expression: '[[], [[]], [[[]]]].flatten() == [[], [[]]]'}") + @TestParameters("{expression: '[1,[2,[3,4]]].flatten() == [1,2,[3,4]]'}") + @TestParameters("{expression: '[1,2,[],[],[3,4]].flatten() == [1,2,3,4]'}") + @TestParameters("{expression: '[1,[2,3],[[4,5]], [[[6,7]]]].flatten() == [1,2,3,[4,5],[[6,7]]]'}") + @TestParameters("{expression: 'dyn([1]).flatten() == [1]'}") + @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) eval(cel, expression); + + assertThat(result).isTrue(); + } + + @Test + @TestParameters("{expression: '[1,2,3,4].flatten(1) == [1,2,3,4]'}") + @TestParameters("{expression: '[1,[2,[3,[4]]]].flatten(0) == [1,[2,[3,[4]]]]'}") + @TestParameters("{expression: '[1,[2,[3,[4]]]].flatten(2) == [1,2,3,[4]]'}") + @TestParameters("{expression: '[1,[2,[3,4]]].flatten(2) == [1,2,3,4]'}") + @TestParameters("{expression: '[[], [[]], [[[]]]].flatten(2) == [[]]'}") + @TestParameters("{expression: '[[], [[]], [[[]]]].flatten(3) == []'}") + @TestParameters("{expression: '[[], [[]], [[[]]]].flatten(4) == []'}") + // 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) eval(cel, expression); + + assertThat(result).isTrue(); + } + + @Test + public void flatten_negativeDepth_throws() { + CelEvaluationException e = + 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"); + } + + @Test + @TestParameters("{expression: '[1].flatten()'}") + @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()); + } + + @Test + @TestParameters("{expression: 'lists.range(9) == [0,1,2,3,4,5,6,7,8]'}") + @TestParameters("{expression: 'lists.range(0) == []'}") + @TestParameters("{expression: 'lists.range(-1) == []'}") + public void range_success(String expression) throws Exception { + boolean result = (boolean) eval(cel, expression); + + assertThat(result).isTrue(); + } + + @Test + @TestParameters("{expression: '[].distinct()', expected: '[]'}") + @TestParameters("{expression: '[].distinct()', expected: '[]'}") + @TestParameters("{expression: '[1].distinct()', expected: '[1]'}") + @TestParameters("{expression: '[-2, 5, -2, 1, 1, 5, -2, 1].distinct()', expected: '[-2, 5, 1]'}") + @TestParameters( + "{expression: '[-2, 5, -2, 1, 1, 5, -2, 1, 5, -2, -2, 1].distinct()', " + + "expected: '[-2, 5, 1]'}") + @TestParameters( + "{expression: '[\"c\", \"a\", \"a\", \"b\", \"a\", \"b\", \"c\", \"c\"].distinct()'," + + " expected: '[\"c\", \"a\", \"b\"]'}") + @TestParameters( + "{expression: '[1, 2.0, \"c\", 3, \"c\", 1].distinct()', " + + "expected: '[1, 2.0, \"c\", 3]'}") + @TestParameters("{expression: '[1, 1.0, 2, 2u].distinct()', expected: '[1, 2]'}") + @TestParameters("{expression: '[[1], [1], [2]].distinct()', expected: '[[1], [2]]'}") + @TestParameters( + "{expression: '[SimpleTest{name: \"a\"}, SimpleTest{name: \"b\"}, SimpleTest{name: \"a\"}]" + + ".distinct()', " + + "expected: '[SimpleTest{name: \"a\"}, SimpleTest{name: \"b\"}]'}") + @TestParameters("{expression: 'non_list.distinct()', expected: '[1, 2, 3, 4]'}") + public void distinct_success(String expression, String expected) throws Exception { + Object result = + eval( + cel, + expression, + ImmutableMap.of( + "non_list", ImmutableSortedMultiset.of(1L, 2L, 3L, 4L, 4L, 1L, 3L, 2L))); + + assertThat(result).isEqualTo(eval(cel, expected)); + } + + @Test + @TestParameters("{expression: '[5,1,2,3].reverse()', expected: '[3,2,1,5]'}") + @TestParameters("{expression: '[].reverse()', expected: '[]'}") + @TestParameters("{expression: '[1].reverse()', expected: '[1]'}") + @TestParameters( + "{expression: '[\"are\", \"you\", \"as\", \"bored\", \"as\", \"I\", \"am\"].reverse()', " + + "expected: '[\"am\", \"I\", \"as\", \"bored\", \"as\", \"you\", \"are\"]'}") + @TestParameters( + "{expression: '[false, true, true].reverse().reverse()', expected: '[false, true, true]'}") + @TestParameters("{expression: 'non_list.reverse()', expected: '[4, 3, 2, 1]'}") + public void reverse_success(String expression, String expected) throws Exception { + Object result = + eval(cel, expression, ImmutableMap.of("non_list", ImmutableSortedSet.of(4L, 1L, 3L, 2L))); + + assertThat(result).isEqualTo(eval(cel, expected)); + } + + @Test + @TestParameters("{expression: '[].sort()', expected: '[]'}") + @TestParameters("{expression: '[1].sort()', expected: '[1]'}") + @TestParameters("{expression: '[4, 3, 2, 1].sort()', expected: '[1, 2, 3, 4]'}") + @TestParameters( + "{expression: '[\"d\", \"a\", \"b\", \"c\"].sort()', " + + "expected: '[\"a\", \"b\", \"c\", \"d\"]'}") + public void sort_success(String expression, String expected) throws Exception { + Object result = eval(cel, expression); + + assertThat(result).isEqualTo(eval(cel, expected)); + } + + @Test + @TestParameters("{expression: '[3.0, 2, 1u].sort()', expected: '[1u, 2, 3.0]'}") + @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 = eval(cel, expression); + + assertThat(result).isEqualTo(eval(cel, expected)); + } + + @Test + @TestParameters( + "{expression: '[\"d\", 3, 2, \"c\"].sort()', " + + "expectedError: 'List elements must have the same type'}") + @TestParameters( + "{expression: '[SimpleTest{name: \"a\"}, SimpleTest{name: \"b\"}].sort()', " + + "expectedError: 'List elements must be comparable'}") + public void sort_throws(String expression, String expectedError) throws Exception { + assertThat(assertThrows(CelEvaluationException.class, () -> eval(cel, expression))) + .hasCauseThat() + .hasMessageThat() + .contains(expectedError); + } + + @Test + @TestParameters("{expression: '[].sortBy(e, e)', expected: '[]'}") + @TestParameters("{expression: '[\"a\"].sortBy(e, e)', expected: '[\"a\"]'}") + @TestParameters( + "{expression: '[-3, 1, -5, -2, 4].sortBy(e, -(e * e))', " + "expected: '[-5, 4, -3, -2, 1]'}") + @TestParameters( + "{expression: '[-3, 1, -5, -2, 4].map(e, e * 2).sortBy(e, -(e * e)) ', " + + "expected: '[-10, 8, -6, -4, 2]'}") + @TestParameters("{expression: 'lists.range(3).sortBy(e, -e) ', " + "expected: '[2, 1, 0]'}") + @TestParameters( + "{expression: '[\"a\", \"c\", \"b\", \"first\"].sortBy(e, e == \"first\" ? \"\" : e)', " + + "expected: '[\"first\", \"a\", \"b\", \"c\"]'}") + @TestParameters( + "{expression: '[SimpleTest{name: \"baz\"}," + + " SimpleTest{name: \"foo\"}," + + " SimpleTest{name: \"bar\"}].sortBy(e, e.name)', " + + "expected: '[SimpleTest{name: \"bar\"}," + + " SimpleTest{name: \"baz\"}," + + " SimpleTest{name: \"foo\"}]'}") + public void sortBy_success(String expression, String expected) throws Exception { + Object result = eval(cel, expression); + + assertThat(result).isEqualTo(eval(cel, expected)); + } + + @Test + @TestParameters( + "{expression: 'lists.range(3).sortBy(-e, e)', " + + "expectedError: 'variable name must be a simple identifier'}") + @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 { + CelValidationResult result = cel.compile(expression); + assertThat(assertThrows(CelValidationException.class, () -> result.getAst())) + .hasMessageThat() + .contains(expectedError); + } + + @Test + @TestParameters( + "{expression: '[[1, 2], [\"a\", \"b\"]].sortBy(e, e[0])', " + + "expectedError: 'List elements must have the same type'}") + @TestParameters( + "{expression: '[SimpleTest{name: \"a\"}, SimpleTest{name: \"b\"}].sortBy(e, e)', " + + "expectedError: 'List elements must be comparable'}") + public void sortBy_throws_evaluationException(String expression, String expectedError) + throws Exception { + assertThat(assertThrows(CelEvaluationException.class, () -> eval(cel, expression))) + .hasCauseThat() + .hasMessageThat() + .contains(expectedError); + } + + +} diff --git a/extensions/src/test/java/dev/cel/extensions/CelLiteExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelLiteExtensionsTest.java new file mode 100644 index 000000000..c78eb9e58 --- /dev/null +++ b/extensions/src/test/java/dev/cel/extensions/CelLiteExtensionsTest.java @@ -0,0 +1,48 @@ +// 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.extensions; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelOptions; +import dev.cel.compiler.CelCompiler; +import dev.cel.compiler.CelCompilerFactory; +import dev.cel.runtime.CelLiteRuntime; +import dev.cel.runtime.CelLiteRuntimeFactory; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public final class CelLiteExtensionsTest { + + @Test + public void addSetsExtensions() throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addLibraries(CelExtensions.sets(CelOptions.DEFAULT)) + .build(); + CelLiteRuntime runtime = + CelLiteRuntimeFactory.newLiteRuntimeBuilder() + .addLibraries(CelLiteExtensions.sets(CelOptions.DEFAULT)) + .build(); + CelAbstractSyntaxTree ast = compiler.compile("sets.contains([1, 1], [1])").getAst(); + + boolean result = (boolean) runtime.createProgram(ast).eval(); + + assertThat(result).isTrue(); + } +} diff --git a/extensions/src/test/java/dev/cel/extensions/CelMathExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelMathExtensionsTest.java index 320e7dfea..68c80dedb 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; @@ -32,24 +34,39 @@ import dev.cel.compiler.CelCompiler; import dev.cel.compiler.CelCompilerFactory; import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.CelFunctionBinding; import dev.cel.runtime.CelRuntime; -import dev.cel.runtime.CelRuntime.CelFunctionBinding; 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() - .addLibraries(CelExtensions.math(CEL_OPTIONS)) - .build(); - private static final CelRuntime CEL_RUNTIME = - CelRuntimeFactory.standardCelRuntimeBuilder() - .addLibraries(CelExtensions.math(CEL_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}") @@ -84,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); } @@ -123,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); } @@ -150,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(); @@ -169,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'}") @@ -238,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(); @@ -258,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"); } @@ -270,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"); } @@ -284,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() @@ -299,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(); @@ -319,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() @@ -334,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(); } @@ -387,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); } @@ -430,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); } @@ -461,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(); @@ -476,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'}") @@ -536,16 +485,16 @@ public void least_unsignedLongResult_withSignedLongType_success(String expr, lon + " '3'}") public void least_unsignedLongResult_withUnsignedLongType_success( String expr, String expectedResult) throws Exception { - CelOptions celOptions = CelOptions.current().enableUnsignedLongs(true).build(); + CelOptions celOptions = CelOptions.current().build(); 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(); @@ -556,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"); } @@ -568,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"); } @@ -582,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() @@ -597,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, () -> eval(expr)); - CelEvaluationException e = - assertThrows(CelEvaluationException.class, () -> CEL_RUNTIME.createProgram(ast).eval()); - - 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(); @@ -617,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() @@ -632,25 +577,553 @@ 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(); } + + @Test + @TestParameters("{expr: 'math.isNaN(0.0/0.0)', expectedResult: true}") + @TestParameters("{expr: 'math.isNaN(1.0/1.0)', expectedResult: false}") + @TestParameters("{expr: 'math.isNaN(12.031)', expectedResult: false}") + @TestParameters("{expr: 'math.isNaN(-1.0/0.0)', expectedResult: false}") + @TestParameters("{expr: 'math.isNaN(math.round(0.0/0.0))', expectedResult: true}") + @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.compile(expr).getAst(); + + Object result = cel.createProgram(ast).eval(); + + assertThat(result).isEqualTo(expectedResult); + } + + @Test + @TestParameters("{expr: 'math.isNaN()'}") + @TestParameters("{expr: 'math.isNaN(1)'}") + @TestParameters("{expr: 'math.isNaN(9223372036854775807)'}") + @TestParameters("{expr: 'math.isNaN(1u)'}") + public void isNaN_invalidArgs_throwsException(String expr) { + CelValidationException e = + assertThrows(CelValidationException.class, () -> cel.compile(expr).getAst()); + + assertThat(e).hasMessageThat().contains("found no matching overload for 'math.isNaN'"); + } + + @Test + @TestParameters("{expr: 'math.isFinite(1.0/1.5)', expectedResult: true}") + @TestParameters("{expr: 'math.isFinite(15312.2121)', expectedResult: true}") + @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.compile(expr).getAst(); + + Object result = cel.createProgram(ast).eval(); + + assertThat(result).isEqualTo(expectedResult); + } + + @Test + @TestParameters("{expr: 'math.isFinite()'}") + @TestParameters("{expr: 'math.isFinite(1)'}") + @TestParameters("{expr: 'math.isFinite(9223372036854775807)'}") + @TestParameters("{expr: 'math.isFinite(1u)'}") + public void isFinite_invalidArgs_throwsException(String expr) { + CelValidationException e = + assertThrows(CelValidationException.class, () -> cel.compile(expr).getAst()); + + assertThat(e).hasMessageThat().contains("found no matching overload for 'math.isFinite'"); + } + + @Test + @TestParameters("{expr: 'math.isInf(1.0/0.0)', expectedResult: true}") + @TestParameters("{expr: 'math.isInf(-1.0/0.0)', expectedResult: true}") + @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.compile(expr).getAst(); + + Object result = cel.createProgram(ast).eval(); + + assertThat(result).isEqualTo(expectedResult); + } + + @Test + @TestParameters("{expr: 'math.isInf()'}") + @TestParameters("{expr: 'math.isInf(1)'}") + @TestParameters("{expr: 'math.isInf(9223372036854775807)'}") + @TestParameters("{expr: 'math.isInf(1u)'}") + public void isInf_invalidArgs_throwsException(String expr) { + CelValidationException e = + assertThrows(CelValidationException.class, () -> cel.compile(expr).getAst()); + + assertThat(e).hasMessageThat().contains("found no matching overload for 'math.isInf'"); + } + + @Test + @TestParameters("{expr: 'math.ceil(1.2)' , expectedResult: 2.0}") + @TestParameters("{expr: 'math.ceil(54.78)' , expectedResult: 55.0}") + @TestParameters("{expr: 'math.ceil(-2.2)' , expectedResult: -2.0}") + @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.compile(expr).getAst(); + + Object result = cel.createProgram(ast).eval(); + + assertThat(result).isEqualTo(expectedResult); + } + + @Test + @TestParameters("{expr: 'math.ceil()'}") + @TestParameters("{expr: 'math.ceil(1)'}") + @TestParameters("{expr: 'math.ceil(9223372036854775807)'}") + @TestParameters("{expr: 'math.ceil(1u)'}") + public void ceil_invalidArgs_throwsException(String expr) { + CelValidationException e = + assertThrows(CelValidationException.class, () -> cel.compile(expr).getAst()); + + assertThat(e).hasMessageThat().contains("found no matching overload for 'math.ceil'"); + } + + @Test + @TestParameters("{expr: 'math.floor(1.2)' , expectedResult: 1.0}") + @TestParameters("{expr: 'math.floor(-5.2)' , expectedResult: -6.0}") + @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.compile(expr).getAst(); + + Object result = cel.createProgram(ast).eval(); + + assertThat(result).isEqualTo(expectedResult); + } + + @Test + @TestParameters("{expr: 'math.floor()'}") + @TestParameters("{expr: 'math.floor(1)'}") + @TestParameters("{expr: 'math.floor(9223372036854775807)'}") + @TestParameters("{expr: 'math.floor(1u)'}") + public void floor_invalidArgs_throwsException(String expr) { + CelValidationException e = + assertThrows(CelValidationException.class, () -> cel.compile(expr).getAst()); + + assertThat(e).hasMessageThat().contains("found no matching overload for 'math.floor'"); + } + + @Test + @TestParameters("{expr: 'math.round(1.2)' , expectedResult: 1.0}") + @TestParameters("{expr: 'math.round(1.5)' , expectedResult: 2.0}") + @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.compile(expr).getAst(); + + Object result = cel.createProgram(ast).eval(); + + assertThat(result).isEqualTo(expectedResult); + } + + @Test + @TestParameters("{expr: 'math.round()'}") + @TestParameters("{expr: 'math.round(1)'}") + @TestParameters("{expr: 'math.round(9223372036854775807)'}") + @TestParameters("{expr: 'math.round(1u)'}") + public void round_invalidArgs_throwsException(String expr) { + CelValidationException e = + assertThrows(CelValidationException.class, () -> cel.compile(expr).getAst()); + + assertThat(e).hasMessageThat().contains("found no matching overload for 'math.round'"); + } + + @Test + @TestParameters("{expr: 'math.trunc(-1.3)' , expectedResult: -1.0}") + @TestParameters("{expr: 'math.trunc(1.3)' , expectedResult: 1.0}") + @TestParameters("{expr: 'math.trunc(0.0/0.0)' , expectedResult: NaN}") + @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.compile(expr).getAst(); + + Object result = cel.createProgram(ast).eval(); + + assertThat(result).isEqualTo(expectedResult); + } + + @Test + @TestParameters("{expr: 'math.trunc()'}") + @TestParameters("{expr: 'math.trunc(1)'}") + @TestParameters("{expr: 'math.trunc()'}") + @TestParameters("{expr: 'math.trunc(1u)'}") + public void trunc_invalidArgs_throwsException(String expr) { + CelValidationException e = + assertThrows(CelValidationException.class, () -> cel.compile(expr).getAst()); + + assertThat(e).hasMessageThat().contains("found no matching overload for 'math.trunc'"); + } + + @Test + @TestParameters("{expr: 'math.abs(1)', expectedResult: 1}") + @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.compile(expr).getAst(); + + Object result = cel.createProgram(ast).eval(); + + assertThat(result).isEqualTo(expectedResult); + } + + @Test + @TestParameters("{expr: 'math.abs(-234.5)' , expectedResult: 234.5}") + @TestParameters("{expr: 'math.abs(234.5)' , expectedResult: 234.5}") + @TestParameters("{expr: 'math.abs(0.0/0.0)' , expectedResult: NaN}") + @TestParameters("{expr: 'math.abs(-0.0/0.0)' , expectedResult: NaN}") + @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.compile(expr).getAst(); + + Object result = cel.createProgram(ast).eval(); + + assertThat(result).isEqualTo(expectedResult); + } + + @Test + public void abs_overflow_throwsException() { + CelValidationException e = + assertThrows( + CelValidationException.class, + () -> cel.compile("math.abs(-9223372036854775809)").getAst()); + + assertThat(e) + .hasMessageThat() + .contains("ERROR: :1:10: For input string: \"-9223372036854775809\""); + } + + @Test + @TestParameters("{expr: 'math.sign(-100)', expectedResult: -1}") + @TestParameters("{expr: 'math.sign(0)', expectedResult: 0}") + @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.compile(expr).getAst(); + + Object result = cel.createProgram(ast).eval(); + + assertThat(result).isEqualTo(expectedResult); + } + + @Test + @TestParameters("{expr: 'math.sign(-234.5)' , expectedResult: -1.0}") + @TestParameters("{expr: 'math.sign(234.5)' , expectedResult: 1.0}") + @TestParameters("{expr: 'math.sign(0.2321)' , expectedResult: 1.0}") + @TestParameters("{expr: 'math.sign(0.0)' , expectedResult: 0.0}") + @TestParameters("{expr: 'math.sign(-0.0)' , expectedResult: 0.0}") + @TestParameters("{expr: 'math.sign(0.0/0.0)' , expectedResult: NaN}") + @TestParameters("{expr: 'math.sign(-0.0/0.0)' , expectedResult: NaN}") + @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.compile(expr).getAst(); + + Object result = cel.createProgram(ast).eval(); + + assertThat(result).isEqualTo(expectedResult); + } + + @Test + @TestParameters("{expr: 'math.sign()'}") + @TestParameters("{expr: 'math.sign(\"\")'}") + public void sign_invalidArgs_throwsException(String expr) { + CelValidationException e = + assertThrows(CelValidationException.class, () -> cel.compile(expr).getAst()); + + assertThat(e).hasMessageThat().contains("found no matching overload for 'math.sign'"); + } + + @Test + @TestParameters("{expr: 'math.bitAnd(1,2)' , expectedResult: 0}") + @TestParameters("{expr: 'math.bitAnd(1,-1)' , expectedResult: 1}") + @TestParameters( + "{expr: 'math.bitAnd(9223372036854775807,9223372036854775807)' , expectedResult:" + + " 9223372036854775807}") + public void bitAnd_signedInt_success(String expr, long expectedResult) throws Exception { + CelAbstractSyntaxTree ast = cel.compile(expr).getAst(); + + Object result = cel.createProgram(ast).eval(); + + assertThat(result).isEqualTo(expectedResult); + } + + @Test + @TestParameters("{expr: 'math.bitAnd(1u,2u)' , expectedResult: 0}") + @TestParameters("{expr: 'math.bitAnd(1u,3u)' , expectedResult: 1}") + public void bitAnd_unSignedInt_success(String expr, UnsignedLong expectedResult) + throws Exception { + CelAbstractSyntaxTree ast = cel.compile(expr).getAst(); + + Object result = cel.createProgram(ast).eval(); + + assertThat(result).isEqualTo(expectedResult); + } + + @Test + @TestParameters("{expr: 'math.bitAnd()'}") + @TestParameters("{expr: 'math.bitAnd(1u, 1)'}") + @TestParameters("{expr: 'math.bitAnd(1)'}") + public void bitAnd_invalidArgs_throwsException(String expr) { + CelValidationException e = + assertThrows(CelValidationException.class, () -> cel.compile(expr).getAst()); + + assertThat(e).hasMessageThat().contains("found no matching overload for 'math.bitAnd'"); + } + + @Test + public void bitAnd_maxValArg_throwsException() { + CelValidationException e = + assertThrows( + CelValidationException.class, + () -> cel.compile("math.bitAnd(9223372036854775807,9223372036854775809)").getAst()); + + assertThat(e) + .hasMessageThat() + .contains("ERROR: :1:33: For input string: \"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.compile(expr).getAst(); + + Object result = cel.createProgram(ast).eval(); + + assertThat(result).isEqualTo(expectedResult); + } + + @Test + @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.compile(expr).getAst(); + + Object result = cel.createProgram(ast).eval(); + + assertThat(result).isEqualTo(expectedResult); + } + + @Test + @TestParameters("{expr: 'math.bitOr()'}") + @TestParameters("{expr: 'math.bitOr(1u, 1)'}") + @TestParameters("{expr: 'math.bitOr(1)'}") + public void bitOr_invalidArgs_throwsException(String expr) { + CelValidationException e = + assertThrows(CelValidationException.class, () -> cel.compile(expr).getAst()); + + assertThat(e).hasMessageThat().contains("found no matching overload for 'math.bitOr'"); + } + + @Test + @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.compile(expr).getAst(); + + Object result = cel.createProgram(ast).eval(); + + assertThat(result).isEqualTo(expectedResult); + } + + @Test + @TestParameters("{expr: 'math.bitXor(1u, 3u)' , expectedResult: 2}") + @TestParameters("{expr: 'math.bitXor(3u, 5u)' , expectedResult: 6}") + public void bitXor_unSignedInt_success(String expr, UnsignedLong expectedResult) + throws Exception { + CelAbstractSyntaxTree ast = cel.compile(expr).getAst(); + + Object result = cel.createProgram(ast).eval(); + + assertThat(result).isEqualTo(expectedResult); + } + + @Test + @TestParameters("{expr: 'math.bitXor()'}") + @TestParameters("{expr: 'math.bitXor(1u, 1)'}") + @TestParameters("{expr: 'math.bitXor(1)'}") + public void bitXor_invalidArgs_throwsException(String expr) { + CelValidationException e = + assertThrows(CelValidationException.class, () -> cel.compile(expr).getAst()); + + assertThat(e).hasMessageThat().contains("found no matching overload for 'math.bitXor'"); + } + + @Test + @TestParameters("{expr: 'math.bitNot(1)' , expectedResult: -2}") + @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.compile(expr).getAst(); + + Object result = cel.createProgram(ast).eval(); + + assertThat(result).isEqualTo(expectedResult); + } + + @Test + @TestParameters("{expr: 'math.bitNot(1u)' , expectedResult: 18446744073709551614}") + @TestParameters("{expr: 'math.bitNot(12310u)' , expectedResult: 18446744073709539305}") + public void bitNot_unSignedInt_success(String expr, UnsignedLong expectedResult) + throws Exception { + CelAbstractSyntaxTree ast = cel.compile(expr).getAst(); + + Object result = cel.createProgram(ast).eval(); + + assertThat(result).isEqualTo(expectedResult); + } + + @Test + @TestParameters("{expr: 'math.bitNot()'}") + @TestParameters("{expr: 'math.bitNot(1u, 1)'}") + @TestParameters("{expr: 'math.bitNot(\"\")'}") + public void bitNot_invalidArgs_throwsException(String expr) { + CelValidationException e = + assertThrows(CelValidationException.class, () -> cel.compile(expr).getAst()); + + assertThat(e).hasMessageThat().contains("found no matching overload for 'math.bitNot'"); + } + + @Test + @TestParameters("{expr: 'math.bitShiftLeft(1, 2)' , expectedResult: 4}") + @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.compile(expr).getAst(); + + Object result = cel.createProgram(ast).eval(); + + assertThat(result).isEqualTo(expectedResult); + } + + @Test + @TestParameters("{expr: 'math.bitShiftLeft(1u, 2)' , expectedResult: 4}") + @TestParameters("{expr: 'math.bitShiftLeft(2147483648u, 22)' , expectedResult: 9007199254740992}") + @TestParameters("{expr: 'math.bitShiftLeft(1u, 65)' , expectedResult: 0}") + public void bitShiftLeft_unSignedInt_success(String expr, UnsignedLong expectedResult) + throws Exception { + CelAbstractSyntaxTree ast = cel.compile(expr).getAst(); + + Object result = cel.createProgram(ast).eval(); + + assertThat(result).isEqualTo(expectedResult); + } + + @Test + @TestParameters("{expr: 'math.bitShiftLeft(1, -2)'}") + @TestParameters("{expr: 'math.bitShiftLeft(1u, -2)'}") + public void bitShiftLeft_invalidArgs_throwsException(String expr) throws Exception { + CelAbstractSyntaxTree ast = cel.compile(expr).getAst(); + + CelEvaluationException e = + assertThrows(CelEvaluationException.class, () -> cel.createProgram(ast).eval()); + + assertThat(e).hasMessageThat().contains("evaluation error"); + assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class); + assertThat(e).hasCauseThat().hasMessageThat().contains("math.bitShiftLeft() negative offset"); + } + + @Test + @TestParameters( + "{expr: 'math.bitShiftRight(9223372036854775807, 12)' , expectedResult: 2251799813685247}") + @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.compile(expr).getAst(); + + Object result = cel.createProgram(ast).eval(); + + assertThat(result).isEqualTo(expectedResult); + } + + @Test + @TestParameters("{expr: 'math.bitShiftRight(23111u, 12)' , expectedResult: 5}") + @TestParameters("{expr: 'math.bitShiftRight(2147483648u, 22)' , expectedResult: 512}") + @TestParameters("{expr: 'math.bitShiftRight(1u, 65)' , expectedResult: 0}") + public void bitShiftRight_unSignedInt_success(String expr, UnsignedLong expectedResult) + throws Exception { + Object result = eval(expr); + + assertThat(result).isEqualTo(expectedResult); + } + + @Test + @TestParameters("{expr: 'math.bitShiftRight(23111u, -212)'}") + @TestParameters("{expr: 'math.bitShiftRight(23, -212)'}") + public void bitShiftRight_invalidArgs_throwsException(String expr) throws Exception { + CelEvaluationException e = assertThrows(CelEvaluationException.class, () -> eval(expr)); + + assertThat(e).hasMessageThat().contains("evaluation error"); + assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class); + assertThat(e).hasCauseThat().hasMessageThat().contains("math.bitShiftRight() negative offset"); + } + + @Test + @TestParameters("{expr: 'math.sqrt(49.0)', expectedResult: 7.0}") + @TestParameters("{expr: 'math.sqrt(82)', expectedResult: 9.055385138137417}") + @TestParameters("{expr: 'math.sqrt(25u)', expectedResult: 5.0}") + @TestParameters("{expr: 'math.sqrt(0.0/0.0)', expectedResult: NaN}") + @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 { + 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 8cb7a11f7..2ba12910f 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java @@ -20,11 +20,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.primitives.UnsignedLong; -import com.google.protobuf.ByteString; import com.google.protobuf.DoubleValue; -import com.google.protobuf.NullValue; -import com.google.protobuf.util.Durations; -import com.google.protobuf.util.Timestamps; import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; import com.google.testing.junit.testparameterinjector.TestParameters; @@ -32,22 +28,35 @@ 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; import dev.cel.common.CelOptions; import dev.cel.common.CelOverloadDecl; import dev.cel.common.CelValidationException; +import dev.cel.common.CelVarDecl; 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.StructTypeReference; +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.CelRuntime.CelFunctionBinding; -import dev.cel.testing.testdata.proto2.TestAllTypesProto.TestAllTypes; -import dev.cel.testing.testdata.proto2.TestAllTypesProto.TestAllTypes.NestedMessage; +import dev.cel.runtime.InterpreterUtil; +import dev.cel.runtime.PartialVars; +import java.time.Duration; +import java.time.Instant; import java.util.List; import java.util.Map; import java.util.Optional; @@ -55,9 +64,17 @@ import org.junit.runner.RunWith; @RunWith(TestParameterInjector.class) -@SuppressWarnings("unchecked") +@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), @@ -65,13 +82,13 @@ private enum ConstantTestCases { DOUBLE("5.2", "0.0", SimpleType.DOUBLE, 5.2d), UINT("5u", "0u", SimpleType.UINT, UnsignedLong.valueOf(5)), BOOL("true", "false", SimpleType.BOOL, true), - BYTES("b'abc'", "b''", SimpleType.BYTES, ByteString.copyFromUtf8("abc")), - DURATION("duration('180s')", "duration('0s')", SimpleType.DURATION, Durations.fromSeconds(180)), + BYTES("b'abc'", "b''", SimpleType.BYTES, CelByteString.copyFromUtf8("abc")), + DURATION("duration('180s')", "duration('0s')", SimpleType.DURATION, Duration.ofMinutes(3)), TIMESTAMP( "timestamp(1685552643)", "timestamp(0)", SimpleType.TIMESTAMP, - Timestamps.fromSeconds(1685552643)); + Instant.ofEpochSecond(1685552643)); private final String sourceWithNonZeroValue; private final String sourceWithZeroValue; @@ -87,15 +104,98 @@ private enum ConstantTestCases { } } - private static CelBuilder newCelBuilder() { - return CelFactory.standardCelBuilder() - .setOptions( - CelOptions.current().enableUnsignedLongs(true).enableTimestampEpoch(true).build()) + private CelBuilder newCelBuilder() { + return newCelBuilder(Integer.MAX_VALUE); + } + + 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("dev.cel.testing.testdata.proto2") + .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) .addMessageTypes(TestAllTypes.getDescriptor()) - .addRuntimeLibraries(CelOptionalLibrary.INSTANCE) - .addCompilerLibraries(CelOptionalLibrary.INSTANCE); + .addRuntimeLibraries(CelExtensions.optional(version)) + .addCompilerLibraries(CelExtensions.optional(version)); + } + + @Test + public void library() { + CelExtensionLibrary library = + CelExtensions.getExtensionLibrary("optional", CelOptions.DEFAULT); + assertThat(library.name()).isEqualTo("optional"); + assertThat(library.latest().version()).isEqualTo(2); + + // Version 0 + assertThat(library.version(0).functions().stream().map(CelFunctionDecl::name)) + .containsExactly( + "optional.of", + "optional.ofNonZeroValue", + "optional.none", + "value", + "hasValue", + "optional.unwrap", + "or", + "orValue", + "_?._", + "_[?_]", + "_[_]"); + assertThat(library.version(0).macros().stream().map(CelMacro::getFunction)) + .containsExactly("optMap"); + assertThat(library.version(0).variables().stream().map(CelVarDecl::name)) + .containsExactly("optional_type"); + + // Version 1 + assertThat(library.version(1).functions().stream().map(CelFunctionDecl::name)) + .containsExactly( + "optional.of", + "optional.ofNonZeroValue", + "optional.none", + "value", + "hasValue", + "optional.unwrap", + "or", + "orValue", + "_?._", + "_[?_]", + "_[_]"); + assertThat(library.version(1).macros().stream().map(CelMacro::getFunction)) + .containsExactly("optMap", "optFlatMap"); + assertThat(library.version(1).variables().stream().map(CelVarDecl::name)) + .containsExactly("optional_type"); + + // Version 2 + assertThat(library.version(2).functions().stream().map(CelFunctionDecl::name)) + .containsExactly( + "optional.of", + "optional.ofNonZeroValue", + "optional.none", + "value", + "hasValue", + "optional.unwrap", + "or", + "orValue", + "_?._", + "_[?_]", + "_[_]", + "first", + "last"); + assertThat(library.version(2).macros().stream().map(CelMacro::getFunction)) + .containsExactly("optMap", "optFlatMap"); + assertThat(library.version(2).variables().stream().map(CelVarDecl::name)) + .containsExactly("optional_type"); } @Test @@ -103,7 +203,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(); @@ -120,7 +220,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) @@ -138,7 +238,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))); @@ -149,7 +249,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))); @@ -161,7 +261,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(); @@ -174,7 +274,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(); @@ -188,7 +288,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(); @@ -200,7 +300,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(); @@ -211,7 +311,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(); @@ -222,7 +322,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(); @@ -234,7 +334,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(); @@ -244,7 +344,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()); } @@ -255,7 +355,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(); @@ -268,7 +368,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(); @@ -283,7 +383,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))); @@ -303,15 +403,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))); @@ -320,22 +423,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 @@ -346,7 +444,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(); @@ -363,15 +461,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))); @@ -384,7 +485,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(); @@ -397,32 +498,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 @@ -430,7 +528,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(); @@ -449,7 +547,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) @@ -472,7 +570,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( @@ -489,7 +587,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(); @@ -499,13 +597,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 = @@ -513,7 +621,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) @@ -522,6 +630,30 @@ public void optionalFieldSelection_onProtoMessage_returnsOptionalEmpty() throws assertThat(result).isEmpty(); } + @Test + public void optionalFieldSelection_onProtoMessage_chained_returnsSinglyWrappedOptional() + throws Exception { + Cel cel = + newCelBuilder() + .setResultType(OptionalType.create(SimpleType.INT)) + .addVar( + "msg", StructTypeReference.create(NestedTestAllTypes.getDescriptor().getFullName())) + .build(); + CelAbstractSyntaxTree ast = compile(cel, "msg.?payload.?single_int32"); + + Optional result = + (Optional) + cel.createProgram(ast) + .eval( + ImmutableMap.of( + "msg", + NestedTestAllTypes.newBuilder() + .setPayload(TestAllTypes.newBuilder().setSingleInt32(5).build()) + .build())); + + assertThat(result).hasValue(5L); + } + @Test public void optionalFieldSelection_onProtoMessage_returnsOptionalValue() throws Exception { Cel cel = @@ -529,7 +661,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) @@ -539,6 +671,30 @@ public void optionalFieldSelection_onProtoMessage_returnsOptionalValue() throws assertThat(result).hasValue(5L); } + @Test + public void optionalFieldSelection_onProtoMessage_listValue() throws Exception { + Cel cel = newCelBuilder().build(); + CelAbstractSyntaxTree ast = + compile( + cel, "optional.of(TestAllTypes{repeated_string: ['foo']}).?repeated_string.value()"); + + List result = (List) cel.createProgram(ast).eval(); + + assertThat(result).containsExactly("foo"); + } + + @Test + public void optionalFieldSelection_onProtoMessage_indexValue() throws Exception { + Cel cel = newCelBuilder().build(); + CelAbstractSyntaxTree ast = + compile( + cel, "optional.of(TestAllTypes{repeated_string: ['foo']}).?repeated_string[0].value()"); + + String result = (String) cel.createProgram(ast).eval(); + + assertThat(result).isEqualTo("foo"); + } + @Test public void optionalFieldSelection_onProtoMessage_chainedSuccess() throws Exception { Cel cel = @@ -553,7 +709,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) @@ -571,7 +727,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)) @@ -579,8 +738,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'"); } @@ -593,8 +751,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) @@ -615,7 +772,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(); @@ -632,7 +789,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) @@ -652,7 +809,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())); @@ -670,7 +827,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) @@ -695,7 +852,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) @@ -718,7 +875,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) @@ -737,7 +894,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()))); @@ -755,7 +912,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) @@ -766,6 +923,40 @@ public void optionalIndex_onMap_returnsOptionalValue() throws Exception { assertThat(result).isEqualTo(Optional.of("goodbye")); } + @Test + @TestParameters("{source: '{?x: optional.of(1)}'}") + @TestParameters("{source: '{?1: x}'}") + @TestParameters("{source: '{?x: x}'}") + public void optionalIndex_onMapWithUnknownInput_returnsUnknownResult(String source) + throws Exception { + Cel cel = newCelBuilder().addVar("x", OptionalType.create(SimpleType.INT)).build(); + CelAbstractSyntaxTree ast = compile(cel, source); + + Object result = + cel.createProgram(ast) + .eval(PartialVars.of(CelAttributePattern.fromQualifiedIdentifier("x"))); + + assertThat(InterpreterUtil.isUnknown(result)).isTrue(); + } + + @Test + public void optionalIndex_onOptionalMapUsingFieldSelection_returnsOptionalValue() + throws Exception { + Cel cel = + newCelBuilder() + .addVar( + "m", + MapType.create( + SimpleType.STRING, MapType.create(SimpleType.STRING, SimpleType.STRING))) + .setResultType(OptionalType.create(SimpleType.STRING)) + .build(); + CelAbstractSyntaxTree ast = compile(cel, "{?'key': optional.of('test')}.?key"); + + Object result = cel.createProgram(ast).eval(); + + assertThat(result).isEqualTo(Optional.of("test")); + } + @Test public void optionalIndex_onList_returnsOptionalEmpty() throws Exception { Cel cel = @@ -773,7 +964,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()); @@ -786,7 +977,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"))); @@ -800,7 +991,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()); @@ -815,7 +1006,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) @@ -824,6 +1015,22 @@ public void optionalIndex_onOptionalList_returnsOptionalValue() throws Exception assertThat(result).isEqualTo(Optional.of("hello")); } + @Test + public void optionalIndex_onListWithUnknownInput_returnsUnknownResult() throws Exception { + Cel cel = + newCelBuilder() + .addVar("x", OptionalType.create(SimpleType.INT)) + .setResultType(ListType.create(SimpleType.INT)) + .build(); + CelAbstractSyntaxTree ast = compile(cel, "[?x]"); + + Object result = + cel.createProgram(ast) + .eval(PartialVars.of(CelAttributePattern.fromQualifiedIdentifier("x"))); + + assertThat(InterpreterUtil.isUnknown(result)).isTrue(); + } + @Test public void traditionalIndex_onOptionalList_returnsOptionalEmpty() throws Exception { Cel cel = @@ -831,13 +1038,77 @@ 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(InterpreterUtil.isUnknown(result)).isTrue(); + } + + @Test + // LHS + @TestParameters("{expression: 'optx.or(optional.of(1))'}") + @TestParameters("{expression: 'optx.orValue(1)'}") + // RHS + @TestParameters("{expression: 'optional.none().or(optx)'}") + @TestParameters("{expression: 'optional.none().orValue(optx)'}") + public void optionalChainedFunctions_lhsIsUnknown_returnsUnknown(String expression) + throws Exception { + Cel cel = + newCelBuilder() + .addVar("optx", OptionalType.create(SimpleType.INT)) + .addVar("x", SimpleType.INT) + .build(); + CelAbstractSyntaxTree ast = compile(cel, expression); + + Object result = + cel.createProgram(ast) + .eval(PartialVars.of(CelAttributePattern.fromQualifiedIdentifier("optx"))); + + assertThat(InterpreterUtil.isUnknown(result)).isTrue(); + } + + @Test + // LHS + @TestParameters("{expression: 'optional.of(1/0).or(optional.of(1))'}") + @TestParameters("{expression: 'optional.of(1/0).orValue(1)'}") + // RHS + @TestParameters("{expression: 'optional.none().or(optional.of(1/0))'}") + @TestParameters("{expression: 'optional.none().orValue(1/0)'}") + public void optionalChainedFunctions_lhsIsError_returnsError(String expression) throws Exception { + Cel cel = + newCelBuilder() + .addVar("optx", OptionalType.create(SimpleType.INT)) + .addVar("x", SimpleType.INT) + .build(); + CelAbstractSyntaxTree ast = compile(cel, expression); + + assertThrows(CelEvaluationException.class, () -> cel.createProgram(ast).eval()); + } + @Test public void traditionalIndex_onOptionalList_returnsOptionalValue() throws Exception { Cel cel = @@ -845,7 +1116,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) @@ -868,7 +1139,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) @@ -896,7 +1167,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) @@ -922,8 +1193,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) @@ -941,7 +1211,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(); @@ -951,7 +1221,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(); @@ -973,7 +1243,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>>) @@ -1002,8 +1272,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())); @@ -1011,11 +1280,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() @@ -1029,7 +1301,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(); @@ -1040,7 +1312,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(); @@ -1065,7 +1337,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())); @@ -1082,8 +1354,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) @@ -1105,7 +1376,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) @@ -1127,7 +1398,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) @@ -1152,7 +1423,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) @@ -1174,7 +1445,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()) @@ -1187,7 +1461,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'"); } @@ -1204,7 +1478,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) @@ -1230,7 +1504,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())); @@ -1245,7 +1519,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))); @@ -1262,8 +1536,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"); } @@ -1275,7 +1548,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())); @@ -1290,7 +1563,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))); @@ -1306,8 +1579,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))); @@ -1323,8 +1595,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))); @@ -1333,15 +1604,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 '_?_:_'"); } @@ -1356,10 +1630,119 @@ 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() .contains("optFlatMap() variable name must be a simple identifier"); } + + @Test + public void optionalType_typeResolution() throws Exception { + Cel cel = newCelBuilder().build(); + CelAbstractSyntaxTree ast = compile(cel, "optional_type"); + + TypeType optionalRuntimeType = (TypeType) cel.createProgram(ast).eval(); + + assertThat(optionalRuntimeType.name()).isEqualTo("type"); + assertThat(optionalRuntimeType.containingTypeName()).isEqualTo("optional_type"); + } + + @Test + public void optionalType_typeComparison() throws Exception { + Cel cel = newCelBuilder().build(); + + CelAbstractSyntaxTree ast = compile(cel, "type(optional.none()) == optional_type"); + + assertThat(cel.createProgram(ast).eval()).isEqualTo(true); + } + + @Test + @TestParameters("{expression: '[].first().hasValue() == false'}") + @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(compile(cel, expression)).eval(); + assertThat(result).isTrue(); + } + + @Test + @TestParameters("{expression: '[].last().hasValue() == false'}") + @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(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_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(); + assertThat( + assertThrows( + CelValidationException.class, + () -> { + cel.createProgram(compile(cel, expression)).eval(); + })) + .hasMessageThat() + .contains(expectedError); + } + + @Test + public void optionalMapCreation_mapKeySetOnNonOptional_throws() { + String expression = "{?1: dyn(\"one\")}"; + Cel cel = newCelBuilder().build(); + + CelEvaluationException e = + assertThrows( + CelEvaluationException.class, () -> cel.createProgram(compile(cel, expression)).eval()); + assertThat(e) + .hasMessageThat() + .contains("Cannot initialize optional entry '1' from non-optional value one"); + } + + @Test + public void optionalListCreation_listKeySetOnNonOptional_throws() { + String expression = "[?dyn(1)]"; + Cel cel = newCelBuilder().build(); + + CelEvaluationException e = + assertThrows( + CelEvaluationException.class, () -> cel.createProgram(compile(cel, expression)).eval()); + assertThat(e) + .hasMessageThat() + .contains("Cannot initialize optional list element from non-optional value 1"); + } + + @Test + public void optionalMessageCreation_fieldKeySetOnNonOptional_throws() { + String expression = "TestAllTypes{?single_double_wrapper: dyn('foo')}"; + Cel cel = newCelBuilder().build(); + + CelEvaluationException e = + assertThrows( + CelEvaluationException.class, () -> cel.createProgram(compile(cel, expression)).eval()); + assertThat(e) + .hasMessageThat() + .contains( + "Cannot initialize optional entry 'single_double_wrapper' from non-optional value foo"); + } + + private CelAbstractSyntaxTree compile(CelCompiler compiler, String expression) + throws CelValidationException { + CelAbstractSyntaxTree ast = compiler.parse(expression).getAst(); + if (testMode.equals(TestMode.PLANNER_PARSE_ONLY)) { + return ast; + } + + return compiler.check(ast).getAst(); + } } diff --git a/extensions/src/test/java/dev/cel/extensions/CelProtoExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelProtoExtensionsTest.java index d7b66cb1d..f46ea5b1a 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelProtoExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelProtoExtensionsTest.java @@ -26,81 +26,80 @@ 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; import dev.cel.common.CelOverloadDecl; 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; +import dev.cel.expr.conformance.proto2.TestAllTypesExtensions; +import dev.cel.parser.CelMacro; import dev.cel.parser.CelStandardMacro; -import dev.cel.runtime.CelRuntime; -import dev.cel.runtime.CelRuntime.CelFunctionBinding; -import dev.cel.runtime.CelRuntimeFactory; -import dev.cel.testing.testdata.proto2.MessagesProto2Extensions; -import dev.cel.testing.testdata.proto2.NestedMessageInsideExtensions; -import dev.cel.testing.testdata.proto2.Proto2ExtensionScopedMessage; -import dev.cel.testing.testdata.proto2.Proto2Message; -import dev.cel.testing.testdata.proto2.StringHolder; -import dev.cel.testing.testdata.proto2.TestAllTypesProto.TestAllTypes.NestedEnum; +import dev.cel.runtime.CelFunctionBinding; +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(MessagesProto2Extensions.getDescriptor()) - .addVar( - "msg", StructTypeReference.create("dev.cel.testing.testdata.proto2.Proto2Message")) - .setContainer("dev.cel.testing.testdata.proto2") - .build(); - - private static final CelRuntime CEL_RUNTIME = - CelRuntimeFactory.standardCelRuntimeBuilder() - .addFileTypes(MessagesProto2Extensions.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 Proto2Message PACKAGE_SCOPED_EXT_MSG = - Proto2Message.newBuilder() - .setExtension(MessagesProto2Extensions.int32Ext, 1) + private static final TestAllTypes PACKAGE_SCOPED_EXT_MSG = + TestAllTypes.newBuilder() + .setExtension(TestAllTypesExtensions.int32Ext, 1) .setExtension( - MessagesProto2Extensions.nestedExt, - Proto2Message.newBuilder().setSingleInt32(5).build()) - .setExtension(MessagesProto2Extensions.nestedEnumExt, NestedEnum.BAR) + TestAllTypesExtensions.nestedExt, TestAllTypes.newBuilder().setSingleInt32(5).build()) + .setExtension(TestAllTypesExtensions.nestedEnumExt, NestedEnum.BAR) .setExtension( - MessagesProto2Extensions.repeatedStringHolderExt, + TestAllTypesExtensions.repeatedTestAllTypes, ImmutableList.of( - StringHolder.newBuilder().setS("A").build(), - StringHolder.newBuilder().setS("B").build())) + TestAllTypes.newBuilder().setSingleString("A").build(), + TestAllTypes.newBuilder().setSingleString("B").build())) .build(); - private static final Proto2Message MESSAGE_SCOPED_EXT_MSG = - Proto2Message.newBuilder() + private static final TestAllTypes MESSAGE_SCOPED_EXT_MSG = + TestAllTypes.newBuilder() .setExtension( - Proto2ExtensionScopedMessage.nestedMessageInsideExt, - NestedMessageInsideExtensions.newBuilder().setField("test").build()) + Proto2ExtensionScopedMessage.messageScopedNestedExt, + TestAllTypes.newBuilder().setSingleString("test").build()) .setExtension(Proto2ExtensionScopedMessage.int64Ext, 1L) .build(); @Test - @TestParameters("{expr: 'proto.hasExt(msg, dev.cel.testing.testdata.proto2.int32_ext)'}") - @TestParameters("{expr: 'proto.hasExt(msg, dev.cel.testing.testdata.proto2.nested_ext)'}") - @TestParameters("{expr: 'proto.hasExt(msg, dev.cel.testing.testdata.proto2.nested_enum_ext)'}") - @TestParameters( - "{expr: 'proto.hasExt(msg, dev.cel.testing.testdata.proto2.repeated_string_holder_ext)'}") + public void library() { + CelExtensionLibrary library = + CelExtensions.getExtensionLibrary("protos", CelOptions.DEFAULT); + assertThat(library.name()).isEqualTo("protos"); + assertThat(library.latest().version()).isEqualTo(0); + assertThat(library.version(0).functions()).isEmpty(); + assertThat(library.version(0).macros().stream().map(CelMacro::getFunction)) + .containsExactly("hasExt", "getExt"); + } + + @Test + @TestParameters("{expr: 'proto.hasExt(msg, cel.expr.conformance.proto2.int32_ext)'}") + @TestParameters("{expr: 'proto.hasExt(msg, cel.expr.conformance.proto2.nested_ext)'}") + @TestParameters("{expr: 'proto.hasExt(msg, cel.expr.conformance.proto2.nested_enum_ext)'}") @TestParameters( - "{expr: '!proto.hasExt(msg, dev.cel.testing.testdata.proto2.test_all_types_ext)'}") + "{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(); } @@ -108,34 +107,32 @@ public void hasExt_packageScoped_success(String expr) throws Exception { @Test @TestParameters( "{expr: 'proto.hasExt(msg," - + " dev.cel.testing.testdata.proto2.Proto2ExtensionScopedMessage.nested_message_inside_ext)'}") + + " cel.expr.conformance.proto2.Proto2ExtensionScopedMessage.message_scoped_nested_ext)'}") @TestParameters( "{expr: 'proto.hasExt(msg," - + " dev.cel.testing.testdata.proto2.Proto2ExtensionScopedMessage.int64_ext)'}") + + " cel.expr.conformance.proto2.Proto2ExtensionScopedMessage.int64_ext)'}") @TestParameters( "{expr: '!proto.hasExt(msg," - + " dev.cel.testing.testdata.proto2.Proto2ExtensionScopedMessage.message_scoped_nested_ext)'}") + + " cel.expr.conformance.proto2.Proto2ExtensionScopedMessage.message_scoped_repeated_test_all_types)'}") @TestParameters( "{expr: '!proto.hasExt(msg," - + " dev.cel.testing.testdata.proto2.Proto2ExtensionScopedMessage.string_ext)'}") + + " 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(); } @Test - @TestParameters("{expr: 'msg.hasExt(''dev.cel.testing.testdata.proto2.int32_ext'', 0)'}") - @TestParameters("{expr: 'dyn(msg).hasExt(''dev.cel.testing.testdata.proto2.int32_ext'', 0)'}") + @TestParameters("{expr: 'msg.hasExt(''cel.expr.conformance.proto2.int32_ext'', 0)'}") + @TestParameters("{expr: 'dyn(msg).hasExt(''cel.expr.conformance.proto2.int32_ext'', 0)'}") public void hasExt_nonProtoNamespace_success(String expr) throws Exception { StructTypeReference proto2MessageTypeReference = - StructTypeReference.create("dev.cel.testing.testdata.proto2.Proto2Message"); - CelCompiler celCompiler = - CelCompilerFactory.standardCelCompilerBuilder() - .addLibraries(CelExtensions.protos()) + StructTypeReference.create("cel.expr.conformance.proto2.TestAllTypes"); + Cel customCel = + runtimeFlavor + .builder() + .addCompilerLibraries(CelExtensions.protos()) .addVar("msg", proto2MessageTypeReference) .addFunctionDeclarations( CelFunctionDecl.newFunctionDeclaration( @@ -145,60 +142,55 @@ 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(Proto2Message.class, String.class, Long.class), - (arg) -> { - Proto2Message msg = (Proto2Message) 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, dev.cel.testing.testdata.proto2.undefined_field)") + cel.compile("!proto.hasExt(msg, cel.expr.conformance.proto2.undefined_field)") .getAst()); assertThat(exception) .hasMessageThat() - .contains("undefined field 'dev.cel.testing.testdata.proto2.undefined_field'"); + .contains("undefined field 'cel.expr.conformance.proto2.undefined_field'"); } @Test - @TestParameters("{expr: 'proto.getExt(msg, dev.cel.testing.testdata.proto2.int32_ext) == 1'}") + @TestParameters("{expr: 'proto.getExt(msg, cel.expr.conformance.proto2.int32_ext) == 1'}") @TestParameters( - "{expr: 'proto.getExt(msg, dev.cel.testing.testdata.proto2.nested_ext) ==" - + " Proto2Message{single_int32: 5}'}") + "{expr: 'proto.getExt(msg, cel.expr.conformance.proto2.nested_ext) ==" + + " TestAllTypes{single_int32: 5}'}") @TestParameters( - "{expr: 'proto.getExt(msg, dev.cel.testing.testdata.proto2.nested_enum_ext) ==" + "{expr: 'proto.getExt(msg, cel.expr.conformance.proto2.nested_enum_ext) ==" + " TestAllTypes.NestedEnum.BAR'}") @TestParameters( - "{expr: 'proto.getExt(msg, dev.cel.testing.testdata.proto2.repeated_string_holder_ext) ==" - + " [StringHolder{s: ''A''}, StringHolder{s: ''B''}]'}") + "{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(); } @@ -206,45 +198,43 @@ public void getExt_packageScoped_success(String expr) throws Exception { @Test @TestParameters( "{expr: 'proto.getExt(msg," - + " dev.cel.testing.testdata.proto2.Proto2ExtensionScopedMessage.nested_message_inside_ext)" - + " == NestedMessageInsideExtensions{field: ''test''}'}") + + " cel.expr.conformance.proto2.Proto2ExtensionScopedMessage.message_scoped_nested_ext)" + + " == TestAllTypes{single_string: ''test''}'}") @TestParameters( "{expr: 'proto.getExt(msg," - + " dev.cel.testing.testdata.proto2.Proto2ExtensionScopedMessage.int64_ext) == 1'}") + + " 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, dev.cel.testing.testdata.proto2.undefined_field)") + cel.compile("!proto.getExt(msg, cel.expr.conformance.proto2.undefined_field)") .getAst()); assertThat(exception) .hasMessageThat() - .contains("undefined field 'dev.cel.testing.testdata.proto2.undefined_field'"); + .contains("undefined field 'cel.expr.conformance.proto2.undefined_field'"); } @Test - @TestParameters("{expr: 'msg.getExt(''dev.cel.testing.testdata.proto2.int32_ext'', 0) == 1'}") - @TestParameters( - "{expr: 'dyn(msg).getExt(''dev.cel.testing.testdata.proto2.int32_ext'', 0) == 1'}") + @TestParameters("{expr: 'msg.getExt(''cel.expr.conformance.proto2.int32_ext'', 0) == 1'}") + @TestParameters("{expr: 'dyn(msg).getExt(''cel.expr.conformance.proto2.int32_ext'', 0) == 1'}") public void getExt_nonProtoNamespace_success(String expr) throws Exception { StructTypeReference proto2MessageTypeReference = - StructTypeReference.create("dev.cel.testing.testdata.proto2.Proto2Message"); - CelCompiler celCompiler = - CelCompilerFactory.standardCelCompilerBuilder() - .addLibraries(CelExtensions.protos()) + StructTypeReference.create("cel.expr.conformance.proto2.TestAllTypes"); + Cel customCel = + runtimeFlavor + .builder() + .addCompilerLibraries(CelExtensions.protos()) .addVar("msg", proto2MessageTypeReference) .addFunctionDeclarations( CelFunctionDecl.newFunctionDeclaration( @@ -254,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(Proto2Message.class, String.class, Long.class), - (arg) -> { - Proto2Message msg = (Proto2Message) 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(); } @@ -284,22 +271,25 @@ public void getExt_nonProtoNamespace_success(String expr) throws Exception { @Test public void getExt_onAnyPackedExtensionField_success() throws Exception { ExtensionRegistry extensionRegistry = ExtensionRegistry.newInstance(); - MessagesProto2Extensions.registerAllExtensions(extensionRegistry); - Cel cel = - CelFactory.standardCelBuilder() + TestAllTypesExtensions.registerAllExtensions(extensionRegistry); + Cel customCel = + runtimeFlavor + .builder() + // CEL-Internal-2 .addCompilerLibraries(CelExtensions.protos()) - .addFileTypes(MessagesProto2Extensions.getDescriptor()) + .addFileTypes(TestAllTypesExtensions.getDescriptor()) .setExtensionRegistry(extensionRegistry) - .addVar( - "msg", StructTypeReference.create("dev.cel.testing.testdata.proto2.Proto2Message")) + .addVar("msg", StructTypeReference.create("cel.expr.conformance.proto2.TestAllTypes")) .build(); - CelAbstractSyntaxTree ast = - cel.compile("proto.getExt(msg, dev.cel.testing.testdata.proto2.int32_ext)").getAst(); Any anyMsg = Any.pack( - Proto2Message.newBuilder().setExtension(MessagesProto2Extensions.int32Ext, 1).build()); - - Long result = (Long) cel.createProgram(ast).eval(ImmutableMap.of("msg", anyMsg)); + TestAllTypes.newBuilder().setExtension(TestAllTypesExtensions.int32Ext, 1).build()); + Long result = + (Long) + eval( + customCel, + "proto.getExt(msg, cel.expr.conformance.proto2.int32_ext)", + ImmutableMap.of("msg", anyMsg)); assertThat(result).isEqualTo(1); } @@ -317,10 +307,10 @@ private enum ParseErrorTestCase { + " | ...................................................^"), FIELD_INSIDE_PRESENCE_TEST( "proto.getExt(Proto2ExtensionScopedMessage{}," - + " has(dev.cel.testing.testdata.proto2.Proto2ExtensionScopedMessage.int64_ext))", + + " has(cel.expr.conformance.proto2.Proto2ExtensionScopedMessage.int64_ext))", "ERROR: :1:49: invalid extension field\n" + " | proto.getExt(Proto2ExtensionScopedMessage{}," - + " has(dev.cel.testing.testdata.proto2.Proto2ExtensionScopedMessage.int64_ext))\n" + + " has(cel.expr.conformance.proto2.Proto2ExtensionScopedMessage.int64_ext))\n" + " | ................................................^"); private final String expr; @@ -335,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 new file mode 100644 index 000000000..97d0cc90c --- /dev/null +++ b/extensions/src/test/java/dev/cel/extensions/CelRegexExtensionsTest.java @@ -0,0 +1,268 @@ +// 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.extensions; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableList; +import com.google.testing.junit.testparameterinjector.TestParameter; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import com.google.testing.junit.testparameterinjector.TestParameters; +import dev.cel.bundle.Cel; +import dev.cel.common.CelFunctionDecl; +import dev.cel.common.CelOptions; +import dev.cel.runtime.CelEvaluationException; +import java.util.Optional; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public final class CelRegexExtensionsTest extends CelExtensionTestBase { + + @Override + protected Cel newCelEnv() { + return runtimeFlavor + .builder() + .addCompilerLibraries(CelExtensions.regex()) + .addRuntimeLibraries(CelExtensions.regex()) + .build(); + } + + + @Test + public void library() { + CelExtensionLibrary library = + CelExtensions.getExtensionLibrary("regex", CelOptions.DEFAULT); + assertThat(library.name()).isEqualTo("regex"); + assertThat(library.latest().version()).isEqualTo(0); + assertThat(library.version(0).functions().stream().map(CelFunctionDecl::name)) + .containsExactly("regex.replace", "regex.extract", "regex.extractAll"); + assertThat(library.version(0).macros()).isEmpty(); + } + + @Test + @TestParameters("{target: 'abc', regex: '^', replaceStr: 'start_', res: 'start_abc'}") + @TestParameters("{target: 'abc', regex: '$', replaceStr: '_end', res: 'abc_end'}") + @TestParameters("{target: 'a-b', regex: '\\\\b', replaceStr: '|', res: '|a|-|b|'}") + @TestParameters( + "{target: 'foo bar', regex: '(fo)o (ba)r', replaceStr: '\\\\2 \\\\1', res: 'ba fo'}") + @TestParameters("{target: 'foo bar', regex: 'foo', replaceStr: '\\\\\\\\', res: '\\ bar'}") + @TestParameters("{target: 'banana', regex: 'ana', replaceStr: 'x', res: 'bxna'}") + @TestParameters("{target: 'abc', regex: 'b(.)', replaceStr: 'x\\\\1', res: 'axc'}") + @TestParameters( + "{target: 'hello world hello', regex: 'hello', replaceStr: 'hi', res: 'hi world hi'}") + @TestParameters("{target: 'ac', regex: 'a(b)?c', replaceStr: '[\\\\1]', res: '[]'}") + @TestParameters("{target: 'apple pie', regex: 'p', replaceStr: 'X', res: 'aXXle Xie'}") + @TestParameters( + "{target: 'remove all spaces', regex: '\\\\s', replaceStr: '', res: 'removeallspaces'}") + @TestParameters("{target: 'digit:99919291992', regex: '\\\\d+', replaceStr: '3', res: 'digit:3'}") + @TestParameters( + "{target: 'foo bar baz', regex: '\\\\w+', replaceStr: '(\\\\0)', res: '(foo) (bar) (baz)'}") + @TestParameters("{target: '', regex: 'a', replaceStr: 'b', res: ''}") + @TestParameters( + "{target: 'User: Alice, Age: 30', regex: 'User: (?P\\\\w+), Age: (?P\\\\d+)'," + + " replaceStr: '${name} is ${age} years old', res: '${name} is ${age} years old'}") + @TestParameters( + "{target: 'User: Alice, Age: 30', regex: 'User: (?P\\\\w+), Age: (?P\\\\d+)'," + + " replaceStr: '\\\\1 is \\\\2 years old', res: 'Alice is 30 years old'}") + @TestParameters("{target: 'hello ☃', regex: '☃', replaceStr: '❄', res: 'hello ❄'}") + 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); + assertThat(eval(expr)).isEqualTo(res); + } + + @Test + public void replace_nested_success() throws Exception { + String expr = + "regex.replace(" + + " regex.replace('%(foo) %(bar) %2','%\\\\((\\\\w+)\\\\)','${\\\\1}')," + + " '%(\\\\d+)', '$\\\\1')"; + assertThat(eval(expr)).isEqualTo("${foo} ${bar} $2"); + } + + @Test + @TestParameters("{t: 'banana', re: 'a', rep: 'x', i: 0, res: 'banana'}") + @TestParameters("{t: 'banana', re: 'a', rep: 'x', i: 1, res: 'bxnana'}") + @TestParameters("{t: 'banana', re: 'a', rep: 'x', i: 2, res: 'bxnxna'}") + @TestParameters("{t: 'banana', re: 'a', rep: 'x', i: 100, res: 'bxnxnx'}") + @TestParameters("{t: 'banana', re: 'a', rep: 'x', i: -1, res: 'bxnxnx'}") + @TestParameters("{t: 'banana', re: 'a', rep: 'x', i: -100, res: 'bxnxnx'}") + @TestParameters( + "{t: 'cat-dog dog-cat cat-dog dog-cat', re: '(cat)-(dog)', rep: '\\\\2-\\\\1', i: 1," + + " res: 'dog-cat dog-cat cat-dog dog-cat'}") + @TestParameters( + "{t: 'cat-dog dog-cat cat-dog dog-cat', re: '(cat)-(dog)', rep: '\\\\2-\\\\1', i: 2, res:" + + " 'dog-cat dog-cat dog-cat dog-cat'}") + @TestParameters("{t: 'a.b.c', re: '\\\\.', rep: '-', i: 1, res: 'a-b.c'}") + @TestParameters("{t: 'a.b.c', re: '\\\\.', rep: '-', i: -1, res: 'a-b-c'}") + 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); + assertThat(eval(expr)).isEqualTo(res); + } + + @Test + @TestParameters("{target: 'foo bar', regex: '(', replaceStr: '$2 $1'}") + @TestParameters("{target: 'foo bar', regex: '[a-z', replaceStr: '$2 $1'}") + 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); + CelEvaluationException e = + assertThrows(CelEvaluationException.class, () -> eval(expr)); + + assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class); + assertThat(e).hasCauseThat().hasMessageThat().contains("Failed to compile regex: "); + } + + @Test + public void replace_invalidCaptureGroupReplaceStr_throwsException() throws Exception { + String expr = "regex.replace('test', '(.)', '\\\\2')"; + CelEvaluationException e = + assertThrows(CelEvaluationException.class, () -> eval(expr)); + + assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class); + assertThat(e) + .hasCauseThat() + .hasMessageThat() + .contains("Replacement string references group 2 but regex has only 1 group(s)"); + } + + @Test + public void replace_trailingBackslashReplaceStr_throwsException() throws Exception { + String expr = "regex.replace('id=123', 'id=(?P\\\\d+)', '\\\\')"; + CelEvaluationException e = + assertThrows(CelEvaluationException.class, () -> eval(expr)); + + assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class); + assertThat(e) + .hasCauseThat() + .hasMessageThat() + .contains("Invalid replacement string: \\ not allowed at end"); + } + + @Test + public void replace_invalidGroupReferenceReplaceStr_throwsException() throws Exception { + String expr = "regex.replace('id=123', 'id=(?P\\\\d+)', '\\\\a')"; + CelEvaluationException e = + assertThrows(CelEvaluationException.class, () -> eval(expr)); + + assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class); + assertThat(e) + .hasCauseThat() + .hasMessageThat() + .contains("Invalid replacement string: \\ must be followed by a digit"); + } + + @Test + @TestParameters("{target: 'hello world', regex: 'hello(.*)', expectedResult: ' world'}") + @TestParameters("{target: 'item-A, item-B', regex: 'item-(\\\\w+)', expectedResult: 'A'}") + @TestParameters("{target: 'bananana', regex: 'ana', expectedResult: 'ana'}") + @TestParameters( + "{target: 'The color is red', regex: 'The color is (\\\\w+)', expectedResult: 'red'}") + @TestParameters( + "{target: 'The color is red', regex: 'The color is \\\\w+', expectedResult: 'The color is" + + " red'}") + @TestParameters( + "{target: 'phone: 415-5551212', regex: 'phone: (\\\\d{3})?', expectedResult: '415'}") + @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); + Object result = eval(expr); + + assertThat(result).isInstanceOf(Optional.class); + assertThat((Optional) result).hasValue(expectedResult); + } + + @Test + @TestParameters("{target: 'hello world', regex: 'goodbye (.*)'}") + @TestParameters("{target: 'HELLO', regex: 'hello'}") + @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); + Object result = eval(expr); + + assertThat(result).isInstanceOf(Optional.class); + assertThat((Optional) result).isEmpty(); + } + + @Test + @TestParameters("{target: 'phone: 415-5551212', regex: 'phone: ((\\\\d{3})-)?'}") + @TestParameters("{target: 'testuser@testdomain', regex: '(.*)@([^.]*)'}") + public void extract_multipleCaptureGroups_throwsException(String target, String regex) + throws Exception { + String expr = String.format("regex.extract('%s', '%s')", target, regex); + CelEvaluationException e = + assertThrows(CelEvaluationException.class, () -> eval(expr)); + + assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class); + assertThat(e) + .hasCauseThat() + .hasMessageThat() + .contains("Regular expression has more than one capturing group:"); + } + + @SuppressWarnings("ImmutableEnumChecker") // Test only + private enum ExtractAllTestCase { + NO_MATCH("regex.extractAll('id:123, id:456', 'assa')", ImmutableList.of()), + NO_CAPTURE_GROUP( + "regex.extractAll('id:123, id:456', 'id:\\\\d+')", ImmutableList.of("id:123", "id:456")), + CAPTURE_GROUP( + "regex.extractAll('key=\"\", key=\"val\"', 'key=\"([^\"]*)\"')", + ImmutableList.of("", "val")), + SINGLE_NAMED_GROUP( + "regex.extractAll('testuser@testdomain', '(?P.*)@')", + ImmutableList.of("testuser")), + SINGLE_NAMED_MULTIPLE_MATCH_GROUP( + "regex.extractAll('banananana', '(ana)')", ImmutableList.of("ana", "ana")); + private final String expr; + private final ImmutableList expectedResult; + + ExtractAllTestCase(String expr, ImmutableList expectedResult) { + this.expr = expr; + this.expectedResult = expectedResult; + } + } + + @Test + public void extractAll_success(@TestParameter ExtractAllTestCase testCase) throws Exception { + Object result = eval(testCase.expr); + + assertThat(result).isEqualTo(testCase.expectedResult); + } + + @Test + @TestParameters("{target: 'phone: 415-5551212', regex: 'phone: ((\\\\d{3})-)?'}") + @TestParameters("{target: 'testuser@testdomain', regex: '(.*)@([^.]*)'}") + @TestParameters( + "{target: 'Name: John Doe, Age:321', regex: 'Name: (?P.*), Age:(?P\\\\d+)'}") + @TestParameters( + "{target: 'The user testuser belongs to testdomain', regex: 'The (user|domain)" + + " (?P.*) belongs (to) (?P.*)'}") + public void extractAll_multipleCaptureGroups_throwsException(String target, String regex) + throws Exception { + String expr = String.format("regex.extractAll('%s', '%s')", target, regex); + CelEvaluationException e = + assertThrows(CelEvaluationException.class, () -> eval(expr)); + + assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class); + assertThat(e) + .hasCauseThat() + .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 new file mode 100644 index 000000000..091d456f5 --- /dev/null +++ b/extensions/src/test/java/dev/cel/extensions/CelSetsExtensionsTest.java @@ -0,0 +1,465 @@ +// 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. + +package dev.cel.extensions; + +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 com.google.testing.junit.testparameterinjector.TestParameters; +import dev.cel.bundle.Cel; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelContainer; +import dev.cel.common.CelFunctionDecl; +import dev.cel.common.CelOptions; +import dev.cel.common.CelOverloadDecl; +import dev.cel.common.CelValidationException; +import dev.cel.common.CelValidationResult; +import dev.cel.common.types.ListType; +import dev.cel.common.types.SimpleType; +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.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 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() { + CelExtensionLibrary library = + CelExtensions.getExtensionLibrary("sets", CelOptions.DEFAULT); + assertThat(library.name()).isEqualTo("sets"); + assertThat(library.latest().version()).isEqualTo(0); + assertThat(library.version(0).functions().stream().map(CelFunctionDecl::name)) + .containsExactly("sets.contains", "sets.equivalent", "sets.intersects"); + assertThat(library.version(0).macros()).isEmpty(); + } + + @Test + public void contains_integerListWithSameValue_succeeds() throws Exception { + ImmutableList list = ImmutableList.of(1, 2, 3, 4); + ImmutableList subList = ImmutableList.of(1, 2, 3, 4); + assertThat( + eval("sets.contains(list, subList)", ImmutableMap.of("list", list, "subList", subList))) + .isEqualTo(true); + } + + @Test + public void contains_integerListAsExpression_succeeds() throws Exception { + assertThat(eval("sets.contains([1, 1], [1])")).isEqualTo(true); + } + + @Test + @TestParameters( + "{expression: 'sets.contains([TestAllTypes{}], [TestAllTypes{}])', expected: true}") + @TestParameters( + "{expression: 'sets.contains([TestAllTypes{single_int64: 1, single_uint64: 2u}]," + + " [TestAllTypes{single_int64: 1, single_uint64: 2u}])', expected: true}") + @TestParameters( + "{expression: 'sets.contains([TestAllTypes{single_any: [1.0, 2u, 3]}]," + + " [TestAllTypes{single_any: [1u, 2, 3.0]}])', expected: true}") + @TestParameters( + "{expression: 'sets.contains([TestAllTypes{single_int64: 1, single_uint64: 2u}]," + + " [TestAllTypes{single_int64: 2, single_uint64: 3u}])', expected: false}") + public void contains_withProtoMessage_succeeds(String expression, boolean expected) + throws Exception { + assertThat(eval(expression)).isEqualTo(expected); + } + + @Test + @TestParameters("{expression: 'sets.contains([new_int(1)], [1])', expected: true}") + @TestParameters("{expression: 'sets.contains([new_int(1)], [1.0, 1u])', expected: true}") + @TestParameters("{expression: 'sets.contains([new_int(2)], [1])', expected: false}") + public void contains_withFunctionReturningInteger_succeeds(String expression, boolean expected) + throws Exception { + assertThat(eval(expression)).isEqualTo(expected); + } + + @Test + @TestParameters("{list: [1, 2, 3, 4], subList: [1, 2, 3, 4], expected: true}") + @TestParameters("{list: [5, 4, 3, 2, 1], subList: [1, 2, 3], expected: true}") + @TestParameters("{list: [5, 4, 3, 2, 1], subList: [1, 1, 1, 1, 1], expected: true}") + @TestParameters("{list: [], subList: [], expected: true}") + @TestParameters("{list: [1], subList: [], expected: true}") + @TestParameters("{list: [], subList: [1], expected: false}") + @TestParameters("{list: [1], subList: [1], expected: true}") + @TestParameters("{list: [1], subList: [1, 1], expected: true}") + @TestParameters("{list: [1, 1], subList: [1, 1], expected: true}") + @TestParameters("{list: [2, 1], subList: [1], expected: true}") + @TestParameters("{list: [1, 2, 3, 4], subList: [2, 3], expected: true}") + @TestParameters("{list: [1], subList: [2], expected: false}") + @TestParameters("{list: [1], subList: [1, 2], expected: false}") + public void contains_withIntTypes_succeeds( + List list, List subList, boolean expected) throws Exception { + assertThat( + eval("sets.contains(list, subList)", ImmutableMap.of("list", list, "subList", subList))) + .isEqualTo(expected); + } + + @Test + @TestParameters("{list: [1.0], subList: [1.0, 1.0], expected: true}") + @TestParameters("{list: [1.0, 1.00], subList: [1], expected: true}") + @TestParameters("{list: [1.0], subList: [1.00], expected: true}") + @TestParameters("{list: [1.414], subList: [], expected: true}") + @TestParameters("{list: [], subList: [1.414], expected: false}") + @TestParameters("{list: [3.14, 2.71], subList: [2.71], expected: true}") + @TestParameters("{list: [3.9], subList: [3.1], expected: false}") + @TestParameters("{list: [3.2], subList: [3.1], expected: false}") + @TestParameters("{list: [2, 3.0], subList: [2, 3], expected: true}") + public void contains_withDoubleTypes_succeeds( + List list, List subList, boolean expected) throws Exception { + assertThat( + eval("sets.contains(list, subList)", ImmutableMap.of("list", list, "subList", subList))) + .isEqualTo(expected); + } + + @Test + @TestParameters("{expression: 'sets.contains([[1], [2, 3]], [[2, 3]])', expected: true}") + @TestParameters("{expression: 'sets.contains([[1], [2], [3]], [[2, 3]])', expected: false}") + @TestParameters( + "{expression: 'sets.contains([[1, 2], [2, 3]], [[1], [2, 3.0]])', expected: false}") + @TestParameters("{expression: 'sets.contains([[1], [2, 3.0]], [[2, 3]])', expected: true}") + public void contains_withNestedLists_succeeds(String expression, boolean expected) + throws Exception { + assertThat(eval(expression)).isEqualTo(expected); + } + + @Test + @TestParameters("{expression: 'sets.contains([1, \"1\"], [1])', expected: true}") + @TestParameters("{expression: 'sets.contains([1], [1, \"1\"])', expected: false}") + public void contains_withMixingIntAndString_succeeds(String expression, boolean expected) + throws Exception { + assertThat(eval(expression)).isEqualTo(expected); + } + + @Test + 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()) + .contains("found no matching overload for 'sets.contains'"); + } + + @Test + public void contains_withMixedValues_succeeds() throws Exception { + assertThat(eval("sets.contains([1, 2], [2u, 2.0])")).isEqualTo(true); + } + + @Test + @TestParameters("{expression: 'sets.contains([[1], [2, 3.0]], [[2, 3]])', expected: true}") + @TestParameters( + "{expression: 'sets.contains([[1], [2, 3.0], [[[[[5]]]]]], [[[[[[5]]]]]])', expected: true}") + @TestParameters( + "{expression: 'sets.contains([[1], [2, 3.0], [[[[[5]]]]]], [[[[[[5, 1]]]]]])', expected:" + + " false}") + @TestParameters( + "{expression: 'sets.contains([[1], [2, 3.0], [[[[[5, 1]]]]]], [[[[[[5]]]]]])', expected:" + + " false}") + @TestParameters( + "{expression: 'sets.contains([[[[[[5]]]]]], [[1], [2, 3.0], [[[[[5]]]]]])', expected: false}") + public void contains_withMultiLevelNestedList_succeeds(String expression, boolean expected) + throws Exception { + assertThat(eval(expression)).isEqualTo(expected); + } + + @Test + @TestParameters("{expression: 'sets.contains([{1: 1}], [{1: 1}])', expected: true}") + @TestParameters("{expression: 'sets.contains([{1: 1}], [{1u: 1}, {1: 1.0}])', expected: true}") + @TestParameters( + "{expression: 'sets.contains([{\"a\": \"b\"}, {\"c\": \"d\"}], [{\"a\": \"b\"}])', expected:" + + " true}") + @TestParameters("{expression: 'sets.contains([{2: 1}], [{1: 1}])', expected: false}") + @TestParameters( + "{expression: 'sets.contains([{\"a\": \"b\"}], [{\"a\": \"b\"}, {\"c\": \"d\"}])', expected:" + + " false}") + public void contains_withMapValues_succeeds(String expression, boolean expected) + throws Exception { + assertThat(eval(expression)).isEqualTo(expected); + } + + @Test + @TestParameters("{expression: 'sets.equivalent([], [])', expected: true}") + @TestParameters("{expression: 'sets.equivalent([1], [1])', expected: true}") + @TestParameters("{expression: 'sets.equivalent([1], [1, 1])', expected: true}") + @TestParameters("{expression: 'sets.equivalent([1, 1], [1])', expected: true}") + @TestParameters("{expression: 'sets.equivalent([[1], [2, 3]], [[1], [2, 3]])', expected: true}") + @TestParameters("{expression: 'sets.equivalent([2, 1], [1])', expected: false}") + @TestParameters("{expression: 'sets.equivalent([1], [1, 2])', expected: false}") + @TestParameters("{expression: 'sets.equivalent([1, 2], [1, 2, 3])', expected: false}") + @TestParameters("{expression: 'sets.equivalent([1, 2], [2, 2, 2])', expected: false}") + public void equivalent_withIntTypes_succeeds(String expression, boolean expected) + throws Exception { + assertThat(eval(expression)).isEqualTo(expected); + } + + @Test + @TestParameters("{expression: 'sets.equivalent([1, 2, 3], [3u, 2.0, 1])', expected: true}") + @TestParameters("{expression: 'sets.equivalent([1], [1u, 1.0])', expected: true}") + @TestParameters("{expression: 'sets.equivalent([1], [1u, 1.0])', expected: true}") + @TestParameters( + "{expression: 'sets.equivalent([[1.0], [2, 3]], [[1], [2, 3.0]])', expected: true}") + @TestParameters("{expression: 'sets.equivalent([1, 2.0, 3], [1, 2])', expected: false}") + @TestParameters("{expression: 'sets.equivalent([1, 2], [2u, 2, 2.0])', expected: false}") + @TestParameters("{expression: 'sets.equivalent([1, 2], [1u, 2, 2.3])', expected: false}") + public void equivalent_withMixedTypes_succeeds(String expression, boolean expected) + throws Exception { + assertThat(eval(expression)).isEqualTo(expected); + } + + @Test + @TestParameters( + "{expression: 'sets.equivalent([TestAllTypes{}, TestAllTypes{}], [TestAllTypes{}])'," + + " expected: true}") + @TestParameters( + "{expression: 'sets.equivalent([TestAllTypes{}], [TestAllTypes{}, TestAllTypes{}])'," + + " expected: true}") + @TestParameters( + "{expression: 'sets.equivalent([TestAllTypes{single_int64: 1, single_uint64: 2u}]," + + " [TestAllTypes{single_int64: 1, single_uint64: 2u}])', expected: true}") + @TestParameters( + "{expression: 'sets.equivalent([TestAllTypes{single_any: [1.0, 2u, 3]}]," + + " [TestAllTypes{single_any: [1u, 2, 3.0]}])', expected: true}") + @TestParameters( + "{expression: 'sets.equivalent([TestAllTypes{single_any: [1.0, 2u, 3]}," + + " TestAllTypes{single_any: [2,3,4]}], [TestAllTypes{single_any: [1u, 2, 3.0]}])'," + + " expected: false}") + @TestParameters( + "{expression: 'sets.equivalent([TestAllTypes{single_int64: 1, single_uint64: 2u}]," + + " [TestAllTypes{single_int64: 2, single_uint64: 3u}])', expected: false}") + public void equivalent_withProtoMessage_succeeds(String expression, boolean expected) + throws Exception { + assertThat(eval(expression)).isEqualTo(expected); + } + + @Test + @TestParameters("{expression: 'sets.equivalent([{1: 1}], [{1: 1}, {1: 1}])', expected: true}") + @TestParameters("{expression: 'sets.equivalent([{1: 1}, {1: 1}], [{1: 1}])', expected: true}") + @TestParameters("{expression: 'sets.equivalent([{1: 1}], [{1: 1u}, {1: 1.0}])', expected: true}") + @TestParameters( + "{expression: 'sets.equivalent([{1: 1}, {1u: 1}], [{1u: 1}, {1: 1.0}])', expected: true}") + @TestParameters( + "{expression: 'sets.equivalent([{\"a\": \"b\"}, {\"a\": \"b\"}], [{\"a\": \"b\"}])'," + + " expected: true}") + @TestParameters("{expression: 'sets.equivalent([{2: 1}], [{1: 1}])', expected: false}") + @TestParameters( + "{expression: 'sets.equivalent([{\"a\": \"b\"}], [{\"a\": \"b\"}, {\"c\": \"d\"}])'," + + " expected: false}") + public void equivalent_withMapValues_succeeds(String expression, boolean expected) + throws Exception { + assertThat(eval(expression)).isEqualTo(expected); + } + + @Test + @TestParameters("{expression: 'sets.intersects([], [])', expected: false}") + @TestParameters("{expression: 'sets.intersects([1], [])', expected: false}") + @TestParameters("{expression: 'sets.intersects([], [1])', expected: false}") + @TestParameters("{expression: 'sets.intersects([1], [1])', expected: true}") + @TestParameters("{expression: 'sets.intersects([1], [2])', expected: false}") + @TestParameters("{expression: 'sets.intersects([1], [1, 1])', expected: true}") + @TestParameters("{expression: 'sets.intersects([1, 1], [1])', expected: true}") + @TestParameters("{expression: 'sets.intersects([2, 1], [1])', expected: true}") + @TestParameters("{expression: 'sets.intersects([1], [1, 2])', expected: true}") + @TestParameters("{expression: 'sets.intersects([1], [1.0, 2])', expected: true}") + @TestParameters("{expression: 'sets.intersects([1, 2], [2u, 2, 2.0])', expected: true}") + @TestParameters("{expression: 'sets.intersects([1, 2], [1, 2, 2.3])', expected: true}") + @TestParameters("{expression: 'sets.intersects([0, 1, 2], [1, 2, 2.3])', expected: true}") + @TestParameters("{expression: 'sets.intersects([1, 2], [1u, 2, 2.3])', expected: true}") + @TestParameters( + "{expression: 'sets.intersects([[1], [2, 3]], [[1, 2], [2, 3.0]])', expected: true}") + @TestParameters("{expression: 'sets.intersects([1], [\"1\", 2])', expected: false}") + @TestParameters("{expression: 'sets.intersects([1], [1.1, 2])', expected: false}") + @TestParameters("{expression: 'sets.intersects([1], [1.1, 2u])', expected: false}") + public void intersects_withMixedTypes_succeeds(String expression, boolean expected) + throws Exception { + assertThat(eval(expression)).isEqualTo(expected); + } + + @Test + @TestParameters("{expression: 'sets.intersects([{1: 1}], [{1: 1}, {1: 1}])', expected: true}") + @TestParameters("{expression: 'sets.intersects([{1: 1}, {1: 1}], [{1: 1}])', expected: true}") + @TestParameters("{expression: 'sets.intersects([{1: 1}], [{1: 1u}, {1: 1.0}])', expected: true}") + @TestParameters( + "{expression: 'sets.intersects([{1: 1}, {1u: 1}], [{1.0: 1u}, {1u: 1.0}])', expected: true}") + @TestParameters("{expression: 'sets.intersects([{1:2}], [{1:2}, {2:3}])', expected: true}") + @TestParameters( + "{expression: 'sets.intersects([{\"a\": \"b\"}, {\"a\": \"b\"}], [{\"a\": \"b\"}])'," + + " expected: true}") + @TestParameters( + "{expression: 'sets.intersects([{\"a\": \"b\"}], [{\"c\": \"d\"}])', expected: false}") + @TestParameters("{expression: 'sets.intersects([{2: 1}], [{1: 1}])', expected: false}") + public void intersects_withMapValues_succeeds(String expression, boolean expected) + throws Exception { + // 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(eval(expression)).isEqualTo(expected); + } + + @Test + @TestParameters( + "{expression: 'sets.intersects([TestAllTypes{}, TestAllTypes{}], [TestAllTypes{}])'," + + " expected: true}") + @TestParameters( + "{expression: 'sets.intersects([TestAllTypes{}], [TestAllTypes{}, TestAllTypes{}])'," + + " expected: true}") + @TestParameters( + "{expression: 'sets.intersects([TestAllTypes{single_int64: 1, single_uint64: 2u}]," + + " [TestAllTypes{single_int64: 1, single_uint64: 2u}])', expected: true}") + @TestParameters( + "{expression: 'sets.intersects([TestAllTypes{single_any: [1.0, 2u, 3]}]," + + " [TestAllTypes{single_any: [1u, 2, 3.0]}])', expected: true}") + @TestParameters( + "{expression: 'sets.intersects([TestAllTypes{single_any: [1, 2, 3.5]}," + + " TestAllTypes{single_any: [2,3,4]}], [TestAllTypes{single_any: [1u, 2, 3.0]}])'," + + " expected: false}") + @TestParameters( + "{expression: 'sets.intersects([TestAllTypes{single_int64: 1, single_uint64: 2u}]," + + " [TestAllTypes{single_int64: 2, single_uint64: 3u}])', expected: false}") + public void intersects_withProtoMessage_succeeds(String expression, boolean expected) + throws Exception { + assertThat(eval(expression)).isEqualTo(expected); + } + + @Test + public void setsExtension_containsFunctionSubset_succeeds() throws Exception { + CelSetsExtensions setsExtensions = + CelExtensions.sets(CelOptions.DEFAULT, SetsFunction.CONTAINS); + Cel cel = + runtimeFlavor + .builder() + .addCompilerLibraries(setsExtensions) + .addRuntimeLibraries(setsExtensions) + .build(); + + Object evaluatedResult = eval(cel, "sets.contains([1, 2], [2])"); + + assertThat(evaluatedResult).isEqualTo(true); + } + + @Test + public void setsExtension_equivalentFunctionSubset_succeeds() throws Exception { + CelSetsExtensions setsExtensions = + CelExtensions.sets(CelOptions.DEFAULT, SetsFunction.EQUIVALENT); + Cel cel = + runtimeFlavor + .builder() + .addCompilerLibraries(setsExtensions) + .addRuntimeLibraries(setsExtensions) + .build(); + + Object evaluatedResult = eval(cel, "sets.equivalent([1, 1], [1])"); + + assertThat(evaluatedResult).isEqualTo(true); + } + + @Test + public void setsExtension_intersectsFunctionSubset_succeeds() throws Exception { + CelSetsExtensions setsExtensions = + CelExtensions.sets(CelOptions.DEFAULT, SetsFunction.INTERSECTS); + Cel cel = + runtimeFlavor + .builder() + .addCompilerLibraries(setsExtensions) + .addRuntimeLibraries(setsExtensions) + .build(); + + 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); + Cel cel = runtimeFlavor.builder().addCompilerLibraries(setsExtensions).build(); + + assertThrows( + 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); + CelSetsExtensions runtimeLibrary = + CelExtensions.sets(CelOptions.DEFAULT, SetsFunction.EQUIVALENT); + Cel cel = + runtimeFlavor + .builder() + .addCompilerLibraries(setsExtensions) + .addRuntimeLibraries(runtimeLibrary) + .build(); + + 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 cb29dcdce..4b242ddcd 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelStringExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelStringExtensionsTest.java @@ -18,41 +18,68 @@ 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() { + CelExtensionLibrary library = + CelExtensions.getExtensionLibrary("strings", CelOptions.DEFAULT); + assertThat(library.name()).isEqualTo("strings"); + assertThat(library.latest().version()).isEqualTo(0); + assertThat(library.version(0).functions().stream().map(CelFunctionDecl::name)) + .containsExactly( + "charAt", + "indexOf", + "join", + "lastIndexOf", + "lowerAscii", + "replace", + "reverse", + "split", + "strings.quote", + "substring", + "trim", + "upperAscii"); + assertThat(library.version(0).macros()).isEmpty(); + } @Test @TestParameters("{string: 'abcd', beginIndex: 0, expectedResult: 'abcd'}") @@ -67,10 +94,8 @@ public final class CelStringExtensionsTest { @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); } @@ -83,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); } @@ -102,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); } @@ -136,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); } @@ -157,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'"); } @@ -270,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); } @@ -326,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); } @@ -343,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() @@ -391,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); } @@ -419,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); } @@ -433,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( @@ -457,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( @@ -480,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); @@ -497,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() @@ -515,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() @@ -538,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); } @@ -563,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); } @@ -577,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() @@ -625,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); } @@ -657,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); } @@ -672,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"); } @@ -703,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); } @@ -754,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); } @@ -772,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"); @@ -787,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() @@ -810,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); } @@ -822,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); } @@ -849,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); } @@ -868,25 +832,23 @@ 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'"); } @Test + @TestParameters("{string: '@', lastIndexOf: '@@', expectedResult: -1}") @TestParameters("{string: '', lastIndexOf: '', expectedResult: 0}") @TestParameters("{string: 'hello mellow', lastIndexOf: '', expectedResult: 12}") @TestParameters("{string: 'hello mellow', lastIndexOf: 'hello', expectedResult: 0}") @@ -909,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); } @@ -943,31 +904,27 @@ 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); } @Test + @TestParameters("{lastIndexOf: '@@'}") @TestParameters("{lastIndexOf: ' '}") @TestParameters("{lastIndexOf: 'a'}") @TestParameters("{lastIndexOf: 'abc'}") @TestParameters("{lastIndexOf: '나'}") @TestParameters("{lastIndexOf: '😁'}") - public void lastIndexOf_onEmptyString_throwsException(String lastIndexOf) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("''.lastIndexOf(indexOfParam)").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - - CelEvaluationException exception = - assertThrows( - CelEvaluationException.class, - () -> program.eval(ImmutableMap.of("indexOfParam", lastIndexOf))); + public void lastIndexOf_strLengthLessThanSubstrLength_returnsMinusOne(String lastIndexOf) + throws Exception { + Object evaluatedResult = + eval("''.lastIndexOf(indexOfParam)", ImmutableMap.of("s", "", "indexOfParam", lastIndexOf)); - assertThat(exception).hasMessageThat().contains("lastIndexOf failure: Offset out of range"); + assertThat(evaluatedResult).isEqualTo(-1); } @Test @@ -997,11 +954,10 @@ public void lastIndexOf_onEmptyString_throwsException(String lastIndexOf) throws "{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); } @@ -1072,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); } @@ -1090,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"); @@ -1105,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() @@ -1138,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); } @@ -1163,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); } @@ -1248,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); } @@ -1309,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() @@ -1381,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); } @@ -1397,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); } @@ -1416,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 = @@ -1451,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.bzl b/java_lite_proto_cel_library.bzl new file mode 100644 index 000000000..efed0bedc --- /dev/null +++ b/java_lite_proto_cel_library.bzl @@ -0,0 +1,54 @@ +# 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. + +"""Starlark rule for generating descriptors that is compatible with Protolite Messages.""" + +load("//:java_lite_proto_cel_library_impl.bzl", "java_lite_proto_cel_library_impl") +load("@com_google_protobuf//bazel:java_lite_proto_library.bzl", "java_lite_proto_library") + +def java_lite_proto_cel_library( + name, + deps, + java_descriptor_class_suffix = None, + debug = False): + """Generates a CelLiteDescriptor + + Args: + name: name of this target. + deps: The list of proto_library rules to generate Java code for. + proto_src: Name of the proto_library target. + java_descriptor_class_suffix (optional): Suffix for the Java class name of the generated CEL lite descriptor. + Default is "CelLiteDescriptor". + debug: (optional) If true, prints additional information during codegen for debugging purposes. + """ + if not name: + fail("You must provide a name.") + + if not deps or len(deps) < 1: + fail("You must provide at least one proto_library dependency.") + + java_proto_library_dep = name + "_java_lite_proto_dep" + java_lite_proto_library( + name = java_proto_library_dep, + deps = deps, + ) + + java_lite_proto_cel_library_impl( + name = name, + deps = deps, + # used_by_android + java_descriptor_class_suffix = java_descriptor_class_suffix, + java_proto_library_dep = java_proto_library_dep, + debug = debug, + ) diff --git a/java_lite_proto_cel_library_impl.bzl b/java_lite_proto_cel_library_impl.bzl new file mode 100644 index 000000000..1c5254a8c --- /dev/null +++ b/java_lite_proto_cel_library_impl.bzl @@ -0,0 +1,132 @@ +# 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. + +""" +Starlark rule for generating descriptors that is compatible with Protolite Messages. +This is an implementation detail. Clients should use 'java_lite_proto_cel_library' instead. +""" + +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("@com_google_protobuf//bazel/common:proto_info.bzl", "ProtoInfo") + +def java_lite_proto_cel_library_impl( + name, + deps, + java_proto_library_dep, + constraints = [], + java_descriptor_class_suffix = None, + debug = False): + """Generates a CelLiteDescriptor + + Args: + name: Name of this target. + deps: The list of proto_library rules to generate Java code for. + java_descriptor_class_suffix (optional): Suffix for the Java class name of the generated CEL lite descriptor. + Default is "CelLiteDescriptor". + constraints: (optional) List of strings that denote which environment the produced java_library label is associated in. + java_proto_library_dep: (optional) Uses the provided java_lite_proto_library or java_proto_library to generate the lite descriptors. + If none is provided, java_lite_proto_library is used by default behind the scenes. Most use cases should not need to provide this. + debug: (optional) If true, prints additional information during codegen for debugging purposes. + """ + if not name: + fail("You must provide a name.") + + if not deps or len(deps) < 1: + fail("You must provide at least one proto_library dependency.") + + generated = name + "_cel_lite_descriptor" + java_lite_proto_cel_library_rule( + name = generated, + debug = debug, + descriptors = deps, + java_descriptor_class_suffix = java_descriptor_class_suffix, + ) + + if not java_proto_library_dep: + java_proto_library_dep = name + "_java_lite_proto_dep" + java_lite_proto_library( + name = java_proto_library_dep, + deps = deps, + ) + + descriptor_codegen_deps = [ + "//common/annotations", + "//protobuf:cel_lite_descriptor", + java_proto_library_dep, + ] + + java_library( + name = name, + srcs = [":" + generated], + deps = descriptor_codegen_deps, + ) + +def _generate_cel_lite_descriptor_class(ctx): + srcjar_output = ctx.actions.declare_file(ctx.attr.name + ".srcjar") + java_file_path = srcjar_output.path + + direct_descriptor_set = depset( + direct = [ + descriptor[ProtoInfo].direct_descriptor_set + for descriptor in ctx.attr.descriptors + ], + ) + transitive_descriptor_set = depset( + transitive = [ + descriptor[ProtoInfo].transitive_descriptor_sets + for descriptor in ctx.attr.descriptors + ], + ) + + args = ctx.actions.args() + args.add("--version", CEL_VERSION) + args.add_joined("--descriptor_set", direct_descriptor_set, join_with = ",") + args.add_joined("--transitive_descriptor_set", transitive_descriptor_set, join_with = ",") + args.add("--out", java_file_path) + + if ctx.attr.java_descriptor_class_suffix: + args.add("--overridden_descriptor_class_suffix", ctx.attr.java_descriptor_class_suffix) + + if ctx.attr.debug: + args.add("--debug") + + ctx.actions.run( + mnemonic = "CelLiteDescriptorGenerator", + arguments = [args], + inputs = transitive_descriptor_set, + outputs = [srcjar_output], + progress_message = "Generating CelLiteDescriptor for: " + ctx.attr.name, + executable = ctx.executable._tool, + ) + + return [DefaultInfo(files = depset([srcjar_output]))] + +java_lite_proto_cel_library_rule = rule( + implementation = _generate_cel_lite_descriptor_class, + attrs = { + "java_descriptor_class_suffix": attr.string(), + "descriptors": attr.label_list( + providers = [ProtoInfo], + ), + "debug": attr.bool(), + "_tool": attr.label( + executable = True, + cfg = "exec", + allow_files = True, + default = Label("//protobuf:cel_lite_descriptor_generator"), + ), + }, +) diff --git a/optimizer/BUILD.bazel b/optimizer/BUILD.bazel index 1dd584b4e..9468b01a9 100644 --- a/optimizer/BUILD.bazel +++ b/optimizer/BUILD.bazel @@ -1,3 +1,5 @@ +load("@rules_java//java:defs.bzl", "java_library") + package( default_applicable_licenses = ["//:license"], default_visibility = ["//visibility:public"], diff --git a/optimizer/optimizers/BUILD.bazel b/optimizer/optimizers/BUILD.bazel index e39612db2..26d98c574 100644 --- a/optimizer/optimizers/BUILD.bazel +++ b/optimizer/optimizers/BUILD.bazel @@ -1,3 +1,5 @@ +load("@rules_java//java:defs.bzl", "java_library") + package( default_applicable_licenses = ["//:license"], default_visibility = ["//visibility:public"], @@ -10,6 +12,10 @@ java_library( java_library( name = "common_subexpression_elimination", - visibility = ["//visibility:public"], # TODO: Expose when ready exports = ["//optimizer/src/main/java/dev/cel/optimizer/optimizers:common_subexpression_elimination"], ) + +java_library( + name = "inlining", + exports = ["//optimizer/src/main/java/dev/cel/optimizer/optimizers:inlining"], +) diff --git a/optimizer/src/main/java/dev/cel/optimizer/AstMutator.java b/optimizer/src/main/java/dev/cel/optimizer/AstMutator.java new file mode 100644 index 000000000..59f842e29 --- /dev/null +++ b/optimizer/src/main/java/dev/cel/optimizer/AstMutator.java @@ -0,0 +1,985 @@ +// 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.optimizer; + +import static com.google.common.collect.ImmutableMap.toImmutableMap; +import static java.lang.Math.max; +import static java.util.stream.Collectors.toCollection; + +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.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.CelExprIdGeneratorFactory; +import dev.cel.common.ast.CelExprIdGeneratorFactory.ExprIdGenerator; +import dev.cel.common.ast.CelExprIdGeneratorFactory.StableIdGenerator; +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.navigation.CelNavigableMutableAst; +import dev.cel.common.navigation.CelNavigableMutableExpr; +import dev.cel.common.navigation.TraversalOrder; +import dev.cel.common.types.CelType; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map.Entry; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** AstMutator contains logic for mutating a {@link CelAbstractSyntaxTree}. */ +@Immutable +public final class AstMutator { + private static final ExprIdGenerator NO_OP_ID_GENERATOR = id -> id; + private static final ExprIdGenerator UNSET_ID_GENERATOR = id -> 0; + private final long iterationLimit; + + /** + * Returns a new instance of an AST mutator with the iteration limit set. + * + *

Mutation is performed by walking the existing AST until the expression node to replace is + * found, then the new subtree is walked to complete the mutation. Visiting of each node + * increments the iteration counter. Replace subtree operations will throw an exception if this + * counter reaches the limit. + * + * @param iterationLimit Must be greater than 0. + */ + public static AstMutator newInstance(long iterationLimit) { + return new AstMutator(iterationLimit); + } + + private AstMutator(long iterationLimit) { + Preconditions.checkState(iterationLimit > 0L); + this.iterationLimit = iterationLimit; + } + + /** Replaces all the expression IDs in the expression tree with 0. */ + public CelMutableExpr clearExprIds(CelMutableExpr expr) { + return renumberExprIds(UNSET_ID_GENERATOR, expr); + } + + /** Wraps the given AST and its subexpressions with a new cel.@block call. */ + public CelMutableAst wrapAstWithNewCelBlock( + String celBlockFunction, CelMutableAst ast, List subexpressions) { + long maxId = getMaxId(ast); + CelMutableExpr blockExpr = + CelMutableExpr.ofCall( + ++maxId, + CelMutableCall.create( + celBlockFunction, + CelMutableExpr.ofList(++maxId, CelMutableList.create(subexpressions)), + ast.expr())); + + return CelMutableAst.of(blockExpr, ast.source()); + } + + /** + * Constructs a new global call wrapped in an AST with the provided ASTs as its argument. This + * will preserve all macro source information contained within the arguments. + */ + public CelMutableAst newGlobalCall(String function, Collection args) { + return newCallAst(Optional.empty(), function, args); + } + + /** + * Constructs a new global call wrapped in an AST with the provided ASTs as its argument. This + * will preserve all macro source information contained within the arguments. + */ + public CelMutableAst newGlobalCall(String function, CelMutableAst... args) { + return newGlobalCall(function, Arrays.asList(args)); + } + + /** + * Constructs a new member call wrapped in an AST the provided ASTs as its arguments. This will + * preserve all macro source information contained within the arguments. + */ + public CelMutableAst newMemberCall(CelMutableAst target, String function, CelMutableAst... args) { + return newMemberCall(target, function, Arrays.asList(args)); + } + + /** + * Constructs a new member call wrapped in an AST the provided ASTs as its arguments. This will + * preserve all macro source information contained within the arguments. + */ + public CelMutableAst newMemberCall( + CelMutableAst target, String function, Collection args) { + return newCallAst(Optional.of(target), function, args); + } + + private CelMutableAst newCallAst( + Optional target, String function, Collection args) { + long maxId = 0; + CelMutableSource combinedSource = CelMutableSource.newInstance(); + for (CelMutableAst arg : args) { + CelMutableAst stableArg = stabilizeAst(arg, maxId); + maxId = getMaxId(stableArg); + combinedSource = combine(combinedSource, stableArg.source()); + } + + Optional maybeTarget = Optional.empty(); + if (target.isPresent()) { + CelMutableAst stableTarget = stabilizeAst(target.get(), maxId); + combinedSource = combine(combinedSource, stableTarget.source()); + maxId = getMaxId(stableTarget); + + maybeTarget = Optional.of(stableTarget); + } + + List exprArgs = + args.stream().map(CelMutableAst::expr).collect(toCollection(ArrayList::new)); + CelMutableCall newCall = + maybeTarget + .map(celMutableAst -> CelMutableCall.create(celMutableAst.expr(), function, exprArgs)) + .orElseGet(() -> CelMutableCall.create(function, exprArgs)); + + CelMutableExpr newCallExpr = CelMutableExpr.ofCall(++maxId, newCall); + + return CelMutableAst.of(newCallExpr, combinedSource); + } + + /** Renumbers all the expr IDs in the given AST in a consecutive manner starting from 1. */ + public CelMutableAst renumberIdsConsecutively(CelMutableAst mutableAst) { + StableIdGenerator stableIdGenerator = CelExprIdGeneratorFactory.newStableIdGenerator(0); + CelMutableExpr mutableExpr = renumberExprIds(stableIdGenerator::renumberId, mutableAst.expr()); + CelMutableSource newSource = + normalizeMacroSource( + mutableAst.source(), Integer.MIN_VALUE, mutableExpr, stableIdGenerator::renumberId); + + return CelMutableAst.of(mutableExpr, newSource); + } + + /** + * Replaces all comprehension identifier names with a unique name based on the given prefix. + * + *

The purpose of this is to avoid errors that can be caused by shadowed variables while + * augmenting an AST. As an example: {@code [2, 3].exists(x, x - 1 > 3) || x - 1 > 3}. Note that + * the scoping of `x - 1` is different between th two LOGICAL_OR branches. Iteration variable `x` + * in `exists` will be mangled to {@code [2, 3].exists(@c0, @c0 - 1 > 3) || x - 1 > 3} to avoid + * erroneously extracting x - 1 as common subexpression. + * + *

The expression IDs are not modified when the identifier names are changed. + * + *

Mangling occurs only if the iteration variable is referenced within the loop step. + * + *

Iteration variables in comprehensions are numbered based on their comprehension nesting + * levels and the iteration variable's type. Examples: + * + *

    + *
  • {@code [true].exists(i, i) && [true].exists(j, j)} -> {@code [true].exists(@c0:0, @c0:0) + * && [true].exists(@c0:0, @c0:0)} // Note that i,j gets replaced to the same @c0:0 in this + * example as they share the same nesting level and type. + *
  • {@code [1].exists(i, i > 0) && [1u].exists(j, j > 0u)} -> {@code [1].exists(@c0:0, @c0:0 + * > 0) && [1u].exists(@c0:1, @c0:1 > 0u)} + *
  • {@code [true].exists(i, i && [true].exists(j, j))} -> {@code [true].exists(@c0:0, @c0:0 + * && [true].exists(@c1:0, @c1:0))} + *
+ * + * @param ast AST containing type-checked references + * @param newIterVarPrefix Prefix to use for new iteration variable identifier name. For example, + * providing @c will produce @c0:0, @c0:1, @c1:0, @c2:0... as new names. + * @param newAccuVarPrefix Prefix to use for new accumulation variable identifier name. + */ + public MangledComprehensionAst mangleComprehensionIdentifierNames( + CelMutableAst ast, + String newIterVarPrefix, + String newIterVar2Prefix, + String newAccuVarPrefix) { + CelNavigableMutableAst navigableMutableAst = CelNavigableMutableAst.fromAst(ast); + Predicate comprehensionIdentifierPredicate = x -> true; + comprehensionIdentifierPredicate = + comprehensionIdentifierPredicate + .and(node -> node.getKind().equals(Kind.COMPREHENSION)) + .and(node -> !node.expr().comprehension().iterVar().startsWith(newIterVarPrefix + ":")) + .and(node -> !node.expr().comprehension().accuVar().startsWith(newAccuVarPrefix + ":")) + .and( + node -> + !node.expr().comprehension().iterVar2().startsWith(newIterVar2Prefix + ":")); + LinkedHashMap comprehensionsToMangle = + navigableMutableAst + .getRoot() + // This is important - mangling needs to happen bottom-up to avoid stepping over + // shadowed variables that are not part of the comprehension being mangled. + .allNodes(TraversalOrder.POST_ORDER) + .filter(comprehensionIdentifierPredicate) + .filter( + node -> { + // Ensure the iter_var or the comprehension result is actually referenced in the + // loop_step. If it's not, we can skip mangling. + String iterVar = node.expr().comprehension().iterVar(); + String iterVar2 = node.expr().comprehension().iterVar2(); + String result = node.expr().comprehension().result().ident().name(); + return CelNavigableMutableExpr.fromExpr(node.expr().comprehension().loopStep()) + .allNodes() + .filter(subNode -> subNode.getKind().equals(Kind.IDENT)) + .map(subNode -> subNode.expr().ident()) + .anyMatch( + ident -> + ident.name().contains(iterVar) + || ident.name().contains(iterVar2) + || ident.name().contains(result)); + }) + .collect( + Collectors.toMap( + k -> k, + v -> { + CelMutableComprehension comprehension = v.expr().comprehension(); + String iterVar = comprehension.iterVar(); + String iterVar2 = comprehension.iterVar2(); + // Identifiers to mangle could be the iteration variable, comprehension + // result or both, but at least one has to exist. + // As an example, [1,2].map(i, 3) would result in optional.empty for iteration + // variable because `i` is not actually used. + Optional iterVarId = + CelNavigableMutableExpr.fromExpr(comprehension.loopStep()) + .allNodes() + .filter( + loopStepNode -> + loopStepNode.getKind().equals(Kind.IDENT) + && loopStepNode.expr().ident().name().equals(iterVar)) + .map(CelNavigableMutableExpr::id) + .findAny(); + Optional iterVar2Id = + CelNavigableMutableExpr.fromExpr(comprehension.loopStep()) + .allNodes() + .filter( + loopStepNode -> + !iterVar2.isEmpty() + && loopStepNode.getKind().equals(Kind.IDENT) + && loopStepNode.expr().ident().name().equals(iterVar2)) + .map(CelNavigableMutableExpr::id) + .findAny(); + Optional iterVarType = + iterVarId.map( + id -> + navigableMutableAst + .getType(id) + .orElseThrow( + () -> + new NoSuchElementException( + "Checked type not present for iteration" + + " variable: " + + iterVarId))); + Optional iterVar2Type = + iterVar2Id.map( + id -> + navigableMutableAst + .getType(id) + .orElseThrow( + () -> + new NoSuchElementException( + "Checked type not present for iteration" + + " variable: " + + iterVar2Id))); + CelType resultType = + navigableMutableAst + .getType(comprehension.result().id()) + .orElseThrow( + () -> + new IllegalStateException( + "Result type was not present for the comprehension ID: " + + comprehension.result().id())); + + return MangledComprehensionType.of(iterVarType, iterVar2Type, resultType); + }, + (x, y) -> { + throw new IllegalStateException( + "Unexpected CelNavigableMutableExpr collision"); + }, + LinkedHashMap::new)); + + // The map that we'll eventually return to the caller. + HashMap mangledIdentNamesToType = + new HashMap<>(); + // Intermediary table used for the purposes of generating a unique mangled variable name. + Table comprehensionLevelToType = + HashBasedTable.create(); + CelMutableExpr mutatedComprehensionExpr = navigableMutableAst.getAst().expr(); + CelMutableSource newSource = navigableMutableAst.getAst().source(); + int iterCount = 0; + for (Entry comprehensionEntry : + comprehensionsToMangle.entrySet()) { + CelNavigableMutableExpr comprehensionNode = comprehensionEntry.getKey(); + MangledComprehensionType comprehensionEntryType = comprehensionEntry.getValue(); + + CelMutableExpr comprehensionExpr = comprehensionNode.expr(); + MangledComprehensionName mangledComprehensionName = + getMangledComprehensionName( + newIterVarPrefix, + newIterVar2Prefix, + newAccuVarPrefix, + comprehensionNode, + comprehensionLevelToType, + comprehensionEntryType); + mangledIdentNamesToType.put(mangledComprehensionName, comprehensionEntryType); + + String iterVar = comprehensionExpr.comprehension().iterVar(); + String iterVar2 = comprehensionExpr.comprehension().iterVar2(); + String accuVar = comprehensionExpr.comprehension().accuVar(); + mutatedComprehensionExpr = + mangleIdentsInComprehensionExpr( + mutatedComprehensionExpr, + comprehensionExpr, + iterVar, + iterVar2, + accuVar, + mangledComprehensionName); + // Repeat the mangling process for the macro source. + newSource = + mangleIdentsInMacroSource( + newSource, + mutatedComprehensionExpr, + iterVar, + iterVar2, + mangledComprehensionName, + comprehensionExpr.id()); + iterCount++; + } + + if (iterCount >= iterationLimit) { + // Note that it's generally impossible to reach this for a well-formed AST. The nesting level + // of AST being mutated is always deeper than the number of identifiers being mangled, thus + // the mutation operation should throw before we ever reach here. + throw new IllegalStateException("Max iteration count reached."); + } + + return MangledComprehensionAst.of( + CelMutableAst.of(mutatedComprehensionExpr, newSource), + ImmutableMap.copyOf(mangledIdentNamesToType)); + } + + private static MangledComprehensionName getMangledComprehensionName( + String newIterVarPrefix, + String newIterVar2Prefix, + String newResultPrefix, + CelNavigableMutableExpr comprehensionNode, + Table comprehensionLevelToType, + MangledComprehensionType comprehensionEntryType) { + MangledComprehensionName mangledComprehensionName; + int comprehensionNestingLevel = countComprehensionNestingLevel(comprehensionNode); + if (comprehensionLevelToType.contains(comprehensionNestingLevel, comprehensionEntryType)) { + mangledComprehensionName = + comprehensionLevelToType.get(comprehensionNestingLevel, comprehensionEntryType); + } else { + // First time encountering the pair of . Generate a unique + // mangled variable name for this. + int uniqueTypeIdx = comprehensionLevelToType.row(comprehensionNestingLevel).size(); + String mangledIterVarName = + newIterVarPrefix + ":" + comprehensionNestingLevel + ":" + uniqueTypeIdx; + String mangledResultName = + newResultPrefix + ":" + comprehensionNestingLevel + ":" + uniqueTypeIdx; + String mangledIterVar2Name = + newIterVar2Prefix + ":" + comprehensionNestingLevel + ":" + uniqueTypeIdx; + + mangledComprehensionName = + MangledComprehensionName.of(mangledIterVarName, mangledIterVar2Name, mangledResultName); + comprehensionLevelToType.put( + comprehensionNestingLevel, comprehensionEntryType, mangledComprehensionName); + } + return mangledComprehensionName; + } + + private static int countComprehensionNestingLevel(CelNavigableMutableExpr comprehensionExpr) { + return comprehensionExpr + .descendants() + .filter(node -> node.getKind().equals(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)) { + nestedLevel++; + } + maybeParent = maybeParent.parent().orElse(null); + } + return nestedLevel; + }) + .max() + .orElse(0); + } + + /** + * Replaces a subtree in the given expression node. 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. + * + *

If the ability to unparse an expression containing a macro call must be retained, use {@link + * #replaceSubtree(CelMutableAst, CelMutableAst, long) instead.} + * + * @param root Original expression node to rewrite. + * @param newExpr New CelExpr to replace the subtree with. + * @param exprIdToReplace Expression id of the subtree that is getting replaced. + */ + public CelMutableAst replaceSubtree( + CelMutableExpr root, CelMutableExpr newExpr, long exprIdToReplace) { + return replaceSubtree( + CelMutableAst.of(root, CelMutableSource.newInstance()), newExpr, exprIdToReplace); + } + + /** + * Replaces a subtree in the given 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. The renumbering occurs even if the subtree was not replaced. + * + *

This will scrub out the description, positions and line offsets from {@code CelSource}. If + * the source contains macro calls, its call IDs will be to be consistent with the renumbered IDs + * in the AST. + * + * @param ast Original ast to mutate. + * @param newExpr New CelExpr to replace the subtree with. + * @param exprIdToReplace Expression id of the subtree that is getting replaced. + */ + public CelMutableAst replaceSubtree( + CelMutableAst ast, CelMutableExpr newExpr, long exprIdToReplace) { + return replaceSubtree( + ast, + CelMutableAst.of( + newExpr, + // Copy the macro call information to the new AST such that macro call map can be + // normalized post-replacement. + ast.source()), + exprIdToReplace); + } + + /** + * Replaces a subtree in the given 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. The renumbering occurs even if the subtree was not replaced. + * + *

This will scrub out the description, positions and line offsets from {@code CelSource}. If + * the source contains macro calls, its call IDs will be to be consistent with the renumbered IDs + * in the AST. + * + * @param ast Original ast to mutate. + * @param newAst New AST to replace the subtree with. + * @param exprIdToReplace Expression id of the subtree that is getting replaced. + */ + public CelMutableAst replaceSubtree( + CelMutableAst ast, CelMutableAst newAst, long exprIdToReplace) { + return replaceSubtree( + CelNavigableMutableAst.fromAst(ast), + CelNavigableMutableAst.fromAst(newAst), + exprIdToReplace); + } + + /** + * Replaces a subtree in the given 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. The renumbering occurs even if the subtree was not replaced. + * + *

This will scrub out the description, positions and line offsets from {@code CelSource}. If + * the source contains macro calls, its call IDs will be to be consistent with the renumbered IDs + * in the AST. + * + * @param navAst Original navigable ast to mutate. + * @param navNewAst New navigable AST to replace the subtree with. + * @param exprIdToReplace Expression id of the subtree that is getting replaced. + */ + public CelMutableAst replaceSubtree( + CelNavigableMutableAst navAst, CelNavigableMutableAst navNewAst, long exprIdToReplace) { + // Stabilize the incoming AST by renumbering all of its expression IDs. + long maxId = max(getMaxId(navAst), getMaxId(navNewAst)); + CelMutableAst ast = navAst.getAst(); + CelMutableAst newAst = navNewAst.getAst(); + newAst = stabilizeAst(newAst, maxId); + long stablizedNewExprRootId = newAst.expr().id(); + + // Mutate the AST root with the new subtree. All the existing expr IDs are renumbered in the + // process, but its original IDs are memoized so that we can normalize the expr IDs + // in the macro source map. + StableIdGenerator stableIdGenerator = + CelExprIdGeneratorFactory.newStableIdGenerator(getMaxId(newAst)); + + CelMutableExpr mutatedRoot = + mutateExpr(stableIdGenerator::renumberId, ast.expr(), newAst.expr(), exprIdToReplace); + CelMutableSource newAstSource = + CelMutableSource.newInstance().setDescription(ast.source().getDescription()); + if (!ast.source().getMacroCalls().isEmpty()) { + newAstSource = combine(newAstSource, ast.source()); + } + + if (!newAst.source().getMacroCalls().isEmpty()) { + stableIdGenerator.memoize( + stablizedNewExprRootId, stableIdGenerator.renumberId(exprIdToReplace)); + newAstSource = combine(newAstSource, newAst.source()); + } + + newAstSource = + normalizeMacroSource( + newAstSource, exprIdToReplace, mutatedRoot, stableIdGenerator::renumberId); + return CelMutableAst.of(mutatedRoot, newAstSource); + } + + private CelMutableExpr mangleIdentsInComprehensionExpr( + CelMutableExpr root, + CelMutableExpr comprehensionExpr, + String originalIterVar, + String originalIterVar2, + String originalAccuVar, + MangledComprehensionName mangledComprehensionName) { + CelMutableComprehension comprehension = comprehensionExpr.comprehension(); + replaceIdentName( + comprehension.loopStep(), originalIterVar, mangledComprehensionName.iterVarName()); + replaceIdentName(comprehensionExpr, originalAccuVar, mangledComprehensionName.resultName()); + + comprehension.setIterVar(mangledComprehensionName.iterVarName()); + + // Most standard macros set accu_var as __result__, but not all (ex: cel.bind). + if (comprehension.accuVar().equals(originalAccuVar)) { + comprehension.setAccuVar(mangledComprehensionName.resultName()); + } + + if (!originalIterVar2.isEmpty()) { + comprehension.setIterVar2(mangledComprehensionName.iterVar2Name()); + replaceIdentName( + comprehension.loopStep(), originalIterVar2, mangledComprehensionName.iterVar2Name()); + } + + return mutateExpr(NO_OP_ID_GENERATOR, root, comprehensionExpr, comprehensionExpr.id()); + } + + private void replaceIdentName( + CelMutableExpr comprehensionExpr, String originalIdentName, String newIdentName) { + int iterCount; + for (iterCount = 0; iterCount < iterationLimit; iterCount++) { + CelMutableExpr identToMangle = + CelNavigableMutableExpr.fromExpr(comprehensionExpr) + .descendants() + .map(CelNavigableMutableExpr::expr) + .filter( + node -> + node.getKind().equals(Kind.IDENT) + && node.ident().name().equals(originalIdentName)) + .findAny() + .orElse(null); + if (identToMangle == null) { + break; + } + + comprehensionExpr = + mutateExpr( + NO_OP_ID_GENERATOR, + comprehensionExpr, + CelMutableExpr.ofIdent(newIdentName), + identToMangle.id()); + } + + if (iterCount >= iterationLimit) { + throw new IllegalStateException("Max iteration count reached."); + } + } + + private CelMutableSource mangleIdentsInMacroSource( + CelMutableSource sourceBuilder, + CelMutableExpr mutatedComprehensionExpr, + String originalIterVar, + String originalIterVar2, + MangledComprehensionName mangledComprehensionName, + long originalComprehensionId) { + if (!sourceBuilder.getMacroCalls().containsKey(originalComprehensionId)) { + return sourceBuilder; + } + + // First, normalize the macro source. + // ex: [x].exists(x, [x].exists(x, x == 1)) -> [x].exists(x, [@c1].exists(x, @c0 == 1)). + CelMutableSource newSource = + normalizeMacroSource(sourceBuilder, -1, mutatedComprehensionExpr, (id) -> id); + + // Note that in the above example, the iteration variable is not replaced after normalization. + // This is because populating a macro call map upon parse generates a new unique identifier + // that does not exist in the main AST. Thus, we need to manually replace the identifier. + // Also note that this only applies when the macro is at leaf. For nested macros, the iteration + // variable actually exists in the main AST thus, this step isn't needed. + // ex: [1].map(x, [2].filter(y, x == y). Here, the variable declaration `x` exists in the AST + // but not `y`. + CelMutableExpr macroExpr = newSource.getMacroCalls().get(originalComprehensionId); + // By convention, the iteration variable is always the first argument of the + // macro call expression. + CelMutableExpr identToMangle = macroExpr.call().args().get(0); + if (identToMangle.ident().name().equals(originalIterVar)) { + macroExpr = + mutateExpr( + NO_OP_ID_GENERATOR, + macroExpr, + CelMutableExpr.ofIdent(mangledComprehensionName.iterVarName()), + identToMangle.id()); + } + if (!originalIterVar2.isEmpty()) { + // Similarly by convention, iter_var2 is always the second argument of the macro call. + identToMangle = macroExpr.call().args().get(1); + if (identToMangle.ident().name().equals(originalIterVar2)) { + macroExpr = + mutateExpr( + NO_OP_ID_GENERATOR, + macroExpr, + CelMutableExpr.ofIdent(mangledComprehensionName.iterVar2Name()), + identToMangle.id()); + } + } + + newSource.addMacroCalls(originalComprehensionId, macroExpr); + + return newSource; + } + + private static CelMutableSource combine( + CelMutableSource celSource1, CelMutableSource celSource2) { + return CelMutableSource.newInstance() + .setDescription( + Strings.isNullOrEmpty(celSource1.getDescription()) + ? celSource2.getDescription() + : celSource1.getDescription()) + .addAllExtensions(celSource1.getExtensions()) + .addAllExtensions(celSource2.getExtensions()) + .addAllMacroCalls(celSource1.getMacroCalls()) + .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 + * IDs are also normalized. + */ + private CelMutableAst stabilizeAst(CelMutableAst mutableAst, long seedExprId) { + CelMutableExpr mutableExpr = mutableAst.expr(); + CelMutableSource source = mutableAst.source(); + StableIdGenerator stableIdGenerator = + CelExprIdGeneratorFactory.newStableIdGenerator(seedExprId); + CelMutableExpr mutatedExpr = renumberExprIds(stableIdGenerator::nextExprId, mutableExpr); + + CelMutableSource sourceBuilder = + CelMutableSource.newInstance().addAllExtensions(source.getExtensions()); + // Update the macro call IDs and their call IDs + for (Entry macroCall : source.getMacroCalls().entrySet()) { + long macroId = macroCall.getKey(); + long newCallId = stableIdGenerator.renumberId(macroId); + CelMutableExpr existingMacroCallExpr = CelMutableExpr.newInstance(macroCall.getValue()); + + CelMutableExpr newCall = + renumberExprIds(stableIdGenerator::renumberId, existingMacroCallExpr); + + sourceBuilder.addMacroCalls(newCallId, newCall); + } + + return CelMutableAst.of(mutatedExpr, sourceBuilder); + } + + private CelMutableSource normalizeMacroSource( + CelMutableSource source, + long exprIdToReplace, + CelMutableExpr mutatedRoot, + ExprIdGenerator idGenerator) { + // Remove the macro metadata that no longer exists in the AST due to being replaced. + source.clearMacroCall(exprIdToReplace); + if (source.getMacroCalls().isEmpty()) { + return source; + } + + ImmutableMap allExprs = + CelNavigableMutableExpr.fromExpr(mutatedRoot) + .allNodes() + .map(CelNavigableMutableExpr::expr) + .collect( + toImmutableMap( + CelMutableExpr::id, + expr -> expr, + (expr1, expr2) -> { + // Comprehensions can reuse same expression (result). We just need to ensure + // that they are identical. + if (expr1.equals(expr2)) { + return expr1; + } + throw new IllegalStateException( + "Expected expressions to be the same for id: " + expr1.id()); + })); + + CelMutableSource newMacroSource = + CelMutableSource.newInstance() + .setDescription(source.getDescription()) + .addAllExtensions(source.getExtensions()); + // Update the macro call IDs and their call references + for (Entry existingMacroCall : source.getMacroCalls().entrySet()) { + long macroId = existingMacroCall.getKey(); + long callId = idGenerator.generate(macroId); + + if (!allExprs.containsKey(callId)) { + continue; + } + + CelMutableExpr existingMacroCallExpr = + CelMutableExpr.newInstance(existingMacroCall.getValue()); + CelMutableExpr newMacroCallExpr = renumberExprIds(idGenerator, existingMacroCallExpr); + + CelNavigableMutableExpr callNav = CelNavigableMutableExpr.fromExpr(newMacroCallExpr); + ArrayList callDescendants = + callNav + .descendants() + .map(CelNavigableMutableExpr::expr) + .collect(toCollection(ArrayList::new)); + + for (CelMutableExpr callChild : callDescendants) { + if (!allExprs.containsKey(callChild.id())) { + continue; + } + + CelMutableExpr mutatedExpr = allExprs.get(callChild.id()); + if (!callChild.equals(mutatedExpr)) { + newMacroCallExpr = + mutateExpr(NO_OP_ID_GENERATOR, newMacroCallExpr, mutatedExpr, callChild.id()); + } + } + + if (exprIdToReplace > 0) { + long replacedId = idGenerator.generate(exprIdToReplace); + boolean isListExprBeingReplaced = + allExprs.containsKey(replacedId) + && allExprs.get(replacedId).getKind().equals(Kind.LIST); + if (isListExprBeingReplaced) { + unwrapListArgumentsInMacroCallExpr( + allExprs.get(callId).comprehension(), newMacroCallExpr); + } + } + + newMacroSource.addMacroCalls(callId, newMacroCallExpr); + } + + // Replace comprehension nodes with a NOT_SET reference to reduce AST size. + for (Entry macroCall : newMacroSource.getMacroCalls().entrySet()) { + CelMutableExpr macroCallExpr = macroCall.getValue(); + CelNavigableMutableExpr.fromExpr(macroCallExpr) + .allNodes() + .filter(node -> node.getKind().equals(Kind.COMPREHENSION)) + .map(CelNavigableMutableExpr::expr) + .forEach( + node -> { + CelMutableExpr mutatedNode = + mutateExpr( + NO_OP_ID_GENERATOR, + macroCallExpr, + CelMutableExpr.ofNotSet(node.id()), + node.id()); + macroCall.setValue(mutatedNode); + }); + + // Prune any NOT_SET (comprehension) nodes that no longer exist in the main AST + // 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)) + .map(CelNavigableMutableExpr::id) + .filter(id -> !allExprs.containsKey(id)) + .forEach( + id -> { + ArrayList newCallArgs = + macroCallExpr.call().args().stream() + .filter(node -> node.id() != id) + .collect(toCollection(ArrayList::new)); + CelMutableCall call = macroCallExpr.call(); + call.setArgs(newCallArgs); + }); + } + + return newMacroSource; + } + + /** + * Unwraps the arguments in the extraneous list_expr which is present in the AST but does not + * exist in the macro call map. `map`, `filter` are examples of such. + * + *

This method inspects the comprehension's accumulator initializer to infer that the list_expr + * solely exists to match the expected result type of the macro call signature. + * + * @param comprehension Comprehension in the main AST to extract the macro call arguments from + * (loop step). + * @param newMacroCallExpr (Output parameter) Modified macro call expression with the call + * arguments unwrapped. + */ + private static void unwrapListArgumentsInMacroCallExpr( + CelMutableComprehension comprehension, CelMutableExpr newMacroCallExpr) { + CelMutableExpr accuInit = comprehension.accuInit(); + if (!accuInit.getKind().equals(Kind.LIST) || !accuInit.list().elements().isEmpty()) { + // Does not contain an extraneous list. + return; + } + + CelMutableExpr loopStepExpr = comprehension.loopStep(); + List loopStepArgs = loopStepExpr.call().args(); + if (loopStepArgs.size() != 2 && loopStepArgs.size() != 3) { + throw new IllegalArgumentException( + String.format( + "Expected exactly 2 or 3 arguments but got %d instead on expr id: %d", + loopStepArgs.size(), loopStepExpr.id())); + } + + CelMutableCall existingMacroCall = newMacroCallExpr.call(); + CelMutableCall newMacroCall = + existingMacroCall.target().isPresent() + ? CelMutableCall.create(existingMacroCall.target().get(), existingMacroCall.function()) + : CelMutableCall.create(existingMacroCall.function()); + newMacroCall.addArgs( + existingMacroCall.args().get(0)); // iter_var is first argument of the call by convention + + CelMutableList extraneousList; + if (loopStepArgs.size() == 2) { + extraneousList = loopStepArgs.get(1).list(); + } else { + newMacroCall.addArgs(loopStepArgs.get(0)); + // For map(x,y,z), z is wrapped in a _+_(@result, [z]) + extraneousList = loopStepArgs.get(1).call().args().get(1).list(); + } + + newMacroCall.addArgs(extraneousList.elements()); + + newMacroCallExpr.setCall(newMacroCall); + } + + private CelMutableExpr mutateExpr( + ExprIdGenerator idGenerator, + CelMutableExpr root, + CelMutableExpr newExpr, + long exprIdToReplace) { + MutableExprVisitor mutableAst = + MutableExprVisitor.newInstance(idGenerator, newExpr, exprIdToReplace, iterationLimit); + return mutableAst.visit(root); + } + + private CelMutableExpr renumberExprIds(ExprIdGenerator idGenerator, CelMutableExpr root) { + MutableExprVisitor mutableAst = + MutableExprVisitor.newInstance(idGenerator, root, Integer.MIN_VALUE, iterationLimit); + return mutableAst.visit(root); + } + + private static long getMaxId(CelMutableAst mutableAst) { + return getMaxId(CelNavigableMutableAst.fromAst(mutableAst)); + } + + private static long getMaxId(CelNavigableMutableAst navAst) { + long maxId = navAst.getRoot().maxId(); + for (Entry macroCall : + navAst.getAst().source().getMacroCalls().entrySet()) { + maxId = max(maxId, getMaxId(macroCall.getValue())); + } + + return maxId; + } + + private static long getMaxId(CelMutableExpr mutableExpr) { + return CelNavigableMutableExpr.fromExpr(mutableExpr) + .allNodes() + .mapToLong(CelNavigableMutableExpr::id) + .max() + .orElseThrow(NoSuchElementException::new); + } + + /** + * Intermediate value class to store the mangled identifiers for iteration variable and the + * comprehension result. + */ + @AutoValue + public abstract static class MangledComprehensionAst { + + /** AST after the iteration variables have been mangled. */ + public abstract CelMutableAst mutableAst(); + + /** Map containing the mangled identifier names to their types. */ + public abstract ImmutableMap + mangledComprehensionMap(); + + private static MangledComprehensionAst of( + CelMutableAst ast, + ImmutableMap mangledComprehensionMap) { + return new AutoValue_AstMutator_MangledComprehensionAst(ast, mangledComprehensionMap); + } + } + + /** + * Intermediate value class to store the types for iter_var and comprehension result of which its + * identifier names are being mangled. + */ + @AutoValue + public abstract static class MangledComprehensionType { + + /** + * Type of iter_var. Empty if iter_var is not referenced in the expression anywhere (ex: "i" in + * "[1].exists(i, true)" + */ + public abstract Optional iterVarType(); + + /** Type of iter_var2. */ + public abstract Optional iterVar2Type(); + + /** Type of comprehension result */ + public abstract CelType resultType(); + + private static MangledComprehensionType of( + Optional iterVarType, Optional iterVarType2, CelType resultType) { + return new AutoValue_AstMutator_MangledComprehensionType( + iterVarType, iterVarType2, resultType); + } + } + + /** + * Intermediate value class to store the mangled names for iteration variable and the + * comprehension result. + */ + @AutoValue + public abstract static class MangledComprehensionName { + + /** Mangled name for iter_var */ + public abstract String iterVarName(); + + /** Mangled name for iter_var2 */ + public abstract String iterVar2Name(); + + /** Mangled name for comprehension result */ + public abstract String resultName(); + + private static MangledComprehensionName of( + String iterVarName, String iterVar2Name, String resultName) { + return new AutoValue_AstMutator_MangledComprehensionName( + iterVarName, iterVar2Name, resultName); + } + } +} diff --git a/optimizer/src/main/java/dev/cel/optimizer/BUILD.bazel b/optimizer/src/main/java/dev/cel/optimizer/BUILD.bazel index c86b4559b..e9e8994a2 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/BUILD.bazel +++ b/optimizer/src/main/java/dev/cel/optimizer/BUILD.bazel @@ -1,3 +1,5 @@ +load("@rules_java//java:defs.bzl", "java_library") + package( default_applicable_licenses = ["//:license"], default_visibility = [ @@ -36,7 +38,7 @@ java_library( deps = [ ":ast_optimizer", ":optimization_exception", - "//common", + "//common:cel_ast", "@maven//:com_google_errorprone_error_prone_annotations", ], ) @@ -53,9 +55,8 @@ java_library( ":optimization_exception", ":optimizer_builder", "//bundle:cel", - "//common", + "//common:cel_ast", "//common:compiler_common", - "//common/navigation", "@maven//:com_google_guava_guava", ], ) @@ -67,27 +68,33 @@ java_library( ], deps = [ ":optimization_exception", + "//:auto_value", "//bundle:cel", - "//common", - "//common/navigation", + "//common:cel_ast", + "//common:compiler_common", + "@maven//:com_google_guava_guava", ], ) java_library( name = "mutable_ast", srcs = [ - "MutableAst.java", + "AstMutator.java", "MutableExprVisitor.java", ], tags = [ ], deps = [ "//:auto_value", - "//common", + "//common:cel_ast", + "//common:mutable_ast", + "//common:mutable_source", "//common/annotations", "//common/ast", "//common/ast:expr_factory", - "//common/navigation", + "//common/ast:mutable_expr", + "//common/navigation:common", + "//common/navigation:mutable_navigation", "//common/types:type_providers", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", diff --git a/optimizer/src/main/java/dev/cel/optimizer/CelAstOptimizer.java b/optimizer/src/main/java/dev/cel/optimizer/CelAstOptimizer.java index 730b5cc3c..7a14f0f70 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/CelAstOptimizer.java +++ b/optimizer/src/main/java/dev/cel/optimizer/CelAstOptimizer.java @@ -14,14 +14,47 @@ package dev.cel.optimizer; -import dev.cel.bundle.CelBuilder; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import dev.cel.bundle.Cel; import dev.cel.common.CelAbstractSyntaxTree; -import dev.cel.common.navigation.CelNavigableAst; +import dev.cel.common.CelFunctionDecl; +import dev.cel.common.CelVarDecl; /** Public interface for performing a single, custom optimization on an AST. */ public interface CelAstOptimizer { /** Optimizes a single AST. */ - CelAbstractSyntaxTree optimize(CelNavigableAst navigableAst, CelBuilder cel) - throws CelOptimizationException; + OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel) throws CelOptimizationException; + + /** + * Denotes the result of a single optimization pass on an AST. + * + *

The optimizer may optionally populate new variable and function declarations generated as + * part of optimizing an AST. + */ + @AutoValue + abstract class OptimizationResult { + public abstract CelAbstractSyntaxTree optimizedAst(); + + public abstract ImmutableList newVarDecls(); + + public abstract ImmutableList newFunctionDecls(); + + /** + * Create an optimization result with new declarations. The optimizer must populate these + * declarations after an optimization pass if they are required for type-checking to success. + */ + public static OptimizationResult create( + CelAbstractSyntaxTree optimizedAst, + ImmutableList newVarDecls, + ImmutableList newFunctionDecls) { + return new AutoValue_CelAstOptimizer_OptimizationResult( + optimizedAst, newVarDecls, newFunctionDecls); + } + + public static OptimizationResult create(CelAbstractSyntaxTree optimizedAst) { + return create(optimizedAst, ImmutableList.of(), ImmutableList.of()); + } + } } diff --git a/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerImpl.java b/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerImpl.java index d57181fc2..4ac8764f1 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerImpl.java +++ b/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerImpl.java @@ -18,10 +18,9 @@ import com.google.common.collect.ImmutableSet; import dev.cel.bundle.Cel; -import dev.cel.bundle.CelBuilder; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelValidationException; -import dev.cel.common.navigation.CelNavigableAst; +import dev.cel.optimizer.CelAstOptimizer.OptimizationResult; import java.util.Arrays; final class CelOptimizerImpl implements CelOptimizer { @@ -39,13 +38,20 @@ public CelAbstractSyntaxTree optimize(CelAbstractSyntaxTree ast) throws CelOptim throw new IllegalArgumentException("AST must be type-checked."); } + Cel celOptimizerEnv = cel; CelAbstractSyntaxTree optimizedAst = ast; - CelBuilder celBuilder = cel.toCelBuilder(); try { for (CelAstOptimizer optimizer : astOptimizers) { - CelNavigableAst navigableAst = CelNavigableAst.fromAst(optimizedAst); - optimizedAst = optimizer.optimize(navigableAst, celBuilder); - optimizedAst = celBuilder.build().check(optimizedAst).getAst(); + OptimizationResult result = optimizer.optimize(optimizedAst, celOptimizerEnv); + if (!result.newFunctionDecls().isEmpty() || !result.newVarDecls().isEmpty()) { + celOptimizerEnv = + celOptimizerEnv + .toCelBuilder() + .addVarDeclarations(result.newVarDecls()) + .addFunctionDeclarations(result.newFunctionDecls()) + .build(); + } + optimizedAst = celOptimizerEnv.check(result.optimizedAst()).getAst(); } } catch (CelValidationException e) { throw new CelOptimizationException( diff --git a/optimizer/src/main/java/dev/cel/optimizer/MutableAst.java b/optimizer/src/main/java/dev/cel/optimizer/MutableAst.java deleted file mode 100644 index 1a466df67..000000000 --- a/optimizer/src/main/java/dev/cel/optimizer/MutableAst.java +++ /dev/null @@ -1,811 +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.optimizer; - -import static com.google.common.collect.ImmutableList.toImmutableList; -import static com.google.common.collect.ImmutableMap.toImmutableMap; -import static java.lang.Math.max; - -import com.google.auto.value.AutoValue; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Preconditions; -import com.google.common.collect.HashBasedTable; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Table; -import com.google.errorprone.annotations.Immutable; -import dev.cel.common.CelAbstractSyntaxTree; -import dev.cel.common.CelSource; -import dev.cel.common.ast.CelExpr; -import dev.cel.common.ast.CelExpr.CelCall; -import dev.cel.common.ast.CelExpr.CelComprehension; -import dev.cel.common.ast.CelExpr.CelIdent; -import dev.cel.common.ast.CelExpr.ExprKind.Kind; -import dev.cel.common.ast.CelExprFactory; -import dev.cel.common.ast.CelExprIdGeneratorFactory; -import dev.cel.common.ast.CelExprIdGeneratorFactory.ExprIdGenerator; -import dev.cel.common.ast.CelExprIdGeneratorFactory.MonotonicIdGenerator; -import dev.cel.common.ast.CelExprIdGeneratorFactory.StableIdGenerator; -import dev.cel.common.navigation.CelNavigableAst; -import dev.cel.common.navigation.CelNavigableExpr; -import dev.cel.common.navigation.CelNavigableExpr.TraversalOrder; -import dev.cel.common.types.CelType; -import java.util.Collection; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map.Entry; -import java.util.NoSuchElementException; -import java.util.Optional; -import java.util.function.Predicate; -import java.util.stream.Collectors; - -/** MutableAst contains logic for mutating a {@link CelAbstractSyntaxTree}. */ -@Immutable -public final class MutableAst { - private static final ExprIdGenerator NO_OP_ID_GENERATOR = id -> id; - private final long iterationLimit; - - /** - * Returns a new instance of a Mutable AST with the iteration limit set. - * - *

Mutation is performed by walking the existing AST until the expression node to replace is - * found, then the new subtree is walked to complete the mutation. Visiting of each node - * increments the iteration counter. Replace subtree operations will throw an exception if this - * counter reaches the limit. - * - * @param iterationLimit Must be greater than 0. - */ - public static MutableAst newInstance(long iterationLimit) { - return new MutableAst(iterationLimit); - } - - private MutableAst(long iterationLimit) { - Preconditions.checkState(iterationLimit > 0L); - this.iterationLimit = iterationLimit; - } - - /** Replaces all the expression IDs in the expression tree with 0. */ - public CelExpr clearExprIds(CelExpr celExpr) { - return renumberExprIds((unused) -> 0, celExpr.toBuilder()).build(); - } - - /** - * Replaces a subtree in the given expression node. This operation is intended for AST - * optimization purposes. - * - *

This is a very dangerous operation. Callers should 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. - * - *

If the ability to unparse an expression containing a macro call must be retained, use {@link - * #replaceSubtree(CelAbstractSyntaxTree, CelExpr, long) instead.} - * - * @param celExpr Original expression node to rewrite. - * @param newExpr New CelExpr to replace the subtree with. - * @param exprIdToReplace Expression id of the subtree that is getting replaced. - */ - public CelExpr replaceSubtree(CelExpr celExpr, CelExpr newExpr, long exprIdToReplace) { - MonotonicIdGenerator monotonicIdGenerator = - CelExprIdGeneratorFactory.newMonotonicIdGenerator(0); - return mutateExpr( - unused -> monotonicIdGenerator.nextExprId(), - celExpr.toBuilder(), - newExpr.toBuilder(), - exprIdToReplace) - .build(); - } - - /** - * Replaces a subtree in the given AST. This operation is intended for AST optimization purposes. - * - *

This is a very dangerous operation. Callers should 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. - * - *

This will scrub out the description, positions and line offsets from {@code CelSource}. If - * the source contains macro calls, its call IDs will be to be consistent with the renumbered IDs - * in the AST. - * - * @param ast Original ast to mutate. - * @param newExpr New CelExpr to replace the subtree with. - * @param exprIdToReplace Expression id of the subtree that is getting replaced. - */ - public CelAbstractSyntaxTree replaceSubtree( - CelAbstractSyntaxTree ast, CelExpr newExpr, long exprIdToReplace) { - return replaceSubtreeWithNewAst( - ast, - CelAbstractSyntaxTree.newParsedAst(newExpr, CelSource.newBuilder().build()), - exprIdToReplace); - } - - /** Wraps the given AST and its subexpressions with a new cel.@block call. */ - public CelAbstractSyntaxTree wrapAstWithNewCelBlock( - String celBlockFunction, CelAbstractSyntaxTree ast, Collection subexpressions) { - long maxId = getMaxId(ast); - CelExpr blockExpr = - CelExpr.newBuilder() - .setId(++maxId) - .setCall( - CelCall.newBuilder() - .setFunction(celBlockFunction) - .addArgs( - CelExpr.ofCreateListExpr( - ++maxId, ImmutableList.copyOf(subexpressions), ImmutableList.of()), - ast.getExpr()) - .build()) - .build(); - - return CelAbstractSyntaxTree.newParsedAst(blockExpr, ast.getSource()); - } - - /** - * Generates a new bind macro using the provided initialization and result expression, then - * replaces the subtree using the new bind expr at the designated expr ID. - * - *

The bind call takes the format of: {@code cel.bind(varInit, varName, resultExpr)} - * - * @param ast Original ast to mutate. - * @param varName New variable name for the bind macro call. - * @param varInit Initialization expression to bind to the local variable. - * @param resultExpr Result expression - * @param exprIdToReplace Expression ID of the subtree that is getting replaced. - */ - public CelAbstractSyntaxTree replaceSubtreeWithNewBindMacro( - CelAbstractSyntaxTree ast, - String varName, - CelExpr varInit, - CelExpr resultExpr, - long exprIdToReplace) { - long maxId = max(getMaxId(varInit), getMaxId(ast)); - StableIdGenerator stableIdGenerator = CelExprIdGeneratorFactory.newStableIdGenerator(maxId); - BindMacro bindMacro = newBindMacro(varName, varInit, resultExpr, stableIdGenerator); - // In situations where the existing AST already contains a macro call (ex: nested cel.binds), - // its macro source must be normalized to make it consistent with the newly generated bind - // macro. - CelSource celSource = - normalizeMacroSource( - ast.getSource(), - -1, // Do not replace any of the subexpr in the macro map. - bindMacro.bindMacro().toBuilder(), - stableIdGenerator::renumberId); - celSource = - celSource.toBuilder() - .addMacroCalls(bindMacro.bindExpr().id(), bindMacro.bindMacro()) - .build(); - - return replaceSubtreeWithNewAst( - ast, CelAbstractSyntaxTree.newParsedAst(bindMacro.bindExpr(), celSource), exprIdToReplace); - } - - /** Renumbers all the expr IDs in the given AST in a consecutive manner starting from 1. */ - public CelAbstractSyntaxTree renumberIdsConsecutively(CelAbstractSyntaxTree ast) { - StableIdGenerator stableIdGenerator = CelExprIdGeneratorFactory.newStableIdGenerator(0); - CelExpr.Builder root = - renumberExprIds(stableIdGenerator::renumberId, ast.getExpr().toBuilder()); - CelSource newSource = - normalizeMacroSource( - ast.getSource(), Integer.MIN_VALUE, root, stableIdGenerator::renumberId); - - return CelAbstractSyntaxTree.newParsedAst(root.build(), newSource); - } - - /** - * Replaces all comprehension identifier names with a unique name based on the given prefix. - * - *

The purpose of this is to avoid errors that can be caused by shadowed variables while - * augmenting an AST. As an example: {@code [2, 3].exists(x, x - 1 > 3) || x - 1 > 3}. Note that - * the scoping of `x - 1` is different between th two LOGICAL_OR branches. Iteration variable `x` - * in `exists` will be mangled to {@code [2, 3].exists(@c0, @c0 - 1 > 3) || x - 1 > 3} to avoid - * erroneously extracting x - 1 as common subexpression. - * - *

The expression IDs are not modified when the identifier names are changed. - * - *

Mangling occurs only if the iteration variable is referenced within the loop step. - * - *

Iteration variables in comprehensions are numbered based on their comprehension nesting - * levels and the iteration variable's type. Examples: - * - *

    - *
  • {@code [true].exists(i, i) && [true].exists(j, j)} -> {@code [true].exists(@c0:0, @c0:0) - * && [true].exists(@c0:0, @c0:0)} // Note that i,j gets replaced to the same @c0:0 in this - * example as they share the same nesting level and type. - *
  • {@code [1].exists(i, i > 0) && [1u].exists(j, j > 0u)} -> {@code [1].exists(@c0:0, @c0:0 - * > 0) && [1u].exists(@c0:1, @c0:1 > 0u)} - *
  • {@code [true].exists(i, i && [true].exists(j, j))} -> {@code [true].exists(@c0:0, @c0:0 - * && [true].exists(@c1:0, @c1:0))} - *
- * - * @param ast AST to mutate - * @param newIterVarPrefix Prefix to use for new iteration variable identifier name. For example, - * providing @c will produce @c0:0, @c0:1, @c1:0, @c2:0... as new names. - * @param newResultPrefix Prefix to use for new comprehensin result identifier names. - */ - public MangledComprehensionAst mangleComprehensionIdentifierNames( - CelAbstractSyntaxTree ast, String newIterVarPrefix, String newResultPrefix) { - CelNavigableAst newNavigableAst = CelNavigableAst.fromAst(ast); - Predicate comprehensionIdentifierPredicate = x -> true; - comprehensionIdentifierPredicate = - comprehensionIdentifierPredicate - .and(node -> node.getKind().equals(Kind.COMPREHENSION)) - .and(node -> !node.expr().comprehension().iterVar().startsWith(newIterVarPrefix)) - .and(node -> !node.expr().comprehension().accuVar().startsWith(newResultPrefix)); - - LinkedHashMap comprehensionsToMangle = - newNavigableAst - .getRoot() - // This is important - mangling needs to happen bottom-up to avoid stepping over - // shadowed variables that are not part of the comprehension being mangled. - .allNodes(TraversalOrder.POST_ORDER) - .filter(comprehensionIdentifierPredicate) - .filter( - node -> { - // Ensure the iter_var or the comprehension result is actually referenced in the - // loop_step. If it's not, we - // can skip mangling. - String iterVar = node.expr().comprehension().iterVar(); - String result = node.expr().comprehension().result().ident().name(); - return CelNavigableExpr.fromExpr(node.expr().comprehension().loopStep()) - .allNodes() - .filter(subNode -> subNode.getKind().equals(Kind.IDENT)) - .map(subNode -> subNode.expr().ident()) - .anyMatch( - ident -> ident.name().contains(iterVar) || ident.name().contains(result)); - }) - .collect( - Collectors.toMap( - k -> k, - v -> { - CelComprehension comprehension = v.expr().comprehension(); - String iterVar = comprehension.iterVar(); - // Identifiers to mangle could be the iteration variable, comprehension result - // or both, but at least one has to exist. - // As an example, [1,2].map(i, 3) would produce an optional.empty because `i` - // is not actually used. - Optional iterVarId = - CelNavigableExpr.fromExpr(comprehension.loopStep()) - .allNodes() - .filter( - loopStepNode -> - loopStepNode.expr().identOrDefault().name().equals(iterVar)) - .map(CelNavigableExpr::id) - .findAny(); - Optional iterVarType = - iterVarId.map( - id -> - ast.getType(id) - .orElseThrow( - () -> - new NoSuchElementException( - "Checked type not present for iteration variable:" - + " " - + iterVarId))); - Optional resultType = ast.getType(comprehension.result().id()); - - return MangledComprehensionType.of(iterVarType, resultType); - }, - (x, y) -> { - throw new IllegalStateException("Unexpected CelNavigableExpr collision"); - }, - LinkedHashMap::new)); - int iterCount = 0; - - // The map that we'll eventually return to the caller. - HashMap mangledIdentNamesToType = - new HashMap<>(); - // Intermediary table used for the purposes of generating a unique mangled variable name. - Table comprehensionLevelToType = - HashBasedTable.create(); - for (Entry comprehensionEntry : - comprehensionsToMangle.entrySet()) { - iterCount++; - // Refetch the comprehension node as mutating the AST could have renumbered its IDs. - CelNavigableExpr comprehensionNode = - newNavigableAst - .getRoot() - .allNodes(TraversalOrder.POST_ORDER) - .filter(comprehensionIdentifierPredicate) - .findAny() - .orElseThrow( - () -> new NoSuchElementException("Failed to refetch mutated comprehension")); - MangledComprehensionType comprehensionEntryType = comprehensionEntry.getValue(); - - CelExpr.Builder comprehensionExpr = comprehensionNode.expr().toBuilder(); - int comprehensionNestingLevel = countComprehensionNestingLevel(comprehensionNode); - MangledComprehensionName mangledComprehensionName; - if (comprehensionLevelToType.contains(comprehensionNestingLevel, comprehensionEntryType)) { - mangledComprehensionName = - comprehensionLevelToType.get(comprehensionNestingLevel, comprehensionEntryType); - } else { - // First time encountering the pair of . Generate a unique - // mangled variable name for this. - int uniqueTypeIdx = comprehensionLevelToType.row(comprehensionNestingLevel).size(); - String mangledIterVarName = - newIterVarPrefix + comprehensionNestingLevel + ":" + uniqueTypeIdx; - String mangledResultName = - newResultPrefix + comprehensionNestingLevel + ":" + uniqueTypeIdx; - mangledComprehensionName = - MangledComprehensionName.of(mangledIterVarName, mangledResultName); - comprehensionLevelToType.put( - comprehensionNestingLevel, comprehensionEntryType, mangledComprehensionName); - } - mangledIdentNamesToType.put(mangledComprehensionName, comprehensionEntryType); - - String iterVar = comprehensionExpr.comprehension().iterVar(); - String accuVar = comprehensionExpr.comprehension().accuVar(); - CelExpr.Builder mutatedComprehensionExpr = - mangleIdentsInComprehensionExpr( - newNavigableAst.getAst().getExpr().toBuilder(), - comprehensionExpr, - iterVar, - accuVar, - mangledComprehensionName); - // Repeat the mangling process for the macro source. - CelSource newSource = - mangleIdentsInMacroSource( - newNavigableAst.getAst(), - mutatedComprehensionExpr, - iterVar, - mangledComprehensionName, - comprehensionExpr.id()); - - newNavigableAst = - CelNavigableAst.fromAst( - CelAbstractSyntaxTree.newParsedAst(mutatedComprehensionExpr.build(), newSource)); - } - - if (iterCount >= iterationLimit) { - // Note that it's generally impossible to reach this for a well-formed AST. The nesting level - // of AST being mutated is always deeper than the number of identifiers being mangled, thus - // the mutation operation should throw before we ever reach here. - throw new IllegalStateException("Max iteration count reached."); - } - - return MangledComprehensionAst.of( - newNavigableAst.getAst(), ImmutableMap.copyOf(mangledIdentNamesToType)); - } - - /** - * Mutates the given AST by replacing a subtree at a given index. - * - * @param ast Existing AST being mutated - * @param newAst New subtree to perform the replacement with. If the subtree has a macro map - * populated, its macro source is merged with the existing AST's after normalization. - * @param exprIdToReplace The expr ID in the existing AST to replace the subtree at. - */ - @VisibleForTesting - CelAbstractSyntaxTree replaceSubtreeWithNewAst( - CelAbstractSyntaxTree ast, CelAbstractSyntaxTree newAst, long exprIdToReplace) { - // Stabilize the incoming AST by renumbering all of its expression IDs. - long maxId = max(getMaxId(ast), getMaxId(newAst)); - newAst = stabilizeAst(newAst, maxId); - - // Mutate the AST root with the new subtree. All the existing expr IDs are renumbered in the - // process, but its original IDs are memoized so that we can normalize the expr IDs - // in the macro source map. - StableIdGenerator stableIdGenerator = - CelExprIdGeneratorFactory.newStableIdGenerator(getMaxId(newAst)); - CelExpr.Builder mutatedRoot = - mutateExpr( - stableIdGenerator::renumberId, - ast.getExpr().toBuilder(), - newAst.getExpr().toBuilder(), - exprIdToReplace); - - CelSource newAstSource = ast.getSource(); - if (!newAst.getSource().getMacroCalls().isEmpty()) { - // The root is mutated, but the expr IDs in the macro map needs to be normalized. - // In situations where an AST with a new macro map is being inserted (ex: new bind call), - // the new subtree's expr ID is not memoized in the stable ID generator because the ID never - // existed in the main AST. - // In this case, we forcibly memoize the new subtree ID with a newly generated ID so - // that the macro map IDs can be normalized properly. - stableIdGenerator.memoize( - newAst.getExpr().id(), stableIdGenerator.renumberId(exprIdToReplace)); - newAstSource = combine(newAstSource, newAst.getSource()); - } - - newAstSource = - normalizeMacroSource( - newAstSource, exprIdToReplace, mutatedRoot, stableIdGenerator::renumberId); - - return CelAbstractSyntaxTree.newParsedAst(mutatedRoot.build(), newAstSource); - } - - private CelExpr.Builder mangleIdentsInComprehensionExpr( - CelExpr.Builder root, - CelExpr.Builder comprehensionExpr, - String originalIterVar, - String originalAccuVar, - MangledComprehensionName mangledComprehensionName) { - CelExpr.Builder modifiedLoopStep = - replaceIdentName( - comprehensionExpr.comprehension().loopStep().toBuilder(), - originalIterVar, - mangledComprehensionName.iterVarName()); - comprehensionExpr.setComprehension( - comprehensionExpr.comprehension().toBuilder() - .setLoopStep(modifiedLoopStep.build()) - .build()); - comprehensionExpr = - replaceIdentName(comprehensionExpr, originalAccuVar, mangledComprehensionName.resultName()); - - CelComprehension.Builder newComprehension = - comprehensionExpr.comprehension().toBuilder() - .setIterVar(mangledComprehensionName.iterVarName()); - // Most standard macros set accu_var as __result__, but not all (ex: cel.bind). - if (newComprehension.accuVar().equals(originalAccuVar)) { - newComprehension.setAccuVar(mangledComprehensionName.resultName()); - } - - return mutateExpr( - NO_OP_ID_GENERATOR, - root, - comprehensionExpr.setComprehension(newComprehension.build()), - comprehensionExpr.id()); - } - - private CelExpr.Builder replaceIdentName( - CelExpr.Builder comprehensionExpr, String originalIdentName, String newIdentName) { - int iterCount; - for (iterCount = 0; iterCount < iterationLimit; iterCount++) { - Optional identToMangle = - CelNavigableExpr.fromExpr(comprehensionExpr.build()) - .descendants() - .map(CelNavigableExpr::expr) - .filter(node -> node.identOrDefault().name().equals(originalIdentName)) - .findAny(); - if (!identToMangle.isPresent()) { - break; - } - - comprehensionExpr = - mutateExpr( - NO_OP_ID_GENERATOR, - comprehensionExpr, - CelExpr.newBuilder().setIdent(CelIdent.newBuilder().setName(newIdentName).build()), - identToMangle.get().id()); - } - - if (iterCount >= iterationLimit) { - throw new IllegalStateException("Max iteration count reached."); - } - - return comprehensionExpr; - } - - private CelSource mangleIdentsInMacroSource( - CelAbstractSyntaxTree ast, - CelExpr.Builder mutatedComprehensionExpr, - String originalIterVar, - MangledComprehensionName mangledComprehensionName, - long originalComprehensionId) { - if (!ast.getSource().getMacroCalls().containsKey(originalComprehensionId)) { - return ast.getSource(); - } - - // First, normalize the macro source. - // ex: [x].exists(x, [x].exists(x, x == 1)) -> [x].exists(x, [@c1].exists(x, @c0 == 1)). - CelSource.Builder newSource = - normalizeMacroSource(ast.getSource(), -1, mutatedComprehensionExpr, (id) -> id).toBuilder(); - - // Note that in the above example, the iteration variable is not replaced after normalization. - // This is because populating a macro call map upon parse generates a new unique identifier - // that does not exist in the main AST. Thus, we need to manually replace the identifier. - // Also note that this only applies when the macro is at leaf. For nested macros, the iteration - // variable actually exists in the main AST thus, this step isn't needed. - // ex: [1].map(x, [2].filter(y, x == y). Here, the variable declaration `x` exists in the AST - // but not `y`. - CelExpr.Builder macroExpr = newSource.getMacroCalls().get(originalComprehensionId).toBuilder(); - // By convention, the iteration variable is always the first argument of the - // macro call expression. - CelExpr identToMangle = macroExpr.call().args().get(0); - if (identToMangle.identOrDefault().name().equals(originalIterVar)) { - macroExpr = - mutateExpr( - NO_OP_ID_GENERATOR, - macroExpr, - CelExpr.newBuilder() - .setIdent( - CelIdent.newBuilder() - .setName(mangledComprehensionName.iterVarName()) - .build()), - identToMangle.id()); - } - - newSource.addMacroCalls(originalComprehensionId, macroExpr.build()); - return newSource.build(); - } - - private BindMacro newBindMacro( - String varName, CelExpr varInit, CelExpr resultExpr, StableIdGenerator stableIdGenerator) { - // Renumber incoming expression IDs in the init and result expression to avoid collision with - // the main AST. Existing IDs are memoized for a macro source sanitization pass at the end - // (e.g: inserting a bind macro to an existing macro expr) - varInit = renumberExprIds(stableIdGenerator::nextExprId, varInit.toBuilder()).build(); - resultExpr = renumberExprIds(stableIdGenerator::nextExprId, resultExpr.toBuilder()).build(); - CelExprFactory exprFactory = - CelExprFactory.newInstance((unused) -> stableIdGenerator.nextExprId()); - CelExpr bindMacroExpr = - exprFactory.fold( - "#unused", - exprFactory.newList(), - varName, - varInit, - exprFactory.newBoolLiteral(false), - exprFactory.newIdentifier(varName), - resultExpr); - - CelExpr bindMacroCallExpr = - exprFactory - .newReceiverCall( - "bind", - CelExpr.ofIdentExpr(stableIdGenerator.nextExprId(), "cel"), - CelExpr.ofIdentExpr(stableIdGenerator.nextExprId(), varName), - bindMacroExpr.comprehension().accuInit(), - bindMacroExpr.comprehension().result()) - .toBuilder() - .setId(0) - .build(); - - return BindMacro.of(bindMacroExpr, bindMacroCallExpr); - } - - private static CelSource combine(CelSource celSource1, CelSource celSource2) { - ImmutableMap.Builder macroMap = ImmutableMap.builder(); - macroMap.putAll(celSource1.getMacroCalls()); - macroMap.putAll(celSource2.getMacroCalls()); - - return CelSource.newBuilder().addAllMacroCalls(macroMap.buildOrThrow()).build(); - } - - /** - * 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 - * IDs are also normalized. - */ - private CelAbstractSyntaxTree stabilizeAst(CelAbstractSyntaxTree ast, long seedExprId) { - StableIdGenerator stableIdGenerator = - CelExprIdGeneratorFactory.newStableIdGenerator(seedExprId); - CelExpr.Builder newExprBuilder = - renumberExprIds(stableIdGenerator::nextExprId, ast.getExpr().toBuilder()); - - if (ast.getSource().getMacroCalls().isEmpty()) { - return CelAbstractSyntaxTree.newParsedAst(newExprBuilder.build(), ast.getSource()); - } - - CelSource.Builder sourceBuilder = CelSource.newBuilder(); - // Update the macro call IDs and their call IDs - for (Entry macroCall : ast.getSource().getMacroCalls().entrySet()) { - long macroId = macroCall.getKey(); - long newCallId = stableIdGenerator.renumberId(macroId); - - CelExpr.Builder newCall = - renumberExprIds(stableIdGenerator::renumberId, macroCall.getValue().toBuilder()); - - sourceBuilder.addMacroCalls(newCallId, newCall.build()); - } - - return CelAbstractSyntaxTree.newParsedAst(newExprBuilder.build(), sourceBuilder.build()); - } - - private CelSource normalizeMacroSource( - CelSource celSource, - long exprIdToReplace, - CelExpr.Builder mutatedRoot, - ExprIdGenerator idGenerator) { - // Remove the macro metadata that no longer exists in the AST due to being replaced. - celSource = celSource.toBuilder().clearMacroCall(exprIdToReplace).build(); - CelSource.Builder sourceBuilder = - CelSource.newBuilder().addAllExtensions(celSource.getExtensions()); - if (celSource.getMacroCalls().isEmpty()) { - return sourceBuilder.build(); - } - - ImmutableMap allExprs = - CelNavigableExpr.fromExpr(mutatedRoot.build()) - .allNodes() - .map(CelNavigableExpr::expr) - .collect( - toImmutableMap( - CelExpr::id, - expr -> expr, - (expr1, expr2) -> { - // Comprehensions can reuse same expression (result). We just need to ensure - // that they are identical. - if (expr1.equals(expr2)) { - return expr1; - } - throw new IllegalStateException( - "Expected expressions to be the same for id: " + expr1.id()); - })); - - // Update the macro call IDs and their call references - for (Entry macroCall : celSource.getMacroCalls().entrySet()) { - long macroId = macroCall.getKey(); - long callId = idGenerator.generate(macroId); - - if (!allExprs.containsKey(callId)) { - continue; - } - - CelExpr.Builder newCall = renumberExprIds(idGenerator, macroCall.getValue().toBuilder()); - CelNavigableExpr callNav = CelNavigableExpr.fromExpr(newCall.build()); - ImmutableList callDescendants = - callNav.descendants().map(CelNavigableExpr::expr).collect(toImmutableList()); - - for (CelExpr callChild : callDescendants) { - if (!allExprs.containsKey(callChild.id())) { - continue; - } - - CelExpr mutatedExpr = allExprs.get(callChild.id()); - if (!callChild.equals(mutatedExpr)) { - newCall = mutateExpr((arg) -> arg, newCall, mutatedExpr.toBuilder(), callChild.id()); - } - } - sourceBuilder.addMacroCalls(callId, newCall.build()); - } - - // Replace comprehension nodes with a NOT_SET reference to reduce AST size. - for (Entry macroCall : sourceBuilder.getMacroCalls().entrySet()) { - CelExpr macroCallExpr = macroCall.getValue(); - CelNavigableExpr.fromExpr(macroCallExpr) - .allNodes() - .filter(node -> node.getKind().equals(Kind.COMPREHENSION)) - .map(CelNavigableExpr::expr) - .forEach( - node -> { - CelExpr.Builder mutatedNode = - mutateExpr( - (id) -> id, - macroCallExpr.toBuilder(), - CelExpr.ofNotSet(node.id()).toBuilder(), - node.id()); - macroCall.setValue(mutatedNode.build()); - }); - } - - return sourceBuilder.build(); - } - - private CelExpr.Builder mutateExpr( - ExprIdGenerator idGenerator, - CelExpr.Builder root, - CelExpr.Builder newExpr, - long exprIdToReplace) { - MutableExprVisitor mutableAst = - MutableExprVisitor.newInstance(idGenerator, newExpr, exprIdToReplace, iterationLimit); - return mutableAst.visit(root); - } - - private CelExpr.Builder renumberExprIds(ExprIdGenerator idGenerator, CelExpr.Builder root) { - MutableExprVisitor mutableAst = - MutableExprVisitor.newInstance(idGenerator, root, Integer.MIN_VALUE, iterationLimit); - return mutableAst.visit(root); - } - - private static long getMaxId(CelAbstractSyntaxTree ast) { - long maxId = getMaxId(ast.getExpr()); - for (Entry macroCall : ast.getSource().getMacroCalls().entrySet()) { - maxId = max(maxId, getMaxId(macroCall.getValue())); - } - - return maxId; - } - - private static long getMaxId(CelExpr newExpr) { - return CelNavigableExpr.fromExpr(newExpr) - .allNodes() - .mapToLong(CelNavigableExpr::id) - .max() - .orElseThrow(NoSuchElementException::new); - } - - private static int countComprehensionNestingLevel(CelNavigableExpr comprehensionExpr) { - int nestedLevel = 0; - Optional maybeParent = comprehensionExpr.parent(); - while (maybeParent.isPresent()) { - if (maybeParent.get().getKind().equals(Kind.COMPREHENSION)) { - nestedLevel++; - } - - maybeParent = maybeParent.get().parent(); - } - return nestedLevel; - } - - /** - * Intermediate value class to store the mangled identifiers for iteration variable and the - * comprehension result. - */ - @AutoValue - public abstract static class MangledComprehensionAst { - - /** AST after the iteration variables have been mangled. */ - public abstract CelAbstractSyntaxTree ast(); - - /** Map containing the mangled identifier names to their types. */ - public abstract ImmutableMap - mangledComprehensionMap(); - - private static MangledComprehensionAst of( - CelAbstractSyntaxTree ast, - ImmutableMap mangledComprehensionMap) { - return new AutoValue_MutableAst_MangledComprehensionAst(ast, mangledComprehensionMap); - } - } - - /** - * Intermediate value class to store the types for iter_var and comprehension result of which its - * identifier names are being mangled. - */ - @AutoValue - public abstract static class MangledComprehensionType { - - /** Type of iter_var */ - public abstract Optional iterVarType(); - - /** Type of comprehension result */ - public abstract Optional resultType(); - - private static MangledComprehensionType of( - Optional iterVarType, Optional resultType) { - Preconditions.checkArgument(iterVarType.isPresent() || resultType.isPresent()); - return new AutoValue_MutableAst_MangledComprehensionType(iterVarType, resultType); - } - } - - /** - * Intermediate value class to store the mangled names for iteration variable and the - * comprehension result. - */ - @AutoValue - public abstract static class MangledComprehensionName { - - /** Mangled name for iter_var */ - public abstract String iterVarName(); - - /** Mangled name for comprehension result */ - public abstract String resultName(); - - private static MangledComprehensionName of(String iterVarName, String resultName) { - return new AutoValue_MutableAst_MangledComprehensionName(iterVarName, resultName); - } - } - - /** - * Intermediate value class to store the generated CelExpr for the bind macro and the macro call - * information. - */ - @AutoValue - abstract static class BindMacro { - /** Comprehension expr for the generated cel.bind macro. */ - abstract CelExpr bindExpr(); - - /** - * Call expr representation that will be stored in the macro call map of the AST. This is - * typically used for the purposes of supporting unparse. - */ - abstract CelExpr bindMacro(); - - private static BindMacro of(CelExpr bindExpr, CelExpr bindMacro) { - return new AutoValue_MutableAst_BindMacro(bindExpr, bindMacro); - } - } -} diff --git a/optimizer/src/main/java/dev/cel/optimizer/MutableExprVisitor.java b/optimizer/src/main/java/dev/cel/optimizer/MutableExprVisitor.java index 36cb86069..7a04a3c3a 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/MutableExprVisitor.java +++ b/optimizer/src/main/java/dev/cel/optimizer/MutableExprVisitor.java @@ -15,30 +15,28 @@ package dev.cel.optimizer; import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableList; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import dev.cel.common.annotations.Internal; -import dev.cel.common.ast.CelExpr; -import dev.cel.common.ast.CelExpr.CelCall; -import dev.cel.common.ast.CelExpr.CelComprehension; -import dev.cel.common.ast.CelExpr.CelCreateList; -import dev.cel.common.ast.CelExpr.CelCreateMap; -import dev.cel.common.ast.CelExpr.CelCreateStruct; -import dev.cel.common.ast.CelExpr.CelSelect; import dev.cel.common.ast.CelExprIdGeneratorFactory.ExprIdGenerator; +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.CelMutableSelect; +import dev.cel.common.ast.CelMutableExpr.CelMutableStruct; +import java.util.List; /** - * MutableExprVisitor performs mutation of {@link CelExpr} based on its configured parameters. + * MutableExprVisitor performs mutation of {@link CelMutableExpr} based on its configured + * parameters. * *

This class is NOT thread-safe. Callers should spawn a new instance of this class each time the * expression is being mutated. - * - *

Note that CelExpr is immutable by design. Therefore, the logic here doesn't actually mutate - * the existing expression tree. Instead, a brand new CelExpr is produced with the subtree swapped - * at the desired expression ID to replace. */ @Internal final class MutableExprVisitor { - private final CelExpr.Builder newExpr; + private final CelMutableExpr newExpr; private final ExprIdGenerator celExprIdGenerator; private final long iterationLimit; private int iterationCount; @@ -46,7 +44,7 @@ final class MutableExprVisitor { static MutableExprVisitor newInstance( ExprIdGenerator idGenerator, - CelExpr.Builder newExpr, + CelMutableExpr newExpr, long exprIdToReplace, long iterationLimit) { // iterationLimit * 2, because the expr can be walked twice due to the immutable nature of @@ -54,107 +52,108 @@ static MutableExprVisitor newInstance( return new MutableExprVisitor(idGenerator, newExpr, exprIdToReplace, iterationLimit * 2); } - CelExpr.Builder visit(CelExpr.Builder root) { + CelMutableExpr visit(CelMutableExpr root) { if (++iterationCount > iterationLimit) { throw new IllegalStateException("Max iteration count reached."); } if (root.id() == exprIdToReplace) { exprIdToReplace = Integer.MIN_VALUE; // Marks that the subtree has been replaced. - return visit(this.newExpr.setId(root.id())); + this.newExpr.setId(root.id()); + return visit(this.newExpr); } root.setId(celExprIdGenerator.generate(root.id())); - switch (root.exprKind().getKind()) { + switch (root.getKind()) { case SELECT: - return visit(root, root.select().toBuilder()); + return visit(root, root.select()); case CALL: - return visit(root, root.call().toBuilder()); - case CREATE_LIST: - return visit(root, root.createList().toBuilder()); - case CREATE_STRUCT: - return visit(root, root.createStruct().toBuilder()); - case CREATE_MAP: - return visit(root, root.createMap().toBuilder()); + return visit(root, root.call()); + case LIST: + return visit(root, root.list()); + case STRUCT: + return visit(root, root.struct()); + case MAP: + return visit(root, root.map()); case COMPREHENSION: - return visit(root, root.comprehension().toBuilder()); + return visit(root, root.comprehension()); case CONSTANT: // Fall-through is intended case IDENT: case NOT_SET: // Note: comprehension arguments can contain a not set root. return root; } - throw new IllegalArgumentException("unexpected root kind: " + root.exprKind().getKind()); + throw new IllegalArgumentException("unexpected root kind: " + root.getKind()); } - private CelExpr.Builder visit(CelExpr.Builder expr, CelSelect.Builder select) { - select.setOperand(visit(select.operand().toBuilder()).build()); - return expr.setSelect(select.build()); + @CanIgnoreReturnValue + private CelMutableExpr visit(CelMutableExpr expr, CelMutableSelect select) { + select.setOperand(visit(select.operand())); + return expr; } - private CelExpr.Builder visit(CelExpr.Builder expr, CelCall.Builder call) { + @CanIgnoreReturnValue + private CelMutableExpr visit(CelMutableExpr expr, CelMutableCall call) { if (call.target().isPresent()) { - call.setTarget(visit(call.target().get().toBuilder()).build()); + call.setTarget(visit(call.target().get())); } - ImmutableList argsBuilders = call.getArgsBuilders(); + List argsBuilders = call.args(); for (int i = 0; i < argsBuilders.size(); i++) { - CelExpr.Builder arg = argsBuilders.get(i); - call.setArg(i, visit(arg).build()); + CelMutableExpr arg = argsBuilders.get(i); + call.setArg(i, visit(arg)); } - return expr.setCall(call.build()); + return expr; } - private CelExpr.Builder visit(CelExpr.Builder expr, CelCreateStruct.Builder createStruct) { - ImmutableList entries = createStruct.getEntriesBuilders(); - for (int i = 0; i < entries.size(); i++) { - CelCreateStruct.Entry.Builder entry = entries.get(i); + @CanIgnoreReturnValue + private CelMutableExpr visit(CelMutableExpr expr, CelMutableStruct struct) { + List entries = struct.entries(); + for (CelMutableStruct.Entry entry : entries) { entry.setId(celExprIdGenerator.generate(entry.id())); - entry.setValue(visit(entry.value().toBuilder()).build()); - - createStruct.setEntry(i, entry.build()); + entry.setValue(visit(entry.value())); } - return expr.setCreateStruct(createStruct.build()); + return expr; } - private CelExpr.Builder visit(CelExpr.Builder expr, CelCreateMap.Builder createMap) { - ImmutableList entriesBuilders = createMap.getEntriesBuilders(); - for (int i = 0; i < entriesBuilders.size(); i++) { - CelCreateMap.Entry.Builder entry = entriesBuilders.get(i); + @CanIgnoreReturnValue + private CelMutableExpr visit(CelMutableExpr expr, CelMutableMap map) { + List entriesBuilders = map.entries(); + for (CelMutableMap.Entry entry : entriesBuilders) { entry.setId(celExprIdGenerator.generate(entry.id())); - entry.setKey(visit(entry.key().toBuilder()).build()); - entry.setValue(visit(entry.value().toBuilder()).build()); - - createMap.setEntry(i, entry.build()); + entry.setKey(visit(entry.key())); + entry.setValue(visit(entry.value())); } - return expr.setCreateMap(createMap.build()); + return expr; } - private CelExpr.Builder visit(CelExpr.Builder expr, CelCreateList.Builder createList) { - ImmutableList elementsBuilders = createList.getElementsBuilders(); + @CanIgnoreReturnValue + private CelMutableExpr visit(CelMutableExpr expr, CelMutableList list) { + List elementsBuilders = list.elements(); for (int i = 0; i < elementsBuilders.size(); i++) { - CelExpr.Builder elem = elementsBuilders.get(i); - createList.setElement(i, visit(elem).build()); + CelMutableExpr elem = elementsBuilders.get(i); + list.setElement(i, visit(elem)); } - return expr.setCreateList(createList.build()); + return expr; } - private CelExpr.Builder visit(CelExpr.Builder expr, CelComprehension.Builder comprehension) { - comprehension.setIterRange(visit(comprehension.iterRange().toBuilder()).build()); - comprehension.setAccuInit(visit(comprehension.accuInit().toBuilder()).build()); - comprehension.setLoopCondition(visit(comprehension.loopCondition().toBuilder()).build()); - comprehension.setLoopStep(visit(comprehension.loopStep().toBuilder()).build()); - comprehension.setResult(visit(comprehension.result().toBuilder()).build()); + @CanIgnoreReturnValue + private CelMutableExpr visit(CelMutableExpr expr, CelMutableComprehension comprehension) { + comprehension.setIterRange(visit(comprehension.iterRange())); + comprehension.setAccuInit(visit(comprehension.accuInit())); + comprehension.setLoopCondition(visit(comprehension.loopCondition())); + comprehension.setLoopStep(visit(comprehension.loopStep())); + comprehension.setResult(visit(comprehension.result())); - return expr.setComprehension(comprehension.build()); + return expr; } private MutableExprVisitor( ExprIdGenerator celExprIdGenerator, - CelExpr.Builder newExpr, + CelMutableExpr newExpr, long exprId, long iterationLimit) { Preconditions.checkState(iterationLimit > 0L); 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 38267774f..155ae262d 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/optimizers/BUILD.bazel +++ b/optimizer/src/main/java/dev/cel/optimizer/optimizers/BUILD.bazel @@ -1,3 +1,5 @@ +load("@rules_java//java:defs.bzl", "java_library") + package( default_applicable_licenses = ["//:license"], default_visibility = [ @@ -14,19 +16,29 @@ java_library( tags = [ ], deps = [ + ":default_optimizer_constants", "//:auto_value", "//bundle:cel", - "//common", + "//checker:standard_decl", + "//common:cel_ast", + "//common:cel_source", "//common:compiler_common", + "//common:mutable_ast", + "//common:operator", "//common/ast", - "//common/ast:expr_util", - "//common/navigation", + "//common/ast:mutable_expr", + "//common/internal:date_time_helpers", + "//common/navigation:common", + "//common/navigation:mutable_navigation", + "//common/types", "//extensions:optional_library", "//optimizer:ast_optimizer", "//optimizer:mutable_ast", "//optimizer:optimization_exception", - "//parser:operator", "//runtime", + "//runtime:partial_vars", + "//runtime:unknown_attributes", + "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", ], ) @@ -39,21 +51,67 @@ java_library( tags = [ ], deps = [ + ":default_optimizer_constants", "//:auto_value", "//bundle:cel", - "//checker:checker_legacy_environment", - "//common", + "//common:cel_ast", + "//common:cel_source", "//common:compiler_common", + "//common:mutable_ast", + "//common:mutable_source", "//common/ast", + "//common/ast:mutable_expr", "//common/navigation", + "//common/navigation:common", + "//common/navigation:mutable_navigation", "//common/types", "//common/types:type_providers", - "//extensions:optional_library", "//optimizer:ast_optimizer", "//optimizer:mutable_ast", - "//parser:operator", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:org_jspecify_jspecify", ], ) + +java_library( + name = "inlining", + srcs = [ + "InliningOptimizer.java", + ], + tags = [ + ], + deps = [ + "//:auto_value", + "//bundle:cel", + "//common:cel_ast", + "//common:mutable_ast", + "//common:operator", + "//common/ast", + "//common/ast:mutable_expr", + "//common/navigation:mutable_navigation", + "//common/types", + "//common/types:type_providers", + "//common/values", + "//optimizer:ast_optimizer", + "//optimizer:mutable_ast", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven//:org_jspecify_jspecify", + ], +) + +java_library( + name = "default_optimizer_constants", + srcs = [ + "DefaultOptimizerConstants.java", + ], + visibility = ["//visibility:private"], + deps = [ + "//checker:standard_decl", + "//common:operator", + "//extensions", + "//extensions:optional_library", + "@maven//:com_google_guava_guava", + ], +) diff --git a/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java b/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java index 6766ae3c4..46f801bb8 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java +++ b/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java @@ -13,37 +13,53 @@ // 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.collect.MoreCollectors.onlyElement; +import static dev.cel.checker.CelStandardDeclarations.StandardFunction.DURATION; +import static dev.cel.checker.CelStandardDeclarations.StandardFunction.TIMESTAMP; import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableList; +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.CelCall; -import dev.cel.common.ast.CelExpr.CelCreateList; -import dev.cel.common.ast.CelExpr.CelCreateMap; -import dev.cel.common.ast.CelExpr.CelCreateStruct; import dev.cel.common.ast.CelExpr.ExprKind.Kind; -import dev.cel.common.ast.CelExprUtil; -import dev.cel.common.navigation.CelNavigableAst; -import dev.cel.common.navigation.CelNavigableExpr; +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.CelNavigableMutableAst; +import dev.cel.common.navigation.CelNavigableMutableExpr; +import dev.cel.common.navigation.TraversalOrder; +import dev.cel.common.types.SimpleType; import dev.cel.extensions.CelOptionalLibrary.Function; +import dev.cel.optimizer.AstMutator; import dev.cel.optimizer.CelAstOptimizer; import dev.cel.optimizer.CelOptimizationException; -import dev.cel.optimizer.MutableAst; -import dev.cel.parser.Operator; +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.HashSet; +import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; -import java.util.Set; /** * Performs optimization for inlining constant scalar and aggregate literal values within function @@ -67,67 +83,93 @@ public static ConstantFoldingOptimizer newInstance( return new ConstantFoldingOptimizer(constantFoldingOptions); } + private final ConstantFoldingOptions constantFoldingOptions; + private final AstMutator astMutator; + private final ImmutableSet foldableFunctions; + // Use optional.of and optional.none as sentinel function names for folding optional calls. // TODO: Leverage CelValue representation of Optionals instead when available. - private static final CelExpr OPTIONAL_NONE_EXPR = - CelExpr.ofCallExpr( - 0, Optional.empty(), Function.OPTIONAL_NONE.getFunction(), ImmutableList.of()); - - private final ConstantFoldingOptions constantFoldingOptions; - private final MutableAst mutableAst; + private static CelMutableExpr newOptionalNoneExpr() { + return CelMutableExpr.ofCall(CelMutableCall.create(Function.OPTIONAL_NONE.getFunction())); + } @Override - public CelAbstractSyntaxTree optimize(CelNavigableAst navigableAst, CelBuilder celBuilder) + public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel) throws CelOptimizationException { - Cel cel = celBuilder.build(); - Set visitedExprs = new HashSet<>(); + // 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; - while (true) { - iterCount++; + boolean continueFolding = true; + while (continueFolding) { if (iterCount >= constantFoldingOptions.maxIterationLimit()) { throw new IllegalStateException("Max iteration count reached."); } - Optional foldableExpr = - navigableAst + iterCount++; + continueFolding = false; + + ImmutableList foldableExprs = + CelNavigableMutableAst.fromAst(mutableAst) .getRoot() - .allNodes() - .filter(ConstantFoldingOptimizer::canFold) - .map(CelNavigableExpr::expr) - .filter(expr -> !visitedExprs.contains(expr)) - .findAny(); - if (!foldableExpr.isPresent()) { - break; - } - visitedExprs.add(foldableExpr.get()); - - Optional mutatedAst; - // Attempt to prune if it is a non-strict call - mutatedAst = maybePruneBranches(navigableAst.getAst(), foldableExpr.get()); - if (!mutatedAst.isPresent()) { - // Evaluate the call then fold - mutatedAst = maybeFold(cel, navigableAst.getAst(), foldableExpr.get()); - } + .allNodes(TraversalOrder.PRE_ORDER) + .filter(this::canFold) + .collect(toImmutableList()); + for (CelNavigableMutableExpr foldableExpr : foldableExprs) { + iterCount++; + + Optional mutatedResult; + // Attempt to prune if it is a non-strict call + mutatedResult = maybePruneBranches(mutableAst, foldableExpr.expr()); + if (!mutatedResult.isPresent()) { + // Evaluate the call then fold + try { + mutatedResult = maybeFold(optimizerEnv, mutableAst, foldableExpr); + } catch (CelEvaluationException e) { + throw new CelOptimizationException( + "Constant folding failure. Failed to evaluate subtree due to: " + e.getMessage(), + e); + } + } - if (!mutatedAst.isPresent()) { - // Skip this expr. It's neither prune-able nor foldable. - continue; - } + if (!mutatedResult.isPresent()) { + // Skip this expr. It's neither prune-able nor foldable. + continue; + } - visitedExprs.clear(); - navigableAst = CelNavigableAst.fromAst(mutatedAst.get()); + continueFolding = true; + mutableAst = mutatedResult.get(); + // Break the loop because we mutated the AST. Since we traverse in PRE_ORDER (top-down), + // mutating a parent node means its children are now obsolete or folded. + // We restart the traversal to gather a fresh list of foldable expressions. + break; + } } // If the output is a list, map, or struct which contains optional entries, then prune it // to make sure that the optionals, if resolved, do not surface in the output literal. - CelAbstractSyntaxTree newAst = pruneOptionalElements(navigableAst); - return mutableAst.renumberIdsConsecutively(newAst); + mutableAst = pruneOptionalElements(mutableAst); + + return OptimizationResult.create(astMutator.renumberIdsConsecutively(mutableAst).toParsedAst()); } - private static boolean canFold(CelNavigableExpr navigableExpr) { + private boolean canFold(CelNavigableMutableExpr navigableExpr) { switch (navigableExpr.getKind()) { case CALL: - CelCall celCall = navigableExpr.expr().call(); - String functionName = celCall.function(); + if (!containsFoldableFunctionOnly(navigableExpr)) { + return false; + } + + // Timestamps/durations in CEL are calls, but they are effectively treated as literals. + // Expressions like timestamp(123) cannot be folded directly, but arithmetics involving + // timestamps can be optimized. + // Ex: timestamp(123) - timestamp(100) = duration("23s") + if (isCallTimestampOrDuration(navigableExpr.expr().call())) { + return false; + } + + CelMutableCall mutableCall = navigableExpr.expr().call(); + String functionName = mutableCall.function(); // These are already folded or do not need to be folded. if (functionName.equals(Function.OPTIONAL_OF.getFunction()) @@ -140,53 +182,105 @@ private static boolean canFold(CelNavigableExpr navigableExpr) { || functionName.equals(Operator.LOGICAL_OR.getFunction())) { // If any element is a constant, this could be a foldable expr (e.g: x && false -> x) - return celCall.args().stream() - .anyMatch(node -> node.exprKind().getKind().equals(Kind.CONSTANT)); + return mutableCall.args().stream().anyMatch(node -> node.getKind().equals(Kind.CONSTANT)); } if (functionName.equals(Operator.CONDITIONAL.getFunction())) { - CelExpr cond = celCall.args().get(0); + CelMutableExpr cond = mutableCall.args().get(0); // A ternary with a constant condition is trivially foldable - return cond.constantOrDefault().getKind().equals(CelConstant.Kind.BOOLEAN_VALUE); + return cond.getKind().equals(Kind.CONSTANT) + && cond.constant().getKind().equals(CelConstant.Kind.BOOLEAN_VALUE); + } + + if (functionName.equals(Operator.EQUALS.getFunction()) + || functionName.equals(Operator.NOT_EQUALS.getFunction())) { + if (mutableCall.args().stream() + .anyMatch(node -> isExprConstantOfKind(node, CelConstant.Kind.BOOLEAN_VALUE)) + || mutableCall.args().stream() + .allMatch(node -> node.getKind().equals(Kind.CONSTANT))) { + return true; + } } if (functionName.equals(Operator.IN.getFunction())) { - return true; + return canFoldInOperator(navigableExpr); } // Default case: all call arguments must be constants. If the argument is a container (ex: // list, map), then its arguments must be a constant. return areChildrenArgConstant(navigableExpr); case SELECT: - CelNavigableExpr operand = navigableExpr.children().collect(onlyElement()); + CelNavigableMutableExpr operand = navigableExpr.children().collect(onlyElement()); return areChildrenArgConstant(operand); case COMPREHENSION: - return !isNestedComprehension(navigableExpr); + return !isNestedComprehension(navigableExpr) && containsFoldableFunctionOnly(navigableExpr); default: return false; } } - private static boolean areChildrenArgConstant(CelNavigableExpr expr) { + private boolean containsFoldableFunctionOnly(CelNavigableMutableExpr navigableExpr) { + return navigableExpr + .allNodes() + .filter(node -> node.getKind().equals(Kind.CALL)) + .map(node -> node.expr().call()) + .allMatch(call -> foldableFunctions.contains(call.function())); + } + + private static boolean isCallTimestampOrDuration(CelMutableCall call) { + return call.function().equals(TIMESTAMP.functionName()) + || 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; } if (expr.getKind().equals(Kind.CALL) - || expr.getKind().equals(Kind.CREATE_LIST) - || expr.getKind().equals(Kind.CREATE_MAP) - || expr.getKind().equals(Kind.CREATE_STRUCT)) { + || expr.getKind().equals(Kind.LIST) + || expr.getKind().equals(Kind.MAP) + || expr.getKind().equals(Kind.SELECT) + || expr.getKind().equals(Kind.STRUCT)) { return expr.children().allMatch(ConstantFoldingOptimizer::areChildrenArgConstant); } return false; } - private static boolean isNestedComprehension(CelNavigableExpr expr) { - Optional maybeParent = expr.parent(); + private static boolean isNestedComprehension(CelNavigableMutableExpr expr) { + Optional maybeParent = expr.parent(); while (maybeParent.isPresent()) { - CelNavigableExpr parent = maybeParent.get(); + CelNavigableMutableExpr parent = maybeParent.get(); if (parent.getKind().equals(Kind.COMPREHENSION)) { return true; } @@ -196,12 +290,13 @@ private static boolean isNestedComprehension(CelNavigableExpr expr) { return false; } - private Optional maybeFold( - Cel cel, CelAbstractSyntaxTree ast, CelExpr expr) throws CelOptimizationException { + private Optional maybeFold( + Cel cel, CelMutableAst mutableAst, CelNavigableMutableExpr node) + throws CelOptimizationException, CelEvaluationException { Object result; try { - result = CelExprUtil.evaluateExpr(cel, expr); - } catch (CelValidationException | CelEvaluationException e) { + result = evaluateExpr(cel, node); + } catch (CelValidationException e) { throw new CelOptimizationException( "Constant folding failure. Failed to evaluate subtree due to: " + e.getMessage(), e); } @@ -211,162 +306,203 @@ private Optional maybeFold( // ex2: optional.ofNonZeroValue(5) -> optional.of(5) if (result instanceof Optional) { Optional optResult = ((Optional) result); - return maybeRewriteOptional(optResult, ast, expr); + return maybeRewriteOptional(optResult, mutableAst, node.expr()); } return maybeAdaptEvaluatedResult(result) - .map(celExpr -> mutableAst.replaceSubtree(ast, celExpr, expr.id())); + .map(celExpr -> astMutator.replaceSubtree(mutableAst, celExpr, node.id())); } - private Optional maybeAdaptEvaluatedResult(Object result) { + private Optional maybeAdaptEvaluatedResult(Object result) { if (CelConstant.isConstantValue(result)) { - return Optional.of( - CelExpr.newBuilder().setConstant(CelConstant.ofObjectValue(result)).build()); + return Optional.of(CelMutableExpr.ofConstant(CelConstant.ofObjectValue(result))); } else if (result instanceof Collection) { Collection collection = (Collection) result; - CelCreateList.Builder createListBuilder = CelCreateList.newBuilder(); + List listElements = new ArrayList<>(); for (Object evaluatedElement : collection) { - Optional adaptedExpr = maybeAdaptEvaluatedResult(evaluatedElement); - if (!adaptedExpr.isPresent()) { + CelMutableExpr adaptedExpr = maybeAdaptEvaluatedResult(evaluatedElement).orElse(null); + if (adaptedExpr == null) { return Optional.empty(); } - createListBuilder.addElements(adaptedExpr.get()); + listElements.add(adaptedExpr); } - return Optional.of(CelExpr.newBuilder().setCreateList(createListBuilder.build()).build()); + return Optional.of(CelMutableExpr.ofList(CelMutableList.create(listElements))); } else if (result instanceof Map) { Map map = (Map) result; - CelCreateMap.Builder createMapBuilder = CelCreateMap.newBuilder(); + List mapEntries = new ArrayList<>(); for (Entry entry : map.entrySet()) { - Optional adaptedKey = maybeAdaptEvaluatedResult(entry.getKey()); - if (!adaptedKey.isPresent()) { + CelMutableExpr adaptedKey = maybeAdaptEvaluatedResult(entry.getKey()).orElse(null); + if (adaptedKey == null) { return Optional.empty(); } - Optional adaptedValue = maybeAdaptEvaluatedResult(entry.getValue()); - if (!adaptedValue.isPresent()) { + CelMutableExpr adaptedValue = maybeAdaptEvaluatedResult(entry.getValue()).orElse(null); + if (adaptedValue == null) { return Optional.empty(); } - createMapBuilder.addEntries( - CelCreateMap.Entry.newBuilder() - .setKey(adaptedKey.get()) - .setValue(adaptedValue.get()) - .build()); + mapEntries.add(CelMutableMap.Entry.create(adaptedKey, adaptedValue)); } - return Optional.of(CelExpr.newBuilder().setCreateMap(createMapBuilder.build()).build()); + return Optional.of(CelMutableExpr.ofMap(CelMutableMap.create(mapEntries))); + } else if (result instanceof Duration) { + String durationStrArg = DateTimeHelpers.toString((Duration) result); + CelMutableCall durationCall = + CelMutableCall.create( + DURATION.functionName(), + CelMutableExpr.ofConstant(CelConstant.ofValue(durationStrArg))); + + return Optional.of(CelMutableExpr.ofCall(durationCall)); + } else if (result instanceof Instant) { + String timestampStrArg = result.toString(); + CelMutableCall timestampCall = + CelMutableCall.create( + TIMESTAMP.functionName(), + CelMutableExpr.ofConstant(CelConstant.ofValue(timestampStrArg))); + + return Optional.of(CelMutableExpr.ofCall(timestampCall)); } // Evaluated result cannot be folded (e.g: unknowns) return Optional.empty(); } - private Optional maybeRewriteOptional( - Optional optResult, CelAbstractSyntaxTree ast, CelExpr expr) { - if (!optResult.isPresent()) { - if (!expr.callOrDefault().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(mutableAst.replaceSubtree(ast, OPTIONAL_NONE_EXPR, expr.id())); - } - } else if (!expr.callOrDefault().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( + Optional optResult, CelMutableAst mutableAst, 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(astMutator.replaceSubtree(mutableAst, newOptionalNoneExpr(), expr.id())); + } - CelExpr newOptionalOfCall = - CelExpr.newBuilder() - .setCall( - CelCall.newBuilder() - .setFunction(Function.OPTIONAL_OF.getFunction()) - .addArgs( - CelExpr.newBuilder() - .setConstant(CelConstant.ofObjectValue(unwrappedResult)) - .build()) - .build()) - .build(); - return Optional.of(mutableAst.replaceSubtree(ast, newOptionalOfCall, expr.id())); + if (isCallToFunction(expr, Function.OPTIONAL_OF.getFunction())) { + return Optional.empty(); } - return Optional.empty(); + if (!CelConstant.isConstantValue(unwrappedResult)) { + // Evaluated result is not a constant. Leave the optional as is. + return Optional.empty(); + } + + CelMutableExpr newOptionalOfCall = + CelMutableExpr.ofCall( + CelMutableCall.create( + Function.OPTIONAL_OF.getFunction(), + CelMutableExpr.ofConstant(CelConstant.ofObjectValue(unwrappedResult)))); + + return Optional.of(astMutator.replaceSubtree(mutableAst, newOptionalOfCall, expr.id())); + } + + private static boolean isCallToFunction(CelMutableExpr expr, String functionName) { + return expr.getKind().equals(Kind.CALL) && expr.call().function().equals(functionName); } /** Inspects the non-strict calls to determine whether a branch can be removed. */ - private Optional maybePruneBranches( - CelAbstractSyntaxTree ast, CelExpr expr) { - if (!expr.exprKind().getKind().equals(Kind.CALL)) { + private Optional maybePruneBranches( + CelMutableAst mutableAst, CelMutableExpr expr) { + if (!expr.getKind().equals(Kind.CALL)) { return Optional.empty(); } - CelCall call = expr.call(); + CelMutableCall call = expr.call(); String function = call.function(); if (function.equals(Operator.LOGICAL_AND.getFunction()) || function.equals(Operator.LOGICAL_OR.getFunction())) { - return maybeShortCircuitCall(ast, expr); + return maybeShortCircuitCall(mutableAst, expr); } else if (function.equals(Operator.CONDITIONAL.getFunction())) { - CelExpr cond = call.args().get(0); - CelExpr truthy = call.args().get(1); - CelExpr falsy = call.args().get(2); - if (!cond.exprKind().getKind().equals(Kind.CONSTANT)) { + CelMutableExpr cond = call.args().get(0); + CelMutableExpr truthy = call.args().get(1); + CelMutableExpr falsy = call.args().get(2); + if (!cond.getKind().equals(Kind.CONSTANT)) { throw new IllegalStateException( - String.format( - "Expected constant condition. Got: %s instead.", cond.exprKind().getKind())); + String.format("Expected constant condition. Got: %s instead.", cond.getKind())); } - CelExpr result = cond.constant().booleanValue() ? truthy : falsy; + CelMutableExpr result = cond.constant().booleanValue() ? truthy : falsy; - return Optional.of(mutableAst.replaceSubtree(ast, result, expr.id())); + return Optional.of(astMutator.replaceSubtree(mutableAst, result, expr.id())); } else if (function.equals(Operator.IN.getFunction())) { - CelExpr callArg = call.args().get(1); - if (!callArg.exprKind().getKind().equals(Kind.CREATE_LIST)) { + CelMutableExpr callArg = call.args().get(1); + if (!callArg.getKind().equals(Kind.LIST)) { return Optional.empty(); } - CelCreateList haystack = callArg.createList(); + CelMutableList haystack = callArg.list(); if (haystack.elements().isEmpty()) { return Optional.of( - mutableAst.replaceSubtree( - ast, - CelExpr.newBuilder().setConstant(CelConstant.ofValue(false)).build(), - expr.id())); + astMutator.replaceSubtree( + mutableAst, CelMutableExpr.ofConstant(CelConstant.ofValue(false)), expr.id())); } - CelExpr needle = call.args().get(0); - if (needle.exprKind().getKind().equals(Kind.CONSTANT) - || needle.exprKind().getKind().equals(Kind.IDENT)) { + CelMutableExpr needle = call.args().get(0); + if (needle.getKind().equals(Kind.CONSTANT) || needle.getKind().equals(Kind.IDENT)) { Object needleValue = - needle.exprKind().getKind().equals(Kind.CONSTANT) ? needle.constant() : needle.ident(); - for (CelExpr elem : haystack.elements()) { - if (elem.constantOrDefault().equals(needleValue) - || elem.identOrDefault().equals(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( - mutableAst.replaceSubtree( - ast, - CelExpr.newBuilder().setConstant(CelConstant.ofValue(true)).build(), + astMutator.replaceSubtree( + mutableAst.expr(), + CelMutableExpr.ofConstant(CelConstant.ofValue(true)), expr.id())); } } } + } else if (function.equals(Operator.EQUALS.getFunction()) + || 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 invertCondition = function.equals(Operator.NOT_EQUALS.getFunction()); + Optional replacementExpr = Optional.empty(); + + if (lhs.getKind().equals(Kind.CONSTANT) && rhs.getKind().equals(Kind.CONSTANT)) { + // If both args are const, don't prune any branches and let maybeFold method evaluate this + // subExpr + return Optional.empty(); + } else if (lhsIsBoolean) { + boolean cond = invertCondition != lhs.constant().booleanValue(); + replacementExpr = + Optional.of( + cond + ? rhs + : CelMutableExpr.ofCall( + CelMutableCall.create(Operator.LOGICAL_NOT.getFunction(), rhs))); + } else if (rhsIsBoolean) { + boolean cond = invertCondition != rhs.constant().booleanValue(); + replacementExpr = + Optional.of( + cond + ? lhs + : CelMutableExpr.ofCall( + CelMutableCall.create(Operator.LOGICAL_NOT.getFunction(), lhs))); + } + + return replacementExpr.map(node -> astMutator.replaceSubtree(mutableAst, node, expr.id())); } return Optional.empty(); } - private Optional maybeShortCircuitCall( - CelAbstractSyntaxTree ast, CelExpr expr) { - CelCall call = expr.call(); + private Optional maybeShortCircuitCall( + CelMutableAst mutableAst, CelMutableExpr expr) { + CelMutableCall call = expr.call(); boolean shortCircuit = false; boolean skip = true; if (call.function().equals(Operator.LOGICAL_OR.getFunction())) { shortCircuit = true; skip = false; } - ImmutableList.Builder newArgsBuilder = new ImmutableList.Builder<>(); + ImmutableList.Builder newArgsBuilder = new ImmutableList.Builder<>(); - for (CelExpr arg : call.args()) { - if (!arg.exprKind().getKind().equals(Kind.CONSTANT)) { + for (CelMutableExpr arg : call.args()) { + if (!arg.getKind().equals(Kind.CONSTANT)) { newArgsBuilder.add(arg); continue; } @@ -375,16 +511,18 @@ private Optional maybeShortCircuitCall( } if (arg.constant().booleanValue() == shortCircuit) { - return Optional.of(mutableAst.replaceSubtree(ast, arg, expr.id())); + return Optional.of(astMutator.replaceSubtree(mutableAst, arg, expr.id())); } } - ImmutableList newArgs = newArgsBuilder.build(); + ImmutableList newArgs = newArgsBuilder.build(); if (newArgs.isEmpty()) { - return Optional.of(mutableAst.replaceSubtree(ast, call.args().get(0), expr.id())); + CelMutableExpr shortCircuitTarget = + call.args().get(0); // either args(0) or args(1) would work here + return Optional.of(astMutator.replaceSubtree(mutableAst, shortCircuitTarget, expr.id())); } if (newArgs.size() == 1) { - return Optional.of(mutableAst.replaceSubtree(ast, newArgs.get(0), expr.id())); + return Optional.of(astMutator.replaceSubtree(mutableAst, newArgs.get(0), expr.id())); } // TODO: Support folding variadic AND/ORs. @@ -392,66 +530,65 @@ private Optional maybeShortCircuitCall( "Folding variadic logical operator is not supported yet."); } - private CelAbstractSyntaxTree pruneOptionalElements(CelNavigableAst navigableAst) { - ImmutableList aggregateLiterals = - navigableAst - .getRoot() + private CelMutableAst pruneOptionalElements(CelMutableAst ast) { + ImmutableList aggregateLiterals = + CelNavigableMutableExpr.fromExpr(ast.expr()) .allNodes() .filter( node -> - node.getKind().equals(Kind.CREATE_LIST) - || node.getKind().equals(Kind.CREATE_MAP) - || node.getKind().equals(Kind.CREATE_STRUCT)) - .map(CelNavigableExpr::expr) + node.getKind().equals(Kind.LIST) + || node.getKind().equals(Kind.MAP) + || node.getKind().equals(Kind.STRUCT)) + .map(CelNavigableMutableExpr::expr) .collect(toImmutableList()); - CelAbstractSyntaxTree ast = navigableAst.getAst(); - for (CelExpr expr : aggregateLiterals) { - switch (expr.exprKind().getKind()) { - case CREATE_LIST: + for (CelMutableExpr expr : aggregateLiterals) { + switch (expr.getKind()) { + case LIST: ast = pruneOptionalListElements(ast, expr); break; - case CREATE_MAP: + case MAP: ast = pruneOptionalMapElements(ast, expr); break; - case CREATE_STRUCT: + case STRUCT: ast = pruneOptionalStructElements(ast, expr); break; default: - throw new IllegalArgumentException("Unexpected exprKind: " + expr.exprKind()); + throw new IllegalArgumentException("Unexpected exprKind: " + expr.getKind()); } } + return ast; } - private CelAbstractSyntaxTree pruneOptionalListElements(CelAbstractSyntaxTree ast, CelExpr expr) { - CelCreateList createList = expr.createList(); - if (createList.optionalIndices().isEmpty()) { - return ast; + private CelMutableAst pruneOptionalListElements(CelMutableAst mutableAst, CelMutableExpr expr) { + CelMutableList list = expr.list(); + if (list.optionalIndices().isEmpty()) { + return mutableAst; } - HashSet optionalIndices = new HashSet<>(createList.optionalIndices()); - ImmutableList.Builder updatedElemBuilder = new ImmutableList.Builder<>(); + HashSet optionalIndices = new HashSet<>(list.optionalIndices()); + ImmutableList.Builder updatedElemBuilder = new ImmutableList.Builder<>(); ImmutableList.Builder updatedIndicesBuilder = new ImmutableList.Builder<>(); int newOptIndex = -1; - for (int i = 0; i < createList.elements().size(); i++) { + for (int i = 0; i < list.elements().size(); i++) { newOptIndex++; - CelExpr element = createList.elements().get(i); + CelMutableExpr element = list.elements().get(i); if (!optionalIndices.contains(i)) { updatedElemBuilder.add(element); continue; } - if (element.exprKind().getKind().equals(Kind.CALL)) { - CelCall call = element.call(); + if (element.getKind().equals(Kind.CALL)) { + CelMutableCall call = element.call(); if (call.function().equals(Function.OPTIONAL_NONE.getFunction())) { // Skip optional.none. // Skipping causes the list to get smaller. newOptIndex--; continue; } else if (call.function().equals(Function.OPTIONAL_OF.getFunction())) { - CelExpr arg = call.args().get(0); - if (arg.exprKind().getKind().equals(Kind.CONSTANT)) { + CelMutableExpr arg = call.args().get(0); + if (arg.getKind().equals(Kind.CONSTANT)) { updatedElemBuilder.add(call.args().get(0)); continue; } @@ -462,27 +599,22 @@ private CelAbstractSyntaxTree pruneOptionalListElements(CelAbstractSyntaxTree as updatedIndicesBuilder.add(newOptIndex); } - return mutableAst.replaceSubtree( - ast, - CelExpr.newBuilder() - .setCreateList( - CelCreateList.newBuilder() - .addElements(updatedElemBuilder.build()) - .addOptionalIndices(updatedIndicesBuilder.build()) - .build()) - .build(), + return astMutator.replaceSubtree( + mutableAst, + CelMutableExpr.ofList( + CelMutableList.create(updatedElemBuilder.build(), updatedIndicesBuilder.build())), expr.id()); } - private CelAbstractSyntaxTree pruneOptionalMapElements(CelAbstractSyntaxTree ast, CelExpr expr) { - CelCreateMap createMap = expr.createMap(); - ImmutableList.Builder updatedEntryBuilder = new ImmutableList.Builder<>(); + private CelMutableAst pruneOptionalMapElements(CelMutableAst ast, CelMutableExpr expr) { + CelMutableMap map = expr.map(); + ImmutableList.Builder updatedEntryBuilder = new ImmutableList.Builder<>(); boolean modified = false; - for (CelCreateMap.Entry entry : createMap.entries()) { - CelExpr key = entry.key(); - Kind keyKind = key.exprKind().getKind(); - CelExpr value = entry.value(); - Kind valueKind = value.exprKind().getKind(); + for (CelMutableMap.Entry entry : map.entries()) { + CelMutableExpr key = entry.key(); + Kind keyKind = key.getKind(); + CelMutableExpr value = entry.value(); + Kind valueKind = value.getKind(); if (!entry.optionalEntry() || !keyKind.equals(Kind.CONSTANT) || !valueKind.equals(Kind.CALL)) { @@ -490,17 +622,18 @@ private CelAbstractSyntaxTree pruneOptionalMapElements(CelAbstractSyntaxTree ast continue; } - CelCall call = value.call(); + CelMutableCall call = value.call(); if (call.function().equals(Function.OPTIONAL_NONE.getFunction())) { // Skip the element. This is resolving an optional.none: ex {?1: optional.none()}. modified = true; continue; } else if (call.function().equals(Function.OPTIONAL_OF.getFunction())) { - CelExpr arg = call.args().get(0); - if (arg.exprKind().getKind().equals(Kind.CONSTANT)) { + CelMutableExpr arg = call.args().get(0); + if (arg.getKind().equals(Kind.CONSTANT)) { modified = true; - updatedEntryBuilder.add( - entry.toBuilder().setOptionalEntry(false).setValue(call.args().get(0)).build()); + entry.setOptionalEntry(false); + entry.setValue(call.args().get(0)); + updatedEntryBuilder.add(entry); continue; } } @@ -509,44 +642,39 @@ private CelAbstractSyntaxTree pruneOptionalMapElements(CelAbstractSyntaxTree ast } if (modified) { - return mutableAst.replaceSubtree( - ast, - CelExpr.newBuilder() - .setCreateMap( - CelCreateMap.newBuilder().addEntries(updatedEntryBuilder.build()).build()) - .build(), - expr.id()); + return astMutator.replaceSubtree( + ast, CelMutableExpr.ofMap(CelMutableMap.create(updatedEntryBuilder.build())), expr.id()); } return ast; } - private CelAbstractSyntaxTree pruneOptionalStructElements( - CelAbstractSyntaxTree ast, CelExpr expr) { - CelCreateStruct createStruct = expr.createStruct(); - ImmutableList.Builder updatedEntryBuilder = + private CelMutableAst pruneOptionalStructElements(CelMutableAst ast, CelMutableExpr expr) { + CelMutableStruct struct = expr.struct(); + ImmutableList.Builder updatedEntryBuilder = new ImmutableList.Builder<>(); boolean modified = false; - for (CelCreateStruct.Entry entry : createStruct.entries()) { - CelExpr value = entry.value(); - Kind valueKind = value.exprKind().getKind(); + for (CelMutableStruct.Entry entry : struct.entries()) { + CelMutableExpr value = entry.value(); + Kind valueKind = value.getKind(); if (!entry.optionalEntry() || !valueKind.equals(Kind.CALL)) { // Preserve the entry as is updatedEntryBuilder.add(entry); continue; } - CelCall call = value.call(); + CelMutableCall call = value.call(); if (call.function().equals(Function.OPTIONAL_NONE.getFunction())) { // Skip the element. This is resolving an optional.none: ex msg{?field: optional.none()}. modified = true; continue; } else if (call.function().equals(Function.OPTIONAL_OF.getFunction())) { - CelExpr arg = call.args().get(0); - if (arg.exprKind().getKind().equals(Kind.CONSTANT)) { + CelMutableExpr arg = call.args().get(0); + if (arg.getKind().equals(Kind.CONSTANT)) { modified = true; - updatedEntryBuilder.add( - entry.toBuilder().setOptionalEntry(false).setValue(call.args().get(0)).build()); + entry.setOptionalEntry(false); + entry.setValue(call.args().get(0)); + updatedEntryBuilder.add(entry); continue; } } @@ -555,36 +683,73 @@ private CelAbstractSyntaxTree pruneOptionalStructElements( } if (modified) { - return mutableAst.replaceSubtree( + return astMutator.replaceSubtree( ast, - CelExpr.newBuilder() - .setCreateStruct( - CelCreateStruct.newBuilder() - .setMessageName(createStruct.messageName()) - .addEntries(updatedEntryBuilder.build()) - .build()) - .build(), + CelMutableExpr.ofStruct( + CelMutableStruct.create(struct.messageName(), updatedEntryBuilder.build())), expr.id()); } return ast; } + @CanIgnoreReturnValue + 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( + CelMutableExprConverter.fromMutableExpr(navigableMutableExpr.expr()), + CelSource.newBuilder().build()); + ast = cel.check(ast).getAst(); + + return cel.createProgram(ast).eval(PartialVars.of(attributePatterns)); + } + /** Options to configure how Constant Folding behave. */ @AutoValue public abstract static class ConstantFoldingOptions { public abstract int maxIterationLimit(); + public abstract ImmutableSet foldableFunctions(); + /** Builder for configuring the {@link ConstantFoldingOptions}. */ @AutoValue.Builder public abstract static class Builder { + abstract ImmutableSet.Builder foldableFunctionsBuilder(); + /** * Limit the number of iteration while performing constant folding. An exception is thrown if * the iteration count exceeds the set value. */ public abstract Builder maxIterationLimit(int value); + /** + * Adds a collection of custom functions that will be a candidate for constant folding. By + * default, standard functions are foldable. + * + *

Note that the implementation of custom functions must be free of side effects. + */ + @CanIgnoreReturnValue + public Builder addFoldableFunctions(Iterable functions) { + checkNotNull(functions); + this.foldableFunctionsBuilder().addAll(functions); + return this; + } + + /** See {@link #addFoldableFunctions(Iterable)}. */ + @CanIgnoreReturnValue + public Builder addFoldableFunctions(String... functions) { + return addFoldableFunctions(Arrays.asList(functions)); + } + public abstract ConstantFoldingOptions build(); Builder() {} @@ -599,8 +764,17 @@ public static Builder newBuilder() { ConstantFoldingOptions() {} } + private static boolean isExprConstantOfKind(CelMutableExpr expr, CelConstant.Kind constantKind) { + return expr.getKind().equals(Kind.CONSTANT) && expr.constant().getKind().equals(constantKind); + } + private ConstantFoldingOptimizer(ConstantFoldingOptions constantFoldingOptions) { this.constantFoldingOptions = constantFoldingOptions; - this.mutableAst = MutableAst.newInstance(constantFoldingOptions.maxIterationLimit()); + this.astMutator = AstMutator.newInstance(constantFoldingOptions.maxIterationLimit()); + this.foldableFunctions = + ImmutableSet.builder() + .addAll(DefaultOptimizerConstants.CEL_CANONICAL_FUNCTIONS) + .addAll(constantFoldingOptions.foldableFunctions()) + .build(); } } diff --git a/optimizer/src/main/java/dev/cel/optimizer/optimizers/DefaultOptimizerConstants.java b/optimizer/src/main/java/dev/cel/optimizer/optimizers/DefaultOptimizerConstants.java new file mode 100644 index 000000000..a9db48391 --- /dev/null +++ b/optimizer/src/main/java/dev/cel/optimizer/optimizers/DefaultOptimizerConstants.java @@ -0,0 +1,50 @@ +// 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. + +package dev.cel.optimizer.optimizers; + +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 dev.cel.checker.CelStandardDeclarations; +import dev.cel.common.Operator; +import dev.cel.extensions.CelExtensions; +import dev.cel.extensions.CelOptionalLibrary; +import dev.cel.extensions.CelOptionalLibrary.Function; + +/** + * Package-private class that holds constants that's generally applicable across canonical + * optimizers provided from CEL. + */ +final class DefaultOptimizerConstants { + + /** + * List of function names from standard functions and extension libraries. These are free of side + * effects, thus amenable for optimization. + */ + static final ImmutableSet CEL_CANONICAL_FUNCTIONS = + ImmutableSet.builder() + .addAll(CelStandardDeclarations.getAllFunctionNames()) + .addAll( + Streams.concat( + stream(Operator.values()).map(Operator::getFunction), + stream(CelOptionalLibrary.Function.values()).map(Function::getFunction)) + .collect(toImmutableSet())) + .addAll(CelExtensions.getAllFunctionNames()) + .build(); + + private DefaultOptimizerConstants() {} +} diff --git a/optimizer/src/main/java/dev/cel/optimizer/optimizers/InliningOptimizer.java b/optimizer/src/main/java/dev/cel/optimizer/optimizers/InliningOptimizer.java new file mode 100644 index 000000000..e4051f82f --- /dev/null +++ b/optimizer/src/main/java/dev/cel/optimizer/optimizers/InliningOptimizer.java @@ -0,0 +1,313 @@ +// 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.collect.ImmutableList.toImmutableList; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import com.google.common.primitives.UnsignedLong; +import dev.cel.bundle.Cel; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelMutableAst; +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.CelMutableStruct; +import dev.cel.common.navigation.CelNavigableMutableAst; +import dev.cel.common.navigation.CelNavigableMutableExpr; +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.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 + * their associated AST. + */ +public final class InliningOptimizer implements CelAstOptimizer { + + private final ImmutableList inlineVariables; + private final AstMutator astMutator; + + /** + * Creates a new {@code InliningOptimizer} with one or more {@link InlineVariable}s. + * + *

Note that the variables to be inlined can be a dependency to one other based on the supplied + * ordering. This allows for recursive inlining where a replacement value might itself contain + * variables that need to be inlined. + * + *

For example, given a source expression {@code "a + b"} and inline variables in the following + * order: + * + *

    + *
  • {@code {a: b, b: 2}}, result: {@code 2 + 2}. + *
  • {@code {b: 2, a: b}}, result: {@code b + 2}. + *
+ */ + public static InliningOptimizer newInstance(InlineVariable... inlineVariables) { + return newInstance(ImmutableList.copyOf(inlineVariables)); + } + + /** + * Creates a new {@code InliningOptimizer}. + * + * @see #newInstance(InlineVariable...) + */ + public static InliningOptimizer newInstance(List inlineVariables) { + return newInstance(InliningOptions.newBuilder().build(), ImmutableList.copyOf(inlineVariables)); + } + + /** + * Creates a new {@code InliningOptimizer}. + * + * @see #newInstance(InlineVariable...) + * @param options {@link InliningOptions} to customize the inlining behavior with. + */ + public static InliningOptimizer newInstance( + InliningOptions options, InlineVariable... inlineVariables) { + return newInstance(options, ImmutableList.copyOf(inlineVariables)); + } + + /** + * Creates a new {@code InliningOptimizer}. + * + * @see #newInstance(InlineVariable...) + * @param options {@link InliningOptions} to customize the inlining behavior with. + */ + public static InliningOptimizer newInstance( + InliningOptions options, List inlineVariables) { + return new InliningOptimizer(options, ImmutableList.copyOf(inlineVariables)); + } + + @Override + public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel) { + CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + for (InlineVariable inlineVariable : inlineVariables) { + ImmutableList inlinableExprs = + CelNavigableMutableAst.fromAst(mutableAst) + .getRoot() + .allNodes() + .filter(node -> canInline(node, inlineVariable.name())) + .collect(toImmutableList()); + + for (CelNavigableMutableExpr inlinableExpr : inlinableExprs) { + CelMutableAst inlineVariableAst = CelMutableAst.fromCelAst(inlineVariable.ast()); + CelMutableExpr replacementExpr = inlineVariableAst.expr(); + + if (inlinableExpr.getKind().equals(Kind.SELECT) + && inlinableExpr.expr().select().testOnly()) { + replacementExpr = rewritePresenceExpr(inlineVariable, replacementExpr); + } + + mutableAst = + astMutator.replaceSubtree( + mutableAst, + CelMutableAst.of(replacementExpr, inlineVariableAst.source()), + inlinableExpr.id()); + } + } + + return OptimizationResult.create(astMutator.renumberIdsConsecutively(mutableAst).toParsedAst()); + } + + private static CelMutableExpr rewritePresenceExpr( + InlineVariable inlineVariable, CelMutableExpr replacementExpr) { + if (replacementExpr.getKind().equals(Kind.SELECT)) { + // Preserve testOnly property for Select replacements (has(A) -> has(B)) + replacementExpr.select().setTestOnly(true); + return replacementExpr; + } + + CelType replacementType = + inlineVariable + .ast() + .getType(replacementExpr.id()) + .orElseThrow(() -> new NoSuchElementException("Type is not present.")); + + if (isSizerType(replacementType)) { + // has(X) -> X.size() != 0 + return createNotEquals( + CelMutableExpr.ofCall(CelMutableCall.create(replacementExpr, "size")), + CelMutableExpr.ofConstant(CelConstant.ofValue(0))); + } + + if (replacementType.isAssignableFrom(SimpleType.NULL_TYPE)) { + // has(X) -> X != null + // This covers well-known wrapper types + + return createNotEquals( + replacementExpr, CelMutableExpr.ofConstant(CelConstant.ofValue(NullValue.NULL_VALUE))); + } + + return getZeroValueExpr(replacementType, replacementExpr) + .map(zeroValue -> createNotEquals(replacementExpr, zeroValue)) + .orElseThrow( + () -> + new IllegalArgumentException( + String.format( + "Unable to inline expression type %s into presence test", + replacementType.name()))); + } + + private static Optional getZeroValueExpr( + CelType type, CelMutableExpr replacementExpr) { + switch (type.kind()) { + case BOOL: + return Optional.of(CelMutableExpr.ofConstant(CelConstant.ofValue(false))); + case DOUBLE: + return Optional.of(CelMutableExpr.ofConstant(CelConstant.ofValue(0.0d))); + case INT: + return Optional.of(CelMutableExpr.ofConstant(CelConstant.ofValue(0))); + case UINT: + return Optional.of(CelMutableExpr.ofConstant(CelConstant.ofValue(UnsignedLong.ZERO))); + case TIMESTAMP: + return Optional.of( + CelMutableExpr.ofCall( + CelMutableCall.create( + "timestamp", CelMutableExpr.ofConstant(CelConstant.ofValue(0))))); + case DURATION: + return Optional.of( + CelMutableExpr.ofCall( + CelMutableCall.create( + "duration", CelMutableExpr.ofConstant(CelConstant.ofValue("0"))))); + case STRUCT: + return Optional.of( + CelMutableExpr.ofStruct( + CelMutableStruct.create( + replacementExpr.struct().messageName(), new ArrayList<>()))); + default: + return Optional.empty(); + } + } + + private static CelMutableExpr createNotEquals(CelMutableExpr left, CelMutableExpr right) { + return CelMutableExpr.ofCall( + CelMutableCall.create(Operator.NOT_EQUALS.getFunction(), left, right)); + } + + private static boolean isSizerType(CelType type) { + return type.kind().equals(CelKind.LIST) + || type.kind().equals(CelKind.MAP) + || type.equals(SimpleType.STRING) + || type.equals(SimpleType.BYTES); + } + + private static boolean canInline(CelNavigableMutableExpr node, String identifier) { + boolean matches = maybeToQualifiedName(node).map(name -> name.equals(identifier)).orElse(false); + + if (!matches) { + 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; + } + + private static Optional maybeToQualifiedName(CelNavigableMutableExpr node) { + if (node.getKind().equals(Kind.IDENT)) { + return Optional.of(node.expr().ident().name()); + } + + if (node.getKind().equals(Kind.SELECT)) { + return node.children() + .findFirst() + .flatMap(InliningOptimizer::maybeToQualifiedName) + .map(operandName -> operandName + "." + node.expr().select().field()); + } + + return Optional.empty(); + } + + /** Represents a variable to be inlined. */ + @AutoValue + public abstract static class InlineVariable { + public abstract String name(); + + public abstract CelAbstractSyntaxTree ast(); + + /** + * Creates a new {@link InlineVariable} with the given name and AST. + * + *

The name must be a simple identifier or a qualified name (e.g. "a.b.c") and cannot be an + * internal variable (starting with @). + */ + public static InlineVariable of(String name, CelAbstractSyntaxTree ast) { + if (name.startsWith("@")) { + throw new IllegalArgumentException("Internal variables cannot be inlined: " + name); + } + return new AutoValue_InliningOptimizer_InlineVariable(name, ast); + } + } + + /** Options to configure how Inlining behaves. */ + @AutoValue + public abstract static class InliningOptions { + public abstract int maxIterationLimit(); + + /** Builder for configuring the {@link InliningOptimizer.InliningOptions}. */ + @AutoValue.Builder + public abstract static class Builder { + + /** + * Limit the number of iteration while inlining variables. An exception is thrown if the + * iteration count exceeds the set value. + */ + public abstract InliningOptions.Builder maxIterationLimit(int value); + + public abstract InliningOptimizer.InliningOptions build(); + + Builder() {} + } + + /** Returns a new options builder with recommended defaults pre-configured. */ + public static InliningOptimizer.InliningOptions.Builder newBuilder() { + return new AutoValue_InliningOptimizer_InliningOptions.Builder().maxIterationLimit(400); + } + + InliningOptions() {} + } + + private InliningOptimizer( + InliningOptions options, ImmutableList inlineVariables) { + this.inlineVariables = inlineVariables; + this.astMutator = AstMutator.newInstance(options.maxIterationLimit()); + } +} 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 a8e431ab9..ce9a5dc77 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/optimizers/SubexpressionOptimizer.java +++ b/optimizer/src/main/java/dev/cel/optimizer/optimizers/SubexpressionOptimizer.java @@ -17,20 +17,23 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableSet.toImmutableSet; -import static java.util.Arrays.stream; +import static java.util.stream.Collectors.toCollection; import com.google.auto.value.AutoValue; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.google.common.base.Verify; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Iterables; import com.google.common.collect.Streams; import com.google.errorprone.annotations.CanIgnoreReturnValue; +import dev.cel.bundle.Cel; import dev.cel.bundle.CelBuilder; -import dev.cel.checker.Standard; import dev.cel.common.CelAbstractSyntaxTree; 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; @@ -39,27 +42,29 @@ import dev.cel.common.CelValidationException; import dev.cel.common.CelVarDecl; import dev.cel.common.ast.CelExpr; -import dev.cel.common.ast.CelExpr.CelIdent; +import dev.cel.common.ast.CelExpr.CelCall; +import dev.cel.common.ast.CelExpr.CelComprehension; +import dev.cel.common.ast.CelExpr.CelList; import dev.cel.common.ast.CelExpr.ExprKind.Kind; -import dev.cel.common.navigation.CelNavigableAst; +import dev.cel.common.ast.CelMutableExpr; +import dev.cel.common.ast.CelMutableExpr.CelMutableComprehension; +import dev.cel.common.ast.CelMutableExprConverter; import dev.cel.common.navigation.CelNavigableExpr; -import dev.cel.common.navigation.CelNavigableExpr.TraversalOrder; +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.ListType; import dev.cel.common.types.SimpleType; -import dev.cel.extensions.CelOptionalLibrary; -import dev.cel.extensions.CelOptionalLibrary.Function; +import dev.cel.optimizer.AstMutator; +import dev.cel.optimizer.AstMutator.MangledComprehensionAst; import dev.cel.optimizer.CelAstOptimizer; -import dev.cel.optimizer.MutableAst; -import dev.cel.optimizer.MutableAst.MangledComprehensionAst; -import dev.cel.parser.Operator; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; +import java.util.Comparator; import java.util.HashSet; import java.util.List; -import java.util.NoSuchElementException; -import java.util.Optional; +import java.util.Set; import java.util.stream.Stream; /** @@ -86,25 +91,22 @@ * } * */ -public class SubexpressionOptimizer implements CelAstOptimizer { - private static final ImmutableSet CSE_DEFAULT_ELIMINABLE_FUNCTIONS = - Streams.concat( - stream(Operator.values()).map(Operator::getFunction), - stream(Standard.Function.values()).map(Standard.Function::getFunction), - stream(CelOptionalLibrary.Function.values()).map(Function::getFunction)) - .collect(toImmutableSet()); +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 MANGLED_COMPREHENSION_IDENTIFIER_PREFIX = "@c"; - private static final String MANGLED_COMPREHENSION_RESULT_PREFIX = "@x"; 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); + @VisibleForTesting static final String MANGLED_COMPREHENSION_ITER_VAR_PREFIX = "@it"; + @VisibleForTesting static final String MANGLED_COMPREHENSION_ITER_VAR2_PREFIX = "@it2"; + @VisibleForTesting static final String MANGLED_COMPREHENSION_ACCU_VAR_PREFIX = "@ac"; + private final SubexpressionOptimizerOptions cseOptions; - private final MutableAst mutableAst; + private final AstMutator astMutator; private final ImmutableSet cseEliminableFunctions; /** @@ -124,87 +126,74 @@ public static SubexpressionOptimizer newInstance(SubexpressionOptimizerOptions c } @Override - public CelAbstractSyntaxTree optimize(CelNavigableAst navigableAst, CelBuilder celBuilder) { - return cseOptions.enableCelBlock() - ? optimizeUsingCelBlock(navigableAst, celBuilder) - : optimizeUsingCelBind(navigableAst); + public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel) { + OptimizationResult result = optimizeUsingCelBlock(ast, cel); + + verifyOptimizedAstCorrectness(result.optimizedAst()); + + return result; } - private CelAbstractSyntaxTree optimizeUsingCelBlock( - CelNavigableAst navigableAst, CelBuilder celBuilder) { - // Retain the original expected result type, so that it can be reset in celBuilder at the end of - // the optimization pass. - CelType resultType = navigableAst.getAst().getResultType(); + private OptimizationResult optimizeUsingCelBlock(CelAbstractSyntaxTree ast, Cel cel) { + CelMutableAst astToModify = CelMutableAst.fromCelAst(ast); + if (!cseOptions.populateMacroCalls()) { + astToModify.source().clearMacroCalls(); + } + MangledComprehensionAst mangledComprehensionAst = - mutableAst.mangleComprehensionIdentifierNames( - navigableAst.getAst(), - MANGLED_COMPREHENSION_IDENTIFIER_PREFIX, - MANGLED_COMPREHENSION_RESULT_PREFIX); - CelAbstractSyntaxTree astToModify = mangledComprehensionAst.ast(); - CelSource sourceToModify = astToModify.getSource(); + astMutator.mangleComprehensionIdentifierNames( + astToModify, + MANGLED_COMPREHENSION_ITER_VAR_PREFIX, + MANGLED_COMPREHENSION_ITER_VAR2_PREFIX, + MANGLED_COMPREHENSION_ACCU_VAR_PREFIX); + astToModify = mangledComprehensionAst.mutableAst(); + CelMutableSource sourceToModify = astToModify.source(); int blockIdentifierIndex = 0; int iterCount; - ArrayList subexpressions = new ArrayList<>(); + ArrayList subexpressions = new ArrayList<>(); + for (iterCount = 0; iterCount < cseOptions.iterationLimit(); iterCount++) { - CelExpr cseCandidate = findCseCandidate(astToModify).map(CelNavigableExpr::expr).orElse(null); - if (cseCandidate == null) { + CelNavigableMutableAst navAst = CelNavigableMutableAst.fromAst(astToModify); + List cseCandidates = getCseCandidates(navAst); + if (cseCandidates.isEmpty()) { break; } - subexpressions.add(cseCandidate); - String blockIdentifier = BLOCK_INDEX_PREFIX + blockIdentifierIndex++; + subexpressions.add(cseCandidates.get(0)); - // Using the CSE candidate, fetch all semantically equivalent subexpressions ahead of time. - ImmutableList allCseCandidates = - getAllCseCandidatesStream(astToModify, cseCandidate).collect(toImmutableList()); + String blockIdentifier = BLOCK_INDEX_PREFIX + blockIdentifierIndex++; // Replace all CSE candidates with new block index identifier - for (CelExpr semanticallyEqualNode : allCseCandidates) { + for (CelMutableExpr cseCandidate : cseCandidates) { iterCount++; - // Refetch the candidate expr as mutating the AST could have renumbered its IDs. - CelExpr exprToReplace = - getAllCseCandidatesStream(astToModify, semanticallyEqualNode) - .findAny() - .orElseThrow( - () -> - new NoSuchElementException( - "No value present for expr ID: " + semanticallyEqualNode.id())); astToModify = - mutableAst.replaceSubtree( - astToModify, - CelExpr.newBuilder() - .setIdent(CelIdent.newBuilder().setName(blockIdentifier).build()) - .build(), - exprToReplace.id()); + 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); } - - sourceToModify = - sourceToModify.toBuilder() - .addAllMacroCalls(astToModify.getSource().getMacroCalls()) - .build(); - astToModify = CelAbstractSyntaxTree.newParsedAst(astToModify.getExpr(), sourceToModify); - - // Retain the existing macro calls in case if the block identifiers are replacing a subtree - // that contains a comprehension. - sourceToModify = astToModify.getSource(); } if (iterCount >= cseOptions.iterationLimit()) { throw new IllegalStateException("Max iteration count reached."); } - if (!cseOptions.populateMacroCalls()) { - astToModify = - CelAbstractSyntaxTree.newParsedAst(astToModify.getExpr(), CelSource.newBuilder().build()); - } - if (iterCount == 0) { // No modification has been made. - return astToModify; + return OptimizationResult.create(ast); } + ImmutableList.Builder newVarDecls = ImmutableList.builder(); + // Add all mangled comprehension identifiers to the environment, so that the subexpressions can // retain context to them. mangledComprehensionAst @@ -214,29 +203,160 @@ private CelAbstractSyntaxTree optimizeUsingCelBlock( type.iterVarType() .ifPresent( iterVarType -> - celBuilder.addVarDeclarations( + newVarDecls.add( CelVarDecl.newVarDeclaration(name.iterVarName(), iterVarType))); - type.resultType() + type.iterVar2Type() .ifPresent( - comprehensionResultType -> - celBuilder.addVarDeclarations( - CelVarDecl.newVarDeclaration( - name.resultName(), comprehensionResultType))); + iterVar2Type -> + newVarDecls.add( + CelVarDecl.newVarDeclaration(name.iterVar2Name(), iterVar2Type))); + + newVarDecls.add(CelVarDecl.newVarDeclaration(name.resultName(), type.resultType())); }); - // Type-check all sub-expressions then add them as block identifiers to the CEL environment - addBlockIdentsToEnv(celBuilder, subexpressions); + // Type-check all sub-expressions then create new block index identifiers. + newVarDecls.addAll(newBlockIndexVariableDeclarations(cel, newVarDecls.build(), subexpressions)); // Wrap the optimized expression in cel.block - celBuilder.addFunctionDeclarations(newCelBlockFunctionDecl(resultType)); astToModify = - mutableAst.wrapAstWithNewCelBlock(CEL_BLOCK_FUNCTION, astToModify, subexpressions); - astToModify = mutableAst.renumberIdsConsecutively(astToModify); + astMutator.wrapAstWithNewCelBlock(CEL_BLOCK_FUNCTION, astToModify, subexpressions); + astToModify = astMutator.renumberIdsConsecutively(astToModify); - // Restore the expected result type the environment had prior to optimization. - celBuilder.setResultType(resultType); + // Tag the AST with cel.block designated as an extension + CelAbstractSyntaxTree optimizedAst = tagAstExtension(astToModify.toParsedAst()); - return tagAstExtension(astToModify); + return OptimizationResult.create( + optimizedAst, + newVarDecls.build(), + ImmutableList.of(newCelBlockFunctionDecl(ast.getResultType()))); + } + + /** + * Asserts that the optimized AST has no correctness issues. + * + * @throws com.google.common.base.VerifyException if the optimized AST is malformed. + */ + @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()) { + 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); + } + + private static void verifyNoInvalidScopedMangledVariables(CelExpr celExpr) { + CelCall celBlockCall = celExpr.call(); + CelExpr blockBody = celBlockCall.args().get(1); + + ImmutableSet allMangledVariablesInBlockBody = + CelNavigableExpr.fromExpr(blockBody) + .allNodes() + .map(CelNavigableExpr::expr) + .flatMap(SubexpressionOptimizer::extractMangledNames) + .collect(toImmutableSet()); + + CelList blockIndices = celBlockCall.args().get(0).list(); + for (CelExpr blockIndex : blockIndices.elements()) { + ImmutableSet indexDeclaredCompVariables = + CelNavigableExpr.fromExpr(blockIndex) + .allNodes() + .map(CelNavigableExpr::expr) + .filter(expr -> expr.getKind() == Kind.COMPREHENSION) + .map(CelExpr::comprehension) + .flatMap(comp -> Stream.of(comp.iterVar(), comp.iterVar2())) + .filter(iter -> !Strings.isNullOrEmpty(iter)) + .collect(toImmutableSet()); + + boolean containsIllegalDeclaration = + CelNavigableExpr.fromExpr(blockIndex) + .allNodes() + .map(CelNavigableExpr::expr) + .filter(expr -> expr.getKind() == Kind.IDENT) + .map(expr -> expr.ident().name()) + .filter(SubexpressionOptimizer::isMangled) + .anyMatch( + ident -> + !indexDeclaredCompVariables.contains(ident) + && allMangledVariablesInBlockBody.contains(ident)); + + Verify.verify( + !containsIllegalDeclaration, + "Illegal declared reference to a comprehension variable found in block indices. Expr: %s", + celExpr); + } + } + + private static Stream extractMangledNames(CelExpr expr) { + if (expr.getKind().equals(Kind.IDENT)) { + String name = expr.ident().name(); + return isMangled(name) ? Stream.of(name) : Stream.empty(); + } + if (expr.getKind().equals(Kind.COMPREHENSION)) { + CelComprehension comp = expr.comprehension(); + return Stream.of(comp.iterVar(), comp.iterVar2(), comp.accuVar()) + .filter(x -> !Strings.isNullOrEmpty(x)) + .filter(SubexpressionOptimizer::isMangled); + } + return Stream.empty(); + } + + private static boolean isMangled(String name) { + return name.startsWith(MANGLED_COMPREHENSION_ITER_VAR_PREFIX) + || name.startsWith(MANGLED_COMPREHENSION_ITER_VAR2_PREFIX); } private static CelAbstractSyntaxTree tagAstExtension(CelAbstractSyntaxTree ast) { @@ -248,20 +368,27 @@ private static CelAbstractSyntaxTree tagAstExtension(CelAbstractSyntaxTree ast) } /** - * Adds all subexpression as numbered identifiers that acts as an indexer to cel.block - * (ex: @index0, @index1..) Each subexpressions are type-checked, then its result type is used as - * the new identifiers' types. + * Creates a list of numbered identifiers from the subexpressions that act as an indexer to + * cel.block (ex: @index0, @index1..). Each subexpressions are type-checked, then its result type + * is used as the new identifiers' types. */ - private static void addBlockIdentsToEnv(CelBuilder celBuilder, List subexpressions) { + private static ImmutableList newBlockIndexVariableDeclarations( + Cel cel, ImmutableList mangledVarDecls, List subexpressions) { // The resulting type of the subexpressions will likely be different from the // entire expression's expected result type. - celBuilder.setResultType(SimpleType.DYN); + CelBuilder celBuilder = cel.toCelBuilder().setResultType(SimpleType.DYN); + // Add the mangled comprehension variables to the environment for type-checking subexpressions + // to succeed + celBuilder.addVarDeclarations(mangledVarDecls); + ImmutableList.Builder varDeclBuilder = ImmutableList.builder(); for (int i = 0; i < subexpressions.size(); i++) { - CelExpr subexpression = subexpressions.get(i); + CelMutableExpr subexpression = subexpressions.get(i); CelAbstractSyntaxTree subAst = - CelAbstractSyntaxTree.newParsedAst(subexpression, CelSource.newBuilder().build()); + CelAbstractSyntaxTree.newParsedAst( + CelMutableExprConverter.fromMutableExpr(subexpression), + CelSource.newBuilder().build()); try { subAst = celBuilder.build().check(subAst).getAst(); @@ -269,248 +396,233 @@ private static void addBlockIdentsToEnv(CelBuilder celBuilder, List sub throw new IllegalStateException("Failed to type-check subexpression", e); } - celBuilder.addVar("@index" + i, subAst.getResultType()); + CelVarDecl indexVar = CelVarDecl.newVarDeclaration("@index" + i, subAst.getResultType()); + celBuilder.addVarDeclarations(indexVar); + varDeclBuilder.add(indexVar); } - } - - private CelAbstractSyntaxTree optimizeUsingCelBind(CelNavigableAst navigableAst) { - CelAbstractSyntaxTree astToModify = - mutableAst - .mangleComprehensionIdentifierNames( - navigableAst.getAst(), - MANGLED_COMPREHENSION_IDENTIFIER_PREFIX, - MANGLED_COMPREHENSION_RESULT_PREFIX) - .ast(); - CelSource sourceToModify = astToModify.getSource(); - - int bindIdentifierIndex = 0; - int iterCount; - for (iterCount = 0; iterCount < cseOptions.iterationLimit(); iterCount++) { - CelExpr cseCandidate = findCseCandidate(astToModify).map(CelNavigableExpr::expr).orElse(null); - if (cseCandidate == null) { - break; - } - - String bindIdentifier = BIND_IDENTIFIER_PREFIX + bindIdentifierIndex; - bindIdentifierIndex++; - - // Using the CSE candidate, fetch all semantically equivalent subexpressions ahead of time. - ImmutableList allCseCandidates = - getAllCseCandidatesStream(astToModify, cseCandidate).collect(toImmutableList()); - - // Replace all CSE candidates with new bind identifier - for (CelExpr semanticallyEqualNode : allCseCandidates) { - iterCount++; - // Refetch the candidate expr as mutating the AST could have renumbered its IDs. - CelExpr exprToReplace = - getAllCseCandidatesStream(astToModify, semanticallyEqualNode) - .findAny() - .orElseThrow( - () -> - new NoSuchElementException( - "No value present for expr ID: " + semanticallyEqualNode.id())); - astToModify = - mutableAst.replaceSubtree( - astToModify, - CelExpr.newBuilder() - .setIdent(CelIdent.newBuilder().setName(bindIdentifier).build()) - .build(), - exprToReplace.id()); - } - - // Find LCA to insert the new cel.bind macro into. - CelNavigableExpr lca = getLca(astToModify, bindIdentifier); - - sourceToModify = - sourceToModify.toBuilder() - .addAllMacroCalls(astToModify.getSource().getMacroCalls()) - .build(); - astToModify = CelAbstractSyntaxTree.newParsedAst(astToModify.getExpr(), sourceToModify); - - // Insert the new bind call - astToModify = - mutableAst.replaceSubtreeWithNewBindMacro( - astToModify, bindIdentifier, cseCandidate, lca.expr(), lca.id()); + return varDeclBuilder.build(); + } - // Retain the existing macro calls in case if the bind identifiers are replacing a subtree - // that contains a comprehension. - sourceToModify = astToModify.getSource(); + private List getCseCandidates(CelNavigableMutableAst navAst) { + if (cseOptions.subexpressionMaxRecursionDepth() > 0) { + return getCseCandidatesWithRecursionDepth( + navAst, cseOptions.subexpressionMaxRecursionDepth()); + } else { + return getCseCandidatesWithCommonSubexpr(navAst); } + } - if (iterCount >= cseOptions.iterationLimit()) { - throw new IllegalStateException("Max iteration count reached."); + /** + * Retrieves all subexpr candidates based on the recursion limit even if there's no duplicate + * subexpr found. + */ + private List getCseCandidatesWithRecursionDepth( + CelNavigableMutableAst navAst, int recursionLimit) { + Preconditions.checkArgument(recursionLimit > 0); + Set ineligibleExprs = getIneligibleExprsFromComprehensionBranches(navAst); + ImmutableList descendants = + navAst + .getRoot() + .descendants(TraversalOrder.PRE_ORDER) + .filter(node -> node.height() <= recursionLimit) + .filter(node -> canEliminate(node, ineligibleExprs)) + .sorted(Comparator.comparingInt(CelNavigableMutableExpr::height).reversed()) + .collect(toImmutableList()); + if (descendants.isEmpty()) { + return new ArrayList<>(); } - if (!cseOptions.populateMacroCalls()) { - astToModify = - CelAbstractSyntaxTree.newParsedAst(astToModify.getExpr(), CelSource.newBuilder().build()); + List cseCandidates = getCseCandidatesWithCommonSubexpr(descendants); + if (!cseCandidates.isEmpty()) { + return cseCandidates; } - if (iterCount == 0) { - // No modification has been made. - return astToModify; + // If there's no common subexpr, just return the one with the highest height that's still below + // the recursion limit, but only if it actually needs to be extracted due to exceeding the + // recursion limit. + boolean astHasMoreExtractableSubexprs = + navAst + .getRoot() + .allNodes(TraversalOrder.POST_ORDER) + .filter(node -> node.height() > recursionLimit) + .anyMatch(node -> canEliminate(node, ineligibleExprs)); + if (astHasMoreExtractableSubexprs) { + cseCandidates.add(descendants.get(0).expr()); + return cseCandidates; } - astToModify = mutableAst.renumberIdsConsecutively(astToModify); - - return astToModify; + // The height of the remaining subexpression is already below the recursion limit. No need to + // extract. + return new ArrayList<>(); } - private Stream getAllCseCandidatesStream( - CelAbstractSyntaxTree ast, CelExpr cseCandidate) { - return CelNavigableAst.fromAst(ast) - .getRoot() - .allNodes() - .filter(this::canEliminate) - .map(CelNavigableExpr::expr) - .filter(expr -> areSemanticallyEqual(cseCandidate, expr)); + private List getCseCandidatesWithCommonSubexpr(CelNavigableMutableAst navAst) { + Set ineligibleExprs = getIneligibleExprsFromComprehensionBranches(navAst); + ImmutableList allNodes = + navAst + .getRoot() + .allNodes(TraversalOrder.PRE_ORDER) + .filter(node -> canEliminate(node, ineligibleExprs)) + .collect(toImmutableList()); + + return getCseCandidatesWithCommonSubexpr(allNodes); } - private static CelNavigableExpr getLca(CelAbstractSyntaxTree ast, String boundIdentifier) { - CelNavigableExpr root = CelNavigableAst.fromAst(ast).getRoot(); - ImmutableList allNodesWithIdentifier = - root.allNodes() - .filter(node -> node.expr().identOrDefault().name().equals(boundIdentifier)) - .collect(toImmutableList()); + private List getCseCandidatesWithCommonSubexpr( + ImmutableList allNodes) { + CelMutableExpr normalizedCseCandidate = null; + HashSet semanticallyEqualNodes = new HashSet<>(); + for (CelNavigableMutableExpr node : allNodes) { + // Normalize the expr to test semantic equivalence. + CelMutableExpr normalizedExpr = normalizeForEquality(node.expr()); + if (semanticallyEqualNodes.contains(normalizedExpr)) { + normalizedCseCandidate = normalizedExpr; + break; + } - if (allNodesWithIdentifier.size() < 2) { - throw new IllegalStateException("Expected at least 2 bound identifiers to be present."); + semanticallyEqualNodes.add(normalizedExpr); } - CelNavigableExpr lca = root; - long lcaAncestorCount = 0; - HashMap ancestors = new HashMap<>(); - for (CelNavigableExpr navigableExpr : allNodesWithIdentifier) { - Optional maybeParent = Optional.of(navigableExpr); - while (maybeParent.isPresent()) { - CelNavigableExpr parent = maybeParent.get(); - if (!ancestors.containsKey(parent.id())) { - ancestors.put(parent.id(), 1L); - continue; - } - - long ancestorCount = ancestors.get(parent.id()); - if (lcaAncestorCount < ancestorCount - || (lcaAncestorCount == ancestorCount && lca.depth() < parent.depth())) { - lca = parent; - lcaAncestorCount = ancestorCount; - } + List cseCandidates = new ArrayList<>(); + if (normalizedCseCandidate == null) { + return cseCandidates; + } - ancestors.put(parent.id(), ancestorCount + 1); - maybeParent = parent.parent(); + for (CelNavigableMutableExpr node : allNodes) { + // Normalize the expr to test semantic equivalence. + CelMutableExpr normalizedExpr = normalizeForEquality(node.expr()); + if (normalizedExpr.equals(normalizedCseCandidate)) { + cseCandidates.add(node.expr()); } } - return lca; + return cseCandidates; } - private Optional findCseCandidate(CelAbstractSyntaxTree ast) { - if (cseOptions.enableCelBlock() && cseOptions.subexpressionMaxRecursionDepth() > 0) { - return findCseCandidateWithRecursionDepth(ast, cseOptions.subexpressionMaxRecursionDepth()); - } else { - return findCseCandidateWithCommonSubexpr(ast); - } + private boolean canEliminate( + CelNavigableMutableExpr navigableExpr, Set ineligibleExprs) { + return !navigableExpr.getKind().equals(Kind.CONSTANT) + && !navigableExpr.getKind().equals(Kind.IDENT) + && !(navigableExpr.getKind().equals(Kind.IDENT) + && navigableExpr.expr().ident().name().startsWith(BIND_IDENTIFIER_PREFIX)) + // Exclude empty lists (cel.bind sets this for iterRange). + && !(navigableExpr.getKind().equals(Kind.LIST) + && navigableExpr.expr().list().elements().isEmpty()) + && containsEliminableFunctionOnly(navigableExpr) + && !ineligibleExprs.contains(navigableExpr.expr()) + && containsComprehensionIdentInSubexpr(navigableExpr) + && containsProperScopedComprehensionIdents(navigableExpr); } - /** - * This retrieves a subexpr candidate based on the recursion limit even if there's no duplicate - * subexpr found. - * - *

TODO: Improve the extraction logic using a suffix tree. - */ - private Optional findCseCandidateWithRecursionDepth( - CelAbstractSyntaxTree ast, int recursionLimit) { - Preconditions.checkArgument(recursionLimit > 0); - ImmutableList allNodes = - CelNavigableAst.fromAst(ast) - .getRoot() - .allNodes(TraversalOrder.POST_ORDER) - .filter(this::canEliminate) - .filter(node -> node.height() <= recursionLimit) - .filter(node -> !areSemanticallyEqual(ast.getExpr(), node.expr())) - .collect(toImmutableList()); - - if (allNodes.isEmpty()) { - return Optional.empty(); + private boolean containsProperScopedComprehensionIdents(CelNavigableMutableExpr navExpr) { + if (!navExpr.getKind().equals(Kind.COMPREHENSION)) { + return true; } - Optional commonSubexpr = findCseCandidateWithCommonSubexpr(allNodes); - if (commonSubexpr.isPresent()) { - return commonSubexpr; - } - // If there's no common subexpr, just return the one with the highest height that's still below - // the recursion limit. - return Optional.of(Iterables.getLast(allNodes)); - } + // For nested comprehensions of form [1].exists(x, [2].exists(y, x == y)), the inner + // comprehension [2].exists(y, x == y) + // should not be extracted out into a block index, as it causes issues with scoping. + ImmutableSet mangledIterVars = + navExpr + .descendants() + .filter(x -> x.getKind().equals(Kind.IDENT)) + .map(x -> x.expr().ident().name()) + .filter( + name -> + name.startsWith(MANGLED_COMPREHENSION_ITER_VAR_PREFIX) + || name.startsWith(MANGLED_COMPREHENSION_ITER_VAR2_PREFIX)) + .collect(toImmutableSet()); - private Optional findCseCandidateWithCommonSubexpr( - ImmutableList allNodes) { - HashSet encounteredNodes = new HashSet<>(); - for (CelNavigableExpr node : allNodes) { - // Normalize the expr to test semantic equivalence. - CelExpr celExpr = normalizeForEquality(node.expr()); - if (encounteredNodes.contains(celExpr)) { - return Optional.of(node); + CelNavigableMutableExpr parent = navExpr.parent().orElse(null); + while (parent != null) { + if (parent.getKind().equals(Kind.COMPREHENSION)) { + CelMutableComprehension comp = parent.expr().comprehension(); + boolean containsParentIterReferences = + mangledIterVars.contains(comp.iterVar()) || mangledIterVars.contains(comp.iterVar2()); + + if (containsParentIterReferences) { + return false; + } } - encounteredNodes.add(celExpr); + parent = parent.parent().orElse(null); } - return Optional.empty(); + return true; } - private Optional findCseCandidateWithCommonSubexpr(CelAbstractSyntaxTree ast) { - ImmutableList allNodes = - CelNavigableAst.fromAst(ast) - .getRoot() - .allNodes(TraversalOrder.PRE_ORDER) - .filter(this::canEliminate) + private boolean containsComprehensionIdentInSubexpr(CelNavigableMutableExpr navExpr) { + if (navExpr.getKind().equals(Kind.COMPREHENSION)) { + return true; + } + + ImmutableList comprehensionIdents = + navExpr + .allNodes() + .filter( + node -> { + if (!node.getKind().equals(Kind.IDENT)) { + return false; + } + + String identName = node.expr().ident().name(); + return identName.startsWith(MANGLED_COMPREHENSION_ITER_VAR_PREFIX) + || identName.startsWith(MANGLED_COMPREHENSION_ITER_VAR2_PREFIX) + || identName.startsWith(MANGLED_COMPREHENSION_ACCU_VAR_PREFIX); + }) .collect(toImmutableList()); - return findCseCandidateWithCommonSubexpr(allNodes); - } + if (comprehensionIdents.isEmpty()) { + return true; + } - private boolean canEliminate(CelNavigableExpr navigableExpr) { - return !navigableExpr.getKind().equals(Kind.CONSTANT) - && !navigableExpr.getKind().equals(Kind.IDENT) - && !navigableExpr.expr().identOrDefault().name().startsWith(BIND_IDENTIFIER_PREFIX) - && !navigableExpr.expr().selectOrDefault().testOnly() - && containsEliminableFunctionOnly(navigableExpr) - && isWithinInlineableComprehension(navigableExpr); - } + for (CelNavigableMutableExpr ident : comprehensionIdents) { + CelNavigableMutableExpr parent = ident.parent().orElse(null); + while (parent != null) { + if (parent.getKind().equals(Kind.COMPREHENSION)) { + return false; + } - private static boolean isWithinInlineableComprehension(CelNavigableExpr expr) { - Optional maybeParent = expr.parent(); - while (maybeParent.isPresent()) { - CelNavigableExpr parent = maybeParent.get(); - if (parent.getKind().equals(Kind.COMPREHENSION)) { - return Streams.concat( - // If the expression is within a comprehension, it is eligible for CSE iff is in - // result, loopStep or iterRange. While result is not human authored, it needs to be - // included to extract subexpressions that are already in cel.bind macro. - CelNavigableExpr.fromExpr(parent.expr().comprehension().result()).descendants(), - CelNavigableExpr.fromExpr(parent.expr().comprehension().loopStep()).allNodes(), - CelNavigableExpr.fromExpr(parent.expr().comprehension().iterRange()).allNodes()) - .filter( - node -> - // Exclude empty lists (cel.bind sets this for iterRange). - !node.getKind().equals(Kind.CREATE_LIST) - || !node.expr().createList().elements().isEmpty()) - .map(CelNavigableExpr::expr) - .anyMatch(node -> node.equals(expr.expr())); + parent = parent.parent().orElse(null); } - maybeParent = parent.parent(); } return true; } - private boolean areSemanticallyEqual(CelExpr expr1, CelExpr expr2) { - return normalizeForEquality(expr1).equals(normalizeForEquality(expr2)); + /** + * Collects a set of nodes that are not eligible to be optimized from comprehension branches. + * + *

All nodes from accumulator initializer and loop condition are not eligible to be optimized + * as that can interfere with scoping of shadowed variables. + */ + private static Set getIneligibleExprsFromComprehensionBranches( + CelNavigableMutableAst navAst) { + HashSet ineligibleExprs = new HashSet<>(); + navAst + .getRoot() + .allNodes() + .filter(node -> node.getKind().equals(Kind.COMPREHENSION)) + .forEach( + node -> { + Set nodes = + Streams.concat( + CelNavigableMutableExpr.fromExpr(node.expr().comprehension().accuInit()) + .allNodes(), + CelNavigableMutableExpr.fromExpr( + node.expr().comprehension().loopCondition()) + .allNodes()) + .map(CelNavigableMutableExpr::expr) + .collect(toCollection(HashSet::new)); + + ineligibleExprs.addAll(nodes); + }); + + return ineligibleExprs; } - private boolean containsEliminableFunctionOnly(CelNavigableExpr navigableExpr) { + private boolean containsEliminableFunctionOnly(CelNavigableMutableExpr navigableExpr) { return navigableExpr .allNodes() .allMatch( @@ -524,44 +636,16 @@ private boolean containsEliminableFunctionOnly(CelNavigableExpr navigableExpr) { } /** - * Converts the {@link CelExpr} to make it suitable for performing semantically equals check in - * {@link #areSemanticallyEqual(CelExpr, CelExpr)}. - * - *

Specifically, this will: + * Converts the {@link CelMutableExpr} to make it suitable for performing a semantically equals + * check. * - *

    - *
  • Set all expr IDs in the expression tree to 0. - *
  • Strip all presence tests (i.e: testOnly is marked as false on {@link - * CelExpr.ExprKind.Kind#SELECT} - *
+ *

Specifically, this will deep copy the mutable expr then set all expr IDs in the expression + * tree to 0. */ - private CelExpr normalizeForEquality(CelExpr celExpr) { - int iterCount; - for (iterCount = 0; iterCount < cseOptions.iterationLimit(); iterCount++) { - CelExpr presenceTestExpr = - CelNavigableExpr.fromExpr(celExpr) - .allNodes() - .map(CelNavigableExpr::expr) - .filter(expr -> expr.selectOrDefault().testOnly()) - .findAny() - .orElse(null); - if (presenceTestExpr == null) { - break; - } - - CelExpr newExpr = - presenceTestExpr.toBuilder() - .setSelect(presenceTestExpr.select().toBuilder().setTestOnly(false).build()) - .build(); - - celExpr = mutableAst.replaceSubtree(celExpr, newExpr, newExpr.id()); - } - - if (iterCount >= cseOptions.iterationLimit()) { - throw new IllegalStateException("Max iteration count reached."); - } + private CelMutableExpr normalizeForEquality(CelMutableExpr mutableExpr) { + CelMutableExpr copiedExpr = CelMutableExpr.newInstance(mutableExpr); - return mutableAst.clearExprIds(celExpr); + return astMutator.clearExprIds(copiedExpr); } @VisibleForTesting @@ -602,11 +686,9 @@ public abstract static class Builder { public abstract Builder populateMacroCalls(boolean value); /** - * Rewrites the optimized AST using cel.@block call instead of cascaded cel.bind macros, aimed - * to produce a more compact AST. {@link CelSource.Extension} field will be populated in the - * AST to inform that special runtime support is required to evaluate the optimized - * expression. + * @deprecated This option is a no-op. cel.@block is always enabled. */ + @Deprecated public abstract Builder enableCelBlock(boolean value); /** @@ -621,13 +703,14 @@ public abstract static class Builder { *

Note that expressions containing no common subexpressions may become a candidate for * extraction to satisfy the max depth requirement. * - *

This is a no-op if {@link #enableCelBlock} is set to false, or the configured value is - * less than 1. + *

This is a no-op if the configured value is less than 1, or no subexpression needs to be + * extracted because the entire expression is already under the designated limit. * *

Examples: * *

    *
  1. a.b.c with depth 1 -> cel.@block([x.b, @index0.c], @index1) + *
  2. a.b.c with depth 3 -> a.b.c *
  3. a.b + a.b.c.d with depth 3 -> cel.@block([a.b, @index0.c.d], @index0 + @index1) *
* @@ -667,8 +750,8 @@ public Builder addEliminableFunctions(String... functions) { public static Builder newBuilder() { return new AutoValue_SubexpressionOptimizer_SubexpressionOptimizerOptions.Builder() .iterationLimit(500) + .enableCelBlock(true) .populateMacroCalls(false) - .enableCelBlock(false) .subexpressionMaxRecursionDepth(0); } @@ -677,10 +760,10 @@ public static Builder newBuilder() { private SubexpressionOptimizer(SubexpressionOptimizerOptions cseOptions) { this.cseOptions = cseOptions; - this.mutableAst = MutableAst.newInstance(cseOptions.iterationLimit()); + this.astMutator = AstMutator.newInstance(cseOptions.iterationLimit()); this.cseEliminableFunctions = ImmutableSet.builder() - .addAll(CSE_DEFAULT_ELIMINABLE_FUNCTIONS) + .addAll(DefaultOptimizerConstants.CEL_CANONICAL_FUNCTIONS) .addAll(cseOptions.eliminableFunctions()) .build(); } diff --git a/optimizer/src/test/java/dev/cel/optimizer/AstMutatorTest.java b/optimizer/src/test/java/dev/cel/optimizer/AstMutatorTest.java new file mode 100644 index 000000000..fa896ebca --- /dev/null +++ b/optimizer/src/test/java/dev/cel/optimizer/AstMutatorTest.java @@ -0,0 +1,1090 @@ +// 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.optimizer; + +import static com.google.common.collect.ImmutableMap.toImmutableMap; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableMap; +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.CelMutableAst; +import dev.cel.common.CelOptions; +import dev.cel.common.CelOverloadDecl; +import dev.cel.common.CelSource; +import dev.cel.common.CelSource.Extension; +import dev.cel.common.CelSource.Extension.Version; +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.CelMutableList; +import dev.cel.common.ast.CelMutableExpr.CelMutableSelect; +import dev.cel.common.ast.CelMutableExprConverter; +import dev.cel.common.navigation.CelNavigableAst; +import dev.cel.common.navigation.CelNavigableExpr; +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.parser.CelStandardMacro; +import dev.cel.parser.CelUnparser; +import dev.cel.parser.CelUnparserFactory; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public class AstMutatorTest { + private static final Cel CEL = + CelFactory.standardCelBuilder() + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .setOptions(CelOptions.current().populateMacroCalls(true).build()) + .addMessageTypes(TestAllTypes.getDescriptor()) + .addCompilerLibraries( + CelOptionalLibrary.INSTANCE, CelExtensions.bindings(), CelExtensions.comprehensions()) + .addRuntimeLibraries(CelOptionalLibrary.INSTANCE, CelExtensions.comprehensions()) + .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) + .addVar("msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())) + .addVar("x", SimpleType.INT) + .build(); + + private static final CelUnparser CEL_UNPARSER = CelUnparserFactory.newUnparser(); + private static final AstMutator AST_MUTATOR = AstMutator.newInstance(1000); + + @Test + public void replaceSubtree_replaceConst() throws Exception { + CelAbstractSyntaxTree ast = CEL.compile("10").getAst(); + CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + CelMutableExpr newBooleanConst = CelMutableExpr.ofConstant(1, CelConstant.ofValue(true)); + + CelMutableAst result = + AST_MUTATOR.replaceSubtree(mutableAst, newBooleanConst, mutableAst.expr().id()); + + assertThat(result.toParsedAst().getExpr()) + .isEqualTo(CelExpr.ofConstant(3, CelConstant.ofValue(true))); + } + + @Test + public void astMutator_returnsParsedAst() throws Exception { + CelAbstractSyntaxTree ast = CEL.compile("10").getAst(); + CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + CelMutableExpr newBooleanConst = CelMutableExpr.ofConstant(1, CelConstant.ofValue(true)); + + CelMutableAst result = + AST_MUTATOR.replaceSubtree(mutableAst, newBooleanConst, mutableAst.expr().id()); + + assertThat(ast.isChecked()).isTrue(); + assertThat(result.toParsedAst().isChecked()).isFalse(); + } + + @Test + public void astMutator_nonMacro_sourceCleared() throws Exception { + CelAbstractSyntaxTree ast = CEL.compile("10").getAst(); + CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + CelMutableExpr newBooleanConst = CelMutableExpr.ofConstant(1, CelConstant.ofValue(true)); + + CelAbstractSyntaxTree mutatedAst = + AST_MUTATOR + .replaceSubtree(mutableAst, newBooleanConst, mutableAst.expr().id()) + .toParsedAst(); + + assertThat(mutatedAst.getSource().getLineOffsets()).isEmpty(); + assertThat(mutatedAst.getSource().getPositionsMap()).isEmpty(); + assertThat(mutatedAst.getSource().getExtensions()).isEmpty(); + assertThat(mutatedAst.getSource().getMacroCalls()).isEmpty(); + assertThat(mutatedAst.getSource().getDescription()).isEqualTo(ast.getSource().getDescription()); + } + + @Test + public void astMutator_macro_sourceMacroCallsPopulated() throws Exception { + CelAbstractSyntaxTree ast = CEL.compile("has(TestAllTypes{}.single_int32)").getAst(); + CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + CelMutableExpr newBooleanConst = CelMutableExpr.ofConstant(1, CelConstant.ofValue(true)); + + CelAbstractSyntaxTree mutatedAst = + AST_MUTATOR.replaceSubtree(mutableAst, newBooleanConst, 1).toParsedAst(); // no_op + + assertThat(mutatedAst.getSource().getLineOffsets()).isEmpty(); + assertThat(mutatedAst.getSource().getPositionsMap()).isEmpty(); + assertThat(mutatedAst.getSource().getExtensions()).isEmpty(); + assertThat(mutatedAst.getSource().getMacroCalls()).isNotEmpty(); + assertThat(mutatedAst.getSource().getDescription()).isEqualTo(ast.getSource().getDescription()); + } + + @Test + public void replaceSubtree_astContainsTaggedExtension_retained() throws Exception { + CelAbstractSyntaxTree ast = CEL.compile("has(TestAllTypes{}.single_int32)").getAst(); + Extension extension = Extension.create("test", Version.of(1, 1)); + CelSource celSource = ast.getSource().toBuilder().addAllExtensions(extension).build(); + ast = + CelAbstractSyntaxTree.newCheckedAst( + ast.getExpr(), celSource, ast.getReferenceMap(), ast.getTypeMap()); + CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + + CelAbstractSyntaxTree mutatedAst = + AST_MUTATOR + .replaceSubtree(mutableAst, CelMutableExpr.ofConstant(CelConstant.ofValue(true)), 1) + .toParsedAst(); + + assertThat(mutatedAst.getSource().getExtensions()).containsExactly(extension); + } + + @Test + public void replaceSubtreeWithNewAst_astsContainTaggedExtension_retained() throws Exception { + // Setup first AST with a test extension + CelAbstractSyntaxTree ast = CEL.compile("has(TestAllTypes{}.single_int32)").getAst(); + Extension extension = Extension.create("test", Version.of(1, 1)); + ast = + CelAbstractSyntaxTree.newCheckedAst( + ast.getExpr(), + ast.getSource().toBuilder().addAllExtensions(extension).build(), + ast.getReferenceMap(), + ast.getTypeMap()); + CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + // Setup second AST with another test extension + CelAbstractSyntaxTree astToReplaceWith = CEL.compile("cel.bind(a, true, a)").getAst(); + Extension extension2 = Extension.create("test2", Version.of(2, 2)); + astToReplaceWith = + CelAbstractSyntaxTree.newCheckedAst( + astToReplaceWith.getExpr(), + astToReplaceWith.getSource().toBuilder().addAllExtensions(extension2).build(), + astToReplaceWith.getReferenceMap(), + astToReplaceWith.getTypeMap()); + CelMutableAst mutableAst2 = CelMutableAst.fromCelAst(astToReplaceWith); + + // Mutate the original AST with the new AST at the root + CelAbstractSyntaxTree mutatedAst = + AST_MUTATOR.replaceSubtree(mutableAst, mutableAst2, mutableAst.expr().id()).toParsedAst(); + + // Expect that both the extensions are merged + assertThat(mutatedAst.getSource().getExtensions()).containsExactly(extension, extension2); + } + + @Test + public void replaceSubtreeWithNewAst_astsContainSameExtensions_deduped() throws Exception { + // Setup first AST with a test extension + CelAbstractSyntaxTree ast = CEL.compile("has(TestAllTypes{}.single_int32)").getAst(); + Extension extension = Extension.create("test", Version.of(1, 1)); + ast = + CelAbstractSyntaxTree.newCheckedAst( + ast.getExpr(), + ast.getSource().toBuilder().addAllExtensions(extension).build(), + ast.getReferenceMap(), + ast.getTypeMap()); + CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + // Setup second AST with the same test extension as above + CelAbstractSyntaxTree astToReplaceWith = CEL.compile("cel.bind(a, true, a)").getAst(); + Extension extension2 = Extension.create("test", Version.of(1, 1)); + astToReplaceWith = + CelAbstractSyntaxTree.newCheckedAst( + astToReplaceWith.getExpr(), + astToReplaceWith.getSource().toBuilder().addAllExtensions(extension2).build(), + astToReplaceWith.getReferenceMap(), + astToReplaceWith.getTypeMap()); + CelMutableAst mutableAst2 = CelMutableAst.fromCelAst(astToReplaceWith); + + // Mutate the original AST with the new AST at the root + CelAbstractSyntaxTree mutatedAst = + AST_MUTATOR.replaceSubtree(mutableAst, mutableAst2, mutableAst.expr().id()).toParsedAst(); + + // Expect that the extension is deduped + assertThat(mutatedAst.getSource().getExtensions()).containsExactly(extension); + } + + @Test + @TestParameters("{source: '[1].exists(x, x > 0)', expectedMacroCallSize: 1}") + @TestParameters( + "{source: '[1].exists(x, x > 0) && [2].exists(x, x > 0)', expectedMacroCallSize: 2}") + @TestParameters( + "{source: '[1].exists(x, [2].exists(y, x > 0 && y > x))', expectedMacroCallSize: 2}") + public void replaceSubtree_rootReplacedWithMacro_macroCallPopulated( + String source, int expectedMacroCallSize) throws Exception { + CelAbstractSyntaxTree ast = CEL.compile("1").getAst(); + CelAbstractSyntaxTree ast2 = CEL.compile(source).getAst(); + CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + CelMutableAst mutableAst2 = CelMutableAst.fromCelAst(ast2); + + CelAbstractSyntaxTree mutatedAst = + AST_MUTATOR.replaceSubtree(mutableAst, mutableAst2, mutableAst.expr().id()).toParsedAst(); + + assertThat(mutatedAst.getSource().getMacroCalls()).hasSize(expectedMacroCallSize); + assertThat(CEL_UNPARSER.unparse(mutatedAst)).isEqualTo(source); + assertThat(CEL.createProgram(CEL.check(mutatedAst).getAst()).eval()).isEqualTo(true); + } + + @Test + public void replaceSubtree_leftBranchReplacedWithMacro_macroCallPopulated() throws Exception { + CelAbstractSyntaxTree ast = CEL.compile("true && false").getAst(); + CelAbstractSyntaxTree ast2 = CEL.compile("[1].exists(x, x > 0)").getAst(); + CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + CelMutableAst mutableAst2 = CelMutableAst.fromCelAst(ast2); + + CelAbstractSyntaxTree mutatedAst = + AST_MUTATOR + .replaceSubtree(mutableAst, mutableAst2, 3) + .toParsedAst(); // Replace false with the macro expr + + assertThat(mutatedAst.getSource().getMacroCalls()).hasSize(1); + assertThat(CEL_UNPARSER.unparse(mutatedAst)).isEqualTo("true && [1].exists(x, x > 0)"); + assertThat(CEL.createProgram(CEL.check(mutatedAst).getAst()).eval()).isEqualTo(true); + } + + @Test + public void replaceSubtree_rightBranchReplacedWithMacro_macroCallPopulated() throws Exception { + CelAbstractSyntaxTree ast = CEL.compile("true && false").getAst(); + CelAbstractSyntaxTree ast2 = CEL.compile("[1].exists(x, x > 0)").getAst(); + CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + CelMutableAst mutableAst2 = CelMutableAst.fromCelAst(ast2); + + CelAbstractSyntaxTree mutatedAst = + AST_MUTATOR + .replaceSubtree(mutableAst, mutableAst2, 1) + .toParsedAst(); // Replace true with the macro expr + + assertThat(mutatedAst.getSource().getMacroCalls()).hasSize(1); + assertThat(CEL_UNPARSER.unparse(mutatedAst)).isEqualTo("[1].exists(x, x > 0) && false"); + assertThat(CEL.createProgram(CEL.check(mutatedAst).getAst()).eval()).isEqualTo(false); + } + + @Test + public void replaceSubtree_macroInsertedIntoExistingMacro_macroCallPopulated() throws Exception { + CelAbstractSyntaxTree ast = CEL.compile("[1].exists(x, x > 0 && true)").getAst(); + CelAbstractSyntaxTree ast2 = CEL.compile("[2].exists(y, y > 0)").getAst(); + CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + CelMutableAst mutableAst2 = CelMutableAst.fromCelAst(ast2); + + CelAbstractSyntaxTree mutatedAst = + AST_MUTATOR + .replaceSubtree(mutableAst, mutableAst2, 9) + .toParsedAst(); // Replace true with the ast2 maro expr + + assertThat(mutatedAst.getSource().getMacroCalls()).hasSize(2); + assertThat(CEL_UNPARSER.unparse(mutatedAst)) + .isEqualTo("[1].exists(x, x > 0 && [2].exists(y, y > 0))"); + assertThat(CEL.createProgram(CEL.check(mutatedAst).getAst()).eval()).isEqualTo(true); + } + + @Test + public void replaceSubtree_macroReplacedWithConstExpr_macroCallCleared() throws Exception { + CelAbstractSyntaxTree ast = + CEL.compile("[1].exists(x, x > 0) && [2].exists(x, x > 0)").getAst(); + CelAbstractSyntaxTree ast2 = CEL.compile("1").getAst(); + CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + CelMutableAst mutableAst2 = CelMutableAst.fromCelAst(ast2); + + CelAbstractSyntaxTree mutatedAst = + AST_MUTATOR.replaceSubtree(mutableAst, mutableAst2, mutableAst.expr().id()).toParsedAst(); + + assertThat(mutatedAst.getSource().getMacroCalls()).isEmpty(); + assertThat(CEL_UNPARSER.unparse(mutatedAst)).isEqualTo("1"); + assertThat(CEL.createProgram(CEL.check(mutatedAst).getAst()).eval()).isEqualTo(1); + } + + @Test + @SuppressWarnings("unchecked") // Test only + public void replaceSubtree_replaceExtraneousListCreatedByMacro_unparseSuccess() throws Exception { + // Certain macros such as `map` or `filter` generates an extraneous list_expr in the loop step's + // argument that does not exist in the original expression. + // For example, the loop step of this expression looks like: + // CALL [10] { + // function: _+_ + // args: { + // IDENT [8] { + // name: __result__ + // } + // LIST [9] { + // elements: { + // CONSTANT [5] { value: 1 } + // } + // } + // } + // } + CelAbstractSyntaxTree ast = CEL.compile("[1].map(x, 1)").getAst(); + CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + CelMutableAst mutableAst2 = CelMutableAst.fromCelAst(ast); + + // These two mutation are equivalent. + CelAbstractSyntaxTree mutatedAstWithList = + AST_MUTATOR + .replaceSubtree( + mutableAst, + CelMutableExpr.ofList( + CelMutableList.create(CelMutableExpr.ofConstant(CelConstant.ofValue(2L)))), + 9L) + .toParsedAst(); + CelAbstractSyntaxTree mutatedAstWithConstant = + AST_MUTATOR + .replaceSubtree(mutableAst2, CelMutableExpr.ofConstant(CelConstant.ofValue(2L)), 5L) + .toParsedAst(); + + assertThat(CEL_UNPARSER.unparse(mutatedAstWithList)).isEqualTo("[1].map(x, 2)"); + assertThat(CEL_UNPARSER.unparse(mutatedAstWithConstant)).isEqualTo("[1].map(x, 2)"); + assertThat((List) CEL.createProgram(CEL.check(mutatedAstWithList).getAst()).eval()) + .containsExactly(2L); + } + + @Test + @SuppressWarnings("unchecked") // Test only + public void replaceSubtree_replaceExtraneousListCreatedByThreeArgMacro_unparseSuccess() + throws Exception { + CelAbstractSyntaxTree ast = CEL.compile("[1].map(x, true, 1)").getAst(); + CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + CelMutableAst mutableAst2 = CelMutableAst.fromCelAst(ast); + + // These two mutation are equivalent. + CelAbstractSyntaxTree mutatedAstWithList = + AST_MUTATOR + .replaceSubtree( + mutableAst, + CelMutableExpr.ofList( + CelMutableList.create(CelMutableExpr.ofConstant(CelConstant.ofValue(2L)))), + 10L) + .toParsedAst(); + CelAbstractSyntaxTree mutatedAstWithConstant = + AST_MUTATOR + .replaceSubtree(mutableAst2, CelMutableExpr.ofConstant(CelConstant.ofValue(2L)), 6L) + .toParsedAst(); + + assertThat(CEL_UNPARSER.unparse(mutatedAstWithList)).isEqualTo("[1].map(x, true, 2)"); + assertThat(CEL_UNPARSER.unparse(mutatedAstWithConstant)).isEqualTo("[1].map(x, true, 2)"); + assertThat((List) CEL.createProgram(CEL.check(mutatedAstWithList).getAst()).eval()) + .containsExactly(2L); + } + + @Test + public void globalCallExpr_replaceRoot() throws Exception { + // Tree shape (brackets are expr IDs): + // + [4] + // + [2] x [5] + // 1 [1] 2 [3] + CelAbstractSyntaxTree ast = CEL.compile("1 + 2 + x").getAst(); + CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + CelMutableExpr newExpr = CelMutableExpr.ofConstant(CelConstant.ofValue(10)); + + CelMutableAst result = AST_MUTATOR.replaceSubtree(mutableAst, newExpr, mutableAst.expr().id()); + + assertThat(result.toParsedAst().getExpr()) + .isEqualTo(CelExpr.ofConstant(7, CelConstant.ofValue(10))); + } + + @Test + public void globalCallExpr_replaceLeaf() throws Exception { + // Tree shape (brackets are expr IDs): + // + [4] + // + [2] x [5] + // 1 [1] 2 [3] + CelAbstractSyntaxTree ast = CEL.compile("1 + 2 + x").getAst(); + CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + CelMutableExpr newExpr = CelMutableExpr.ofConstant(CelConstant.ofValue(10)); + + CelMutableAst result = AST_MUTATOR.replaceSubtree(mutableAst, newExpr, 1); + + assertThat(CEL_UNPARSER.unparse(result.toParsedAst())).isEqualTo("10 + 2 + x"); + } + + @Test + public void globalCallExpr_replaceMiddleBranch() throws Exception { + // Tree shape (brackets are expr IDs): + // + [4] + // + [2] x [5] + // 1 [1] 2 [3] + CelAbstractSyntaxTree ast = CEL.compile("1 + 2 + x").getAst(); + CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + CelMutableExpr newExpr = CelMutableExpr.ofConstant(CelConstant.ofValue(10)); + + CelMutableAst result = AST_MUTATOR.replaceSubtree(mutableAst, newExpr, 2); + + assertThat(CEL_UNPARSER.unparse(result.toParsedAst())).isEqualTo("10 + x"); + } + + @Test + public void globalCallExpr_replaceMiddleBranch_withCallExpr() throws Exception { + // Tree shape (brackets are expr IDs): + // + [4] + // + [2] x [5] + // 1 [1] 2 [3] + CelAbstractSyntaxTree ast = CEL.compile("1 + 2 + x").getAst(); + CelAbstractSyntaxTree ast2 = CEL.compile("4 + 5 + 6").getAst(); + CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + CelMutableExpr newExpr = CelMutableExprConverter.fromCelExpr(ast2.getExpr()); + + CelMutableAst result = AST_MUTATOR.replaceSubtree(mutableAst, newExpr, 2); + + assertThat(CEL_UNPARSER.unparse(result.toParsedAst())).isEqualTo("4 + 5 + 6 + x"); + } + + @Test + public void memberCallExpr_replaceLeafTarget() throws Exception { + // Tree shape (brackets are expr IDs): + // func [2] + // 10 [1] func [4] + // 4 [3] 5 [5] + Cel cel = + CelFactory.standardCelBuilder() + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "func", + CelOverloadDecl.newMemberOverload( + "func_overload", SimpleType.INT, SimpleType.INT, SimpleType.INT))) + .build(); + CelAbstractSyntaxTree ast = cel.compile("10.func(4.func(5))").getAst(); + CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + CelMutableExpr newExpr = CelMutableExpr.ofConstant(CelConstant.ofValue(20)); + + CelMutableAst result = AST_MUTATOR.replaceSubtree(mutableAst, newExpr, 3); + + assertThat(CEL_UNPARSER.unparse(result.toParsedAst())).isEqualTo("10.func(20.func(5))"); + } + + @Test + public void memberCallExpr_replaceLeafArgument() throws Exception { + // Tree shape (brackets are expr IDs): + // func [2] + // 10 [1] func [4] + // 4 [3] 5 [5] + Cel cel = + CelFactory.standardCelBuilder() + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "func", + CelOverloadDecl.newMemberOverload( + "func_overload", SimpleType.INT, SimpleType.INT, SimpleType.INT))) + .build(); + CelAbstractSyntaxTree ast = cel.compile("10.func(4.func(5))").getAst(); + CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + CelMutableExpr newExpr = CelMutableExpr.ofConstant(CelConstant.ofValue(20)); + + CelMutableAst result = AST_MUTATOR.replaceSubtree(mutableAst, newExpr, 5); + + assertThat(CEL_UNPARSER.unparse(result.toParsedAst())).isEqualTo("10.func(4.func(20))"); + } + + @Test + public void memberCallExpr_replaceMiddleBranchTarget() throws Exception { + // Tree shape (brackets are expr IDs): + // func [2] + // 10 [1] func [4] + // 4 [3] 5 [5] + Cel cel = + CelFactory.standardCelBuilder() + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "func", + CelOverloadDecl.newMemberOverload( + "func_overload", SimpleType.INT, SimpleType.INT, SimpleType.INT))) + .build(); + CelAbstractSyntaxTree ast = cel.compile("10.func(4.func(5))").getAst(); + CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + CelMutableExpr newExpr = CelMutableExpr.ofConstant(CelConstant.ofValue(20)); + + CelMutableAst result = AST_MUTATOR.replaceSubtree(mutableAst, newExpr, 1); + + assertThat(CEL_UNPARSER.unparse(result.toParsedAst())).isEqualTo("20.func(4.func(5))"); + } + + @Test + public void memberCallExpr_replaceMiddleBranchArgument() throws Exception { + // Tree shape (brackets are expr IDs): + // func [2] + // 10 [1] func [4] + // 4 [3] 5 [5] + Cel cel = + CelFactory.standardCelBuilder() + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "func", + CelOverloadDecl.newMemberOverload( + "func_overload", SimpleType.INT, SimpleType.INT, SimpleType.INT))) + .build(); + CelAbstractSyntaxTree ast = cel.compile("10.func(4.func(5))").getAst(); + CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + CelMutableExpr newExpr = CelMutableExpr.ofConstant(CelConstant.ofValue(20)); + + CelMutableAst result = AST_MUTATOR.replaceSubtree(mutableAst, newExpr, 4); + + assertThat(CEL_UNPARSER.unparse(result.toParsedAst())).isEqualTo("10.func(20)"); + } + + @Test + public void select_replaceField() throws Exception { + // Tree shape (brackets are expr IDs): + // + [2] + // 5 [1] select [4] + // msg [3] + CelAbstractSyntaxTree ast = CEL.compile("5 + msg.single_int64").getAst(); + CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + CelMutableExpr newExpr = + CelMutableExpr.ofSelect( + CelMutableSelect.create(CelMutableExpr.ofIdent("test"), "single_sint32")); + + CelMutableAst result = AST_MUTATOR.replaceSubtree(mutableAst, newExpr, 4); + + assertThat(CEL_UNPARSER.unparse(result.toParsedAst())).isEqualTo("5 + test.single_sint32"); + } + + @Test + public void select_replaceOperand() throws Exception { + // Tree shape (brackets are expr IDs): + // + [2] + // 5 [1] select [4] + // msg [3] + CelAbstractSyntaxTree ast = CEL.compile("5 + msg.single_int64").getAst(); + CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + CelMutableExpr newExpr = CelMutableExpr.ofIdent("test"); + + CelMutableAst result = AST_MUTATOR.replaceSubtree(mutableAst, newExpr, 3); + + assertThat(CEL_UNPARSER.unparse(result.toParsedAst())).isEqualTo("5 + test.single_int64"); + } + + @Test + public void list_replaceElement() throws Exception { + // Tree shape (brackets are expr IDs): + // list [1] + // 2 [2] 3 [3] 4 [4] + CelAbstractSyntaxTree ast = CEL.compile("[2, 3, 4]").getAst(); + CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + CelMutableExpr newExpr = CelMutableExpr.ofConstant(CelConstant.ofValue(5)); + + CelMutableAst result = AST_MUTATOR.replaceSubtree(mutableAst, newExpr, 4); + + assertThat(CEL_UNPARSER.unparse(result.toParsedAst())).isEqualTo("[2, 3, 5]"); + } + + @Test + public void struct_replaceValue() throws Exception { + // Tree shape (brackets are expr IDs): + // TestAllTypes [1] + // single_int64 [2] + // 2 [3] + CelAbstractSyntaxTree ast = CEL.compile("TestAllTypes{single_int64: 2}").getAst(); + CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + CelMutableExpr newExpr = CelMutableExpr.ofConstant(CelConstant.ofValue(5)); + + CelMutableAst result = AST_MUTATOR.replaceSubtree(mutableAst, newExpr, 3); + + assertThat(CEL_UNPARSER.unparse(result.toParsedAst())) + .isEqualTo("cel.expr.conformance.proto3.TestAllTypes{single_int64: 5}"); + } + + @Test + public void map_replaceKey() throws Exception { + // Tree shape (brackets are expr IDs): + // map [1] + // map_entry [2] + // 'a' [3] : 1 [4] + CelAbstractSyntaxTree ast = CEL.compile("{'a': 1}").getAst(); + CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + CelMutableExpr newExpr = CelMutableExpr.ofConstant(CelConstant.ofValue(5)); + + CelMutableAst result = AST_MUTATOR.replaceSubtree(mutableAst, newExpr, 3); + + assertThat(CEL_UNPARSER.unparse(result.toParsedAst())).isEqualTo("{5: 1}"); + } + + @Test + public void map_replaceValue() throws Exception { + // Tree shape (brackets are expr IDs): + // map [1] + // map_entry [2] + // 'a' [3] : 1 [4] + CelAbstractSyntaxTree ast = CEL.compile("{'a': 1}").getAst(); + CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + CelMutableExpr newExpr = CelMutableExpr.ofConstant(CelConstant.ofValue(5)); + + CelMutableAst result = AST_MUTATOR.replaceSubtree(mutableAst, newExpr, 4); + + assertThat(CEL_UNPARSER.unparse(result.toParsedAst())).isEqualTo("{\"a\": 5}"); + } + + @Test + public void comprehension_replaceIterRange() throws Exception { + CelAbstractSyntaxTree ast = CEL.compile("[true].exists(i, i)").getAst(); + CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + CelMutableExpr newExpr = CelMutableExpr.ofConstant(CelConstant.ofValue(false)); + + CelAbstractSyntaxTree replacedAst = + AST_MUTATOR.replaceSubtree(mutableAst, newExpr, 2).toParsedAst(); + + assertThat(CEL_UNPARSER.unparse(replacedAst)).isEqualTo("[false].exists(i, i)"); + assertThat(CEL.createProgram(CEL.check(replacedAst).getAst()).eval()).isEqualTo(false); + assertConsistentMacroCalls(replacedAst); + } + + @Test + public void comprehension_replaceAccuInit() throws Exception { + CelAbstractSyntaxTree ast = CEL.compile("[false].exists(i, i)").getAst(); + CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + CelMutableExpr newExpr = CelMutableExpr.ofConstant(CelConstant.ofValue(true)); + + CelAbstractSyntaxTree replacedAst = + AST_MUTATOR.replaceSubtree(mutableAst, newExpr, 6).toParsedAst(); + + assertThat(CEL_UNPARSER.unparse(replacedAst)).isEqualTo("[false].exists(i, i)"); + assertThat(CEL.createProgram(CEL.check(replacedAst).getAst()).eval()).isEqualTo(true); + // Check that the init value of accumulator has actually been replaced. + assertThat(ast.getExpr().comprehension().accuInit().constant().booleanValue()).isFalse(); + assertThat(replacedAst.getExpr().comprehension().accuInit().constant().booleanValue()).isTrue(); + assertConsistentMacroCalls(ast); + } + + @Test + public void comprehension_replaceLoopStep() throws Exception { + CelAbstractSyntaxTree ast = CEL.compile("[false].exists(i, i)").getAst(); + CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + CelMutableExpr newExpr = CelMutableExpr.ofIdent("test"); + + CelAbstractSyntaxTree replacedAst = + AST_MUTATOR.replaceSubtree(mutableAst, newExpr, 5).toParsedAst(); + + assertThat(CEL_UNPARSER.unparse(replacedAst)).isEqualTo("[false].exists(i, test)"); + assertConsistentMacroCalls(ast); + } + + @Test + public void mangleComprehensionVariable_singleMacro() throws Exception { + CelAbstractSyntaxTree ast = CEL.compile("[false].exists(i, i)").getAst(); + + CelAbstractSyntaxTree mangledAst = + AST_MUTATOR + .mangleComprehensionIdentifierNames(CelMutableAst.fromCelAst(ast), "@it", "@it2", "@ac") + .mutableAst() + .toParsedAst(); + + assertThat(mangledAst.getExpr().toString()) + .isEqualTo( + "COMPREHENSION [13] {\n" + + " iter_var: @it:0:0\n" + + " iter_range: {\n" + + " LIST [1] {\n" + + " elements: {\n" + + " CONSTANT [2] { value: false }\n" + + " }\n" + + " }\n" + + " }\n" + + " accu_var: @ac:0:0\n" + + " accu_init: {\n" + + " CONSTANT [6] { value: false }\n" + + " }\n" + + " loop_condition: {\n" + + " CALL [9] {\n" + + " function: @not_strictly_false\n" + + " args: {\n" + + " CALL [8] {\n" + + " function: !_\n" + + " args: {\n" + + " IDENT [7] {\n" + + " name: @ac:0:0\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " loop_step: {\n" + + " CALL [11] {\n" + + " function: _||_\n" + + " args: {\n" + + " IDENT [10] {\n" + + " name: @ac:0:0\n" + + " }\n" + + " IDENT [5] {\n" + + " name: @it:0:0\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " result: {\n" + + " IDENT [12] {\n" + + " name: @ac:0:0\n" + + " }\n" + + " }\n" + + "}"); + assertThat(CEL_UNPARSER.unparse(mangledAst)).isEqualTo("[false].exists(@it:0:0, @it:0:0)"); + assertThat(CEL.createProgram(CEL.check(mangledAst).getAst()).eval()).isEqualTo(false); + assertConsistentMacroCalls(ast); + } + + @Test + public void mangleComprehensionVariable_withTwoIterVars_singleMacro() throws Exception { + CelAbstractSyntaxTree ast = CEL.compile("[false].exists(i, v, v)").getAst(); + + CelAbstractSyntaxTree mangledAst = + AST_MUTATOR + .mangleComprehensionIdentifierNames(CelMutableAst.fromCelAst(ast), "@it", "@it2", "@ac") + .mutableAst() + .toParsedAst(); + + assertThat(mangledAst.getExpr().toString()) + .isEqualTo( + "COMPREHENSION [14] {\n" + + " iter_var: @it:0:0\n" + + " iter_var2: @it2:0:0\n" + + " iter_range: {\n" + + " LIST [1] {\n" + + " elements: {\n" + + " CONSTANT [2] { value: false }\n" + + " }\n" + + " }\n" + + " }\n" + + " accu_var: @ac:0:0\n" + + " accu_init: {\n" + + " CONSTANT [7] { value: false }\n" + + " }\n" + + " loop_condition: {\n" + + " CALL [10] {\n" + + " function: @not_strictly_false\n" + + " args: {\n" + + " CALL [9] {\n" + + " function: !_\n" + + " args: {\n" + + " IDENT [8] {\n" + + " name: @ac:0:0\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " loop_step: {\n" + + " CALL [12] {\n" + + " function: _||_\n" + + " args: {\n" + + " IDENT [11] {\n" + + " name: @ac:0:0\n" + + " }\n" + + " IDENT [6] {\n" + + " name: @it2:0:0\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " result: {\n" + + " IDENT [13] {\n" + + " name: @ac:0:0\n" + + " }\n" + + " }\n" + + "}"); + assertThat(CEL_UNPARSER.unparse(mangledAst)) + .isEqualTo("[false].exists(@it:0:0, @it2:0:0, @it2:0:0)"); + assertThat(CEL.createProgram(CEL.check(mangledAst).getAst()).eval()).isEqualTo(false); + assertConsistentMacroCalls(ast); + } + + @Test + public void mangleComprehensionVariable_adjacentMacros_sameIterVarTypes() throws Exception { + CelAbstractSyntaxTree ast = + CEL.compile( + "[1,2,3].map(i, [1, 2, 3].map(i, i + 1)) == [1,2,3].map(j, [1, 2, 3].map(j, j +" + + " 1))") + .getAst(); + + CelAbstractSyntaxTree mangledAst = + AST_MUTATOR + .mangleComprehensionIdentifierNames(CelMutableAst.fromCelAst(ast), "@it", "@it2", "@ac") + .mutableAst() + .toParsedAst(); + + assertThat(CEL_UNPARSER.unparse(mangledAst)) + .isEqualTo( + "[1, 2, 3].map(@it:1:0, [1, 2, 3].map(@it:0:0, @it:0:0 + 1)) == " + + "[1, 2, 3].map(@it:1:0, [1, 2, 3].map(@it:0:0, @it:0:0 + 1))"); + assertThat(CEL.createProgram(CEL.check(mangledAst).getAst()).eval()).isEqualTo(true); + assertConsistentMacroCalls(ast); + } + + @Test + public void mangleComprehensionVariable_withTwoIterVars_adjacentMacros_sameIterVarTypes() + throws Exception { + CelAbstractSyntaxTree ast = + CEL.compile( + // LHS & RHS are same expressions, just different var names + "[1, 2].transformMap(i, v, [1,2].transformMap(i, v, i)) == " + + "[1, 2].transformMap(x, y, [1,2].transformMap(x, y, x))") + .getAst(); + + CelAbstractSyntaxTree mangledAst = + AST_MUTATOR + .mangleComprehensionIdentifierNames(CelMutableAst.fromCelAst(ast), "@it", "@it2", "@ac") + .mutableAst() + .toParsedAst(); + + assertThat(CEL_UNPARSER.unparse(mangledAst)) + .isEqualTo( + "[1, 2].transformMap(@it:1:0, @it2:1:0, [1, 2].transformMap(@it:0:0, @it2:0:0," + + " @it:0:0)) == [1, 2].transformMap(@it:1:0, @it2:1:0, [1," + + " 2].transformMap(@it:0:0, @it2:0:0, @it:0:0))"); + assertThat(CEL.createProgram(CEL.check(mangledAst).getAst()).eval()).isEqualTo(true); + assertConsistentMacroCalls(ast); + } + + @Test + public void mangleComprehensionVariable_adjacentMacros_differentIterVarTypes() throws Exception { + CelAbstractSyntaxTree ast = + CEL.compile( + "[1,2,3].map(i, [1, 2, 3].map(i, i)) == dyn([1u,2u,3u].map(j, [1u, 2u, 3u].map(j," + + " j)))") + .getAst(); + + CelAbstractSyntaxTree mangledAst = + AST_MUTATOR + .mangleComprehensionIdentifierNames(CelMutableAst.fromCelAst(ast), "@it", "@it2", "@ac") + .mutableAst() + .toParsedAst(); + + assertThat(CEL_UNPARSER.unparse(mangledAst)) + .isEqualTo( + "[1, 2, 3].map(@it:1:0, [1, 2, 3].map(@it:0:0, @it:0:0)) == " + + "dyn([1u, 2u, 3u].map(@it:1:1, [1u, 2u, 3u].map(@it:0:1, @it:0:1)))"); + assertThat(CEL.createProgram(CEL.check(mangledAst).getAst()).eval()).isEqualTo(true); + assertConsistentMacroCalls(ast); + } + + @Test + public void mangleComprehensionVariable_macroSourceDisabled_macroCallMapIsEmpty() + throws Exception { + Cel cel = + CelFactory.standardCelBuilder() + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .setOptions(CelOptions.current().populateMacroCalls(false).build()) + .build(); + CelAbstractSyntaxTree ast = cel.compile("[false].exists(i, i)").getAst(); + + CelAbstractSyntaxTree mangledAst = + AST_MUTATOR + .mangleComprehensionIdentifierNames(CelMutableAst.fromCelAst(ast), "@it", "@it2", "@ac") + .mutableAst() + .toParsedAst(); + + assertThat(mangledAst.getSource().getMacroCalls()).isEmpty(); + } + + @Test + public void mangleComprehensionVariable_nestedMacroWithShadowedVariables() throws Exception { + CelAbstractSyntaxTree ast = CEL.compile("[x].exists(x, [x].exists(x, x == 1))").getAst(); + + CelAbstractSyntaxTree mangledAst = + AST_MUTATOR + .mangleComprehensionIdentifierNames(CelMutableAst.fromCelAst(ast), "@it", "@it2", "@ac") + .mutableAst() + .toParsedAst(); + + assertThat(mangledAst.getExpr().toString()) + .isEqualTo( + "COMPREHENSION [27] {\n" + + " iter_var: @it:1:0\n" + + " iter_range: {\n" + + " LIST [1] {\n" + + " elements: {\n" + + " IDENT [2] {\n" + + " name: x\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " accu_var: @ac:1:0\n" + + " accu_init: {\n" + + " CONSTANT [20] { value: false }\n" + + " }\n" + + " loop_condition: {\n" + + " CALL [23] {\n" + + " function: @not_strictly_false\n" + + " args: {\n" + + " CALL [22] {\n" + + " function: !_\n" + + " args: {\n" + + " IDENT [21] {\n" + + " name: @ac:1:0\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " loop_step: {\n" + + " CALL [25] {\n" + + " function: _||_\n" + + " args: {\n" + + " IDENT [24] {\n" + + " name: @ac:1:0\n" + + " }\n" + + " COMPREHENSION [19] {\n" + + " iter_var: @it:0:0\n" + + " iter_range: {\n" + + " LIST [5] {\n" + + " elements: {\n" + + " IDENT [6] {\n" + + " name: @it:1:0\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " accu_var: @ac:0:0\n" + + " accu_init: {\n" + + " CONSTANT [12] { value: false }\n" + + " }\n" + + " loop_condition: {\n" + + " CALL [15] {\n" + + " function: @not_strictly_false\n" + + " args: {\n" + + " CALL [14] {\n" + + " function: !_\n" + + " args: {\n" + + " IDENT [13] {\n" + + " name: @ac:0:0\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " loop_step: {\n" + + " CALL [17] {\n" + + " function: _||_\n" + + " args: {\n" + + " IDENT [16] {\n" + + " name: @ac:0:0\n" + + " }\n" + + " CALL [10] {\n" + + " function: _==_\n" + + " args: {\n" + + " IDENT [9] {\n" + + " name: @it:0:0\n" + + " }\n" + + " CONSTANT [11] { value: 1 }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " result: {\n" + + " IDENT [18] {\n" + + " name: @ac:0:0\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " result: {\n" + + " IDENT [26] {\n" + + " name: @ac:1:0\n" + + " }\n" + + " }\n" + + "}"); + assertThat(CEL_UNPARSER.unparse(mangledAst)) + .isEqualTo("[x].exists(@it:1:0, [@it:1:0].exists(@it:0:0, @it:0:0 == 1))"); + assertThat(CEL.createProgram(CEL.check(mangledAst).getAst()).eval(ImmutableMap.of("x", 1))) + .isEqualTo(true); + assertConsistentMacroCalls(ast); + } + + @Test + public void mangleComprehensionVariable_hasMacro_noOp() throws Exception { + CelAbstractSyntaxTree ast = CEL.compile("has(msg.single_int64)").getAst(); + + CelAbstractSyntaxTree mangledAst = + AST_MUTATOR + .mangleComprehensionIdentifierNames(CelMutableAst.fromCelAst(ast), "@it", "@it2", "@ac") + .mutableAst() + .toParsedAst(); + + assertThat(CEL_UNPARSER.unparse(mangledAst)).isEqualTo("has(msg.single_int64)"); + assertThat( + CEL.createProgram(CEL.check(mangledAst).getAst()) + .eval(ImmutableMap.of("msg", TestAllTypes.getDefaultInstance()))) + .isEqualTo(false); + assertConsistentMacroCalls(ast); + } + + @Test + public void replaceSubtree_iterationLimitReached_throws() throws Exception { + CelAbstractSyntaxTree ast = CEL.compile("true && false").getAst(); + CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + AstMutator astMutator = AstMutator.newInstance(1); + + IllegalStateException e = + assertThrows( + IllegalStateException.class, + () -> + astMutator.replaceSubtree( + mutableAst, CelMutableExpr.ofConstant(CelConstant.ofValue(false)), 1)); + + assertThat(e).hasMessageThat().isEqualTo("Max iteration count reached."); + } + + @Test + public void newGlobalCallAst_success() throws Exception { + CelMutableAst argAst1 = CelMutableAst.fromCelAst(CEL.compile("[1].exists(x, x >= 1)").getAst()); + CelMutableAst argAst2 = CelMutableAst.fromCelAst(CEL.compile("'hello'").getAst()); + + CelMutableAst callAst = AST_MUTATOR.newGlobalCall("func", argAst1, argAst2); + + assertThat(CEL_UNPARSER.unparse(callAst.toParsedAst())) + .isEqualTo("func([1].exists(x, x >= 1), \"hello\")"); + } + + @Test + public void newMemberCallAst_success() throws Exception { + CelMutableAst targetAst = CelMutableAst.fromCelAst(CEL.compile("'hello'").getAst()); + CelMutableAst argAst1 = CelMutableAst.fromCelAst(CEL.compile("[1].exists(x, x >= 1)").getAst()); + CelMutableAst argAst2 = CelMutableAst.fromCelAst(CEL.compile("'world'").getAst()); + + CelMutableAst callAst = AST_MUTATOR.newMemberCall(targetAst, "func", argAst1, argAst2); + + assertThat(CEL_UNPARSER.unparse(callAst.toParsedAst())) + .isEqualTo("\"hello\".func([1].exists(x, x >= 1), \"world\")"); + } + + /** + * Asserts that the expressions that appears in source_info's macro calls are consistent with the + * actual expr nodes in the AST. + */ + private void assertConsistentMacroCalls(CelAbstractSyntaxTree ast) { + assertThat(ast.getSource().getMacroCalls()).isNotEmpty(); + ImmutableMap allExprs = + CelNavigableAst.fromAst(ast) + .getRoot() + .allNodes() + .map(CelNavigableExpr::expr) + .collect(toImmutableMap(CelExpr::id, node -> node, (expr1, expr2) -> expr1)); + for (CelExpr macroCall : ast.getSource().getMacroCalls().values()) { + assertThat(macroCall.id()).isEqualTo(0); + CelNavigableExpr.fromExpr(macroCall) + .descendants() + .map(CelNavigableExpr::expr) + .forEach( + node -> { + CelExpr e = allExprs.get(node.id()); + if (e != null) { + assertThat(node.id()).isEqualTo(e.id()); + if (e.exprKind().getKind().equals(Kind.COMPREHENSION)) { + assertThat(node.exprKind().getKind()).isEqualTo(Kind.NOT_SET); + } else { + assertThat(node.exprKind().getKind()).isEqualTo(e.exprKind().getKind()); + } + } + }); + } + } +} diff --git a/optimizer/src/test/java/dev/cel/optimizer/BUILD.bazel b/optimizer/src/test/java/dev/cel/optimizer/BUILD.bazel index f40903138..8ea72a261 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/BUILD.bazel +++ b/optimizer/src/test/java/dev/cel/optimizer/BUILD.bazel @@ -1,6 +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,26 +12,30 @@ java_library( deps = [ "//:java_truth", "//bundle:cel", - "//common", + "//common:cel_ast", + "//common:cel_source", "//common:compiler_common", + "//common:container", + "//common:mutable_ast", "//common:options", "//common/ast", + "//common/ast:mutable_expr", "//common/navigation", - "//common/resources/testdata/proto3:test_all_types_java_proto", "//common/types", "//compiler", "//extensions", "//extensions:optional_library", "//optimizer", + "//optimizer:ast_optimizer", "//optimizer:mutable_ast", "//optimizer:optimization_exception", "//optimizer:optimizer_builder", "//optimizer:optimizer_impl", - "//parser", "//parser:macro", - "//parser:operator", + "//parser:parser_factory", "//parser:unparser", "//runtime", + "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", "@maven//:com_google_guava_guava", "@maven//:com_google_testparameterinjector_test_parameter_injector", "@maven//:junit_junit", diff --git a/optimizer/src/test/java/dev/cel/optimizer/CelOptimizerImplTest.java b/optimizer/src/test/java/dev/cel/optimizer/CelOptimizerImplTest.java index fdb6cb968..cb0bff6c6 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/CelOptimizerImplTest.java +++ b/optimizer/src/test/java/dev/cel/optimizer/CelOptimizerImplTest.java @@ -23,6 +23,7 @@ import dev.cel.common.CelSource; import dev.cel.common.CelValidationException; import dev.cel.common.ast.CelExpr; +import dev.cel.optimizer.CelAstOptimizer.OptimizationResult; import java.util.ArrayList; import java.util.List; import org.junit.Test; @@ -39,9 +40,9 @@ public void constructCelOptimizer_success() { CelOptimizer celOptimizer = CelOptimizerImpl.newBuilder(CEL) .addAstOptimizers( - (navigableAst, cel) -> + (ast, cel) -> // no-op - navigableAst.getAst()) + OptimizationResult.create(ast)) .build(); assertThat(celOptimizer).isNotNull(); @@ -55,19 +56,19 @@ public void astOptimizers_invokedInOrder() throws Exception { CelOptimizer celOptimizer = CelOptimizerImpl.newBuilder(CEL) .addAstOptimizers( - (navigableAst, cel) -> { + (ast, cel) -> { list.add(1); - return navigableAst.getAst(); + return OptimizationResult.create(ast); }) .addAstOptimizers( - (navigableAst, cel) -> { + (ast, cel) -> { list.add(2); - return navigableAst.getAst(); + return OptimizationResult.create(ast); }) .addAstOptimizers( - (navigableAst, cel) -> { + (ast, cel) -> { list.add(3); - return navigableAst.getAst(); + return OptimizationResult.create(ast); }) .build(); @@ -112,8 +113,10 @@ public void optimizedAst_failsToTypeCheck_throwsException() { CelOptimizerImpl.newBuilder(CEL) .addAstOptimizers( (navigableAst, cel) -> - CelAbstractSyntaxTree.newParsedAst( - CelExpr.ofIdentExpr(1, "undeclared_ident"), CelSource.newBuilder().build())) + OptimizationResult.create( + CelAbstractSyntaxTree.newParsedAst( + CelExpr.ofIdent(1, "undeclared_ident"), + CelSource.newBuilder().build()))) .build(); CelOptimizationException e = diff --git a/optimizer/src/test/java/dev/cel/optimizer/MutableAstTest.java b/optimizer/src/test/java/dev/cel/optimizer/MutableAstTest.java deleted file mode 100644 index da8ecfb7b..000000000 --- a/optimizer/src/test/java/dev/cel/optimizer/MutableAstTest.java +++ /dev/null @@ -1,919 +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.optimizer; - -import static com.google.common.collect.ImmutableMap.toImmutableMap; -import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.assertThrows; - -import com.google.common.collect.ImmutableMap; -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.CelFunctionDecl; -import dev.cel.common.CelOptions; -import dev.cel.common.CelOverloadDecl; -import dev.cel.common.CelSource; -import dev.cel.common.CelSource.Extension; -import dev.cel.common.CelSource.Extension.Version; -import dev.cel.common.ast.CelConstant; -import dev.cel.common.ast.CelExpr; -import dev.cel.common.ast.CelExpr.CelCall; -import dev.cel.common.ast.CelExpr.CelIdent; -import dev.cel.common.ast.CelExpr.CelSelect; -import dev.cel.common.ast.CelExpr.ExprKind.Kind; -import dev.cel.common.navigation.CelNavigableAst; -import dev.cel.common.navigation.CelNavigableExpr; -import dev.cel.common.types.SimpleType; -import dev.cel.common.types.StructTypeReference; -import dev.cel.extensions.CelExtensions; -import dev.cel.extensions.CelOptionalLibrary; -import dev.cel.parser.CelStandardMacro; -import dev.cel.parser.CelUnparser; -import dev.cel.parser.CelUnparserFactory; -import dev.cel.parser.Operator; -import dev.cel.testing.testdata.proto3.TestAllTypesProto.TestAllTypes; -import org.junit.Test; -import org.junit.runner.RunWith; - -@RunWith(TestParameterInjector.class) -public class MutableAstTest { - private static final Cel CEL = - CelFactory.standardCelBuilder() - .setStandardMacros(CelStandardMacro.STANDARD_MACROS) - .setOptions(CelOptions.current().populateMacroCalls(true).build()) - .addMessageTypes(TestAllTypes.getDescriptor()) - .addCompilerLibraries(CelOptionalLibrary.INSTANCE, CelExtensions.bindings()) - .addRuntimeLibraries(CelOptionalLibrary.INSTANCE) - .setContainer("dev.cel.testing.testdata.proto3") - .addVar("msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())) - .addVar("x", SimpleType.INT) - .build(); - - private static final CelUnparser CEL_UNPARSER = CelUnparserFactory.newUnparser(); - private static final MutableAst MUTABLE_AST = MutableAst.newInstance(1000); - - @Test - public void constExpr() throws Exception { - CelAbstractSyntaxTree ast = CEL.compile("10").getAst(); - - CelAbstractSyntaxTree mutatedAst = - MUTABLE_AST.replaceSubtree( - ast, CelExpr.newBuilder().setConstant(CelConstant.ofValue(true)).build(), 1); - - assertThat(mutatedAst.getExpr()) - .isEqualTo(CelExpr.ofConstantExpr(3, CelConstant.ofValue(true))); - } - - @Test - public void mutableAst_returnsParsedAst() throws Exception { - CelAbstractSyntaxTree ast = CEL.compile("10").getAst(); - - CelAbstractSyntaxTree mutatedAst = - MUTABLE_AST.replaceSubtree( - ast, CelExpr.newBuilder().setConstant(CelConstant.ofValue(true)).build(), 1); - - assertThat(ast.isChecked()).isTrue(); - assertThat(mutatedAst.isChecked()).isFalse(); - } - - @Test - public void mutableAst_nonMacro_sourceCleared() throws Exception { - CelAbstractSyntaxTree ast = CEL.compile("10").getAst(); - - CelAbstractSyntaxTree mutatedAst = - MUTABLE_AST.replaceSubtree( - ast, CelExpr.newBuilder().setConstant(CelConstant.ofValue(true)).build(), 1); - - assertThat(mutatedAst.getSource().getDescription()).isEmpty(); - assertThat(mutatedAst.getSource().getLineOffsets()).isEmpty(); - assertThat(mutatedAst.getSource().getPositionsMap()).isEmpty(); - assertThat(mutatedAst.getSource().getExtensions()).isEmpty(); - assertThat(mutatedAst.getSource().getMacroCalls()).isEmpty(); - } - - @Test - public void mutableAst_macro_sourceMacroCallsPopulated() throws Exception { - CelAbstractSyntaxTree ast = CEL.compile("has(TestAllTypes{}.single_int32)").getAst(); - - CelAbstractSyntaxTree mutatedAst = - MUTABLE_AST.replaceSubtree( - ast, CelExpr.newBuilder().setConstant(CelConstant.ofValue(true)).build(), 1); - - assertThat(mutatedAst.getSource().getDescription()).isEmpty(); - assertThat(mutatedAst.getSource().getLineOffsets()).isEmpty(); - assertThat(mutatedAst.getSource().getPositionsMap()).isEmpty(); - assertThat(mutatedAst.getSource().getExtensions()).isEmpty(); - assertThat(mutatedAst.getSource().getMacroCalls()).isNotEmpty(); - } - - @Test - public void mutableAst_astContainsTaggedExtension_retained() throws Exception { - CelAbstractSyntaxTree ast = CEL.compile("has(TestAllTypes{}.single_int32)").getAst(); - Extension extension = Extension.create("test", Version.of(1, 1)); - CelSource celSource = ast.getSource().toBuilder().addAllExtensions(extension).build(); - ast = - CelAbstractSyntaxTree.newCheckedAst( - ast.getExpr(), celSource, ast.getReferenceMap(), ast.getTypeMap()); - - CelAbstractSyntaxTree mutatedAst = - MUTABLE_AST.replaceSubtree( - ast, CelExpr.newBuilder().setConstant(CelConstant.ofValue(true)).build(), 1); - - assertThat(mutatedAst.getSource().getExtensions()).containsExactly(extension); - } - - @Test - @TestParameters("{source: '[1].exists(x, x > 0)', expectedMacroCallSize: 1}") - @TestParameters( - "{source: '[1].exists(x, x > 0) && [2].exists(x, x > 0)', expectedMacroCallSize: 2}") - @TestParameters( - "{source: '[1].exists(x, [2].exists(y, x > 0 && y > x))', expectedMacroCallSize: 2}") - public void replaceSubtree_rootReplacedWithMacro_macroCallPopulated( - String source, int expectedMacroCallSize) throws Exception { - CelAbstractSyntaxTree ast = CEL.compile("1").getAst(); - CelAbstractSyntaxTree ast2 = CEL.compile(source).getAst(); - - CelAbstractSyntaxTree mutatedAst = - MUTABLE_AST.replaceSubtreeWithNewAst( - ast, ast2, CelNavigableAst.fromAst(ast).getRoot().id()); - - assertThat(mutatedAst.getSource().getMacroCalls()).hasSize(expectedMacroCallSize); - assertThat(CEL_UNPARSER.unparse(mutatedAst)).isEqualTo(source); - assertThat(CEL.createProgram(CEL.check(mutatedAst).getAst()).eval()).isEqualTo(true); - } - - @Test - public void replaceSubtree_branchReplacedWithMacro_macroCallPopulated() throws Exception { - CelAbstractSyntaxTree ast = CEL.compile("true && false").getAst(); - CelAbstractSyntaxTree ast2 = CEL.compile("[1].exists(x, x > 0)").getAst(); - - CelAbstractSyntaxTree mutatedAst = - MUTABLE_AST.replaceSubtreeWithNewAst(ast, ast2, 3); // Replace false with the macro expr - CelAbstractSyntaxTree mutatedAst2 = - MUTABLE_AST.replaceSubtreeWithNewAst(ast, ast2, 1); // Replace true with the macro expr - - assertThat(mutatedAst.getSource().getMacroCalls()).hasSize(1); - assertThat(CEL_UNPARSER.unparse(mutatedAst)).isEqualTo("true && [1].exists(x, x > 0)"); - assertThat(CEL.createProgram(CEL.check(mutatedAst).getAst()).eval()).isEqualTo(true); - assertThat(mutatedAst2.getSource().getMacroCalls()).hasSize(1); - assertThat(CEL_UNPARSER.unparse(mutatedAst2)).isEqualTo("[1].exists(x, x > 0) && false"); - assertThat(CEL.createProgram(CEL.check(mutatedAst2).getAst()).eval()).isEqualTo(false); - } - - @Test - public void replaceSubtree_macroInsertedIntoExistingMacro_macroCallPopulated() throws Exception { - CelAbstractSyntaxTree ast = CEL.compile("[1].exists(x, x > 0 && true)").getAst(); - CelAbstractSyntaxTree ast2 = CEL.compile("[2].exists(y, y > 0)").getAst(); - - CelAbstractSyntaxTree mutatedAst = - MUTABLE_AST.replaceSubtreeWithNewAst(ast, ast2, 9); // Replace true with the ast2 maro expr - - assertThat(mutatedAst.getSource().getMacroCalls()).hasSize(2); - assertThat(CEL_UNPARSER.unparse(mutatedAst)) - .isEqualTo("[1].exists(x, x > 0 && [2].exists(y, y > 0))"); - assertThat(CEL.createProgram(CEL.check(mutatedAst).getAst()).eval()).isEqualTo(true); - } - - @Test - public void replaceSubtreeWithNewBindMacro_replaceRoot() throws Exception { - CelAbstractSyntaxTree ast = CEL.compile("1 + 1").getAst(); - String variableName = "@r0"; - CelExpr resultExpr = - CelExpr.newBuilder() - .setCall( - CelCall.newBuilder() - .setFunction(Operator.ADD.getFunction()) - .addArgs( - CelExpr.ofIdentExpr(0, variableName), CelExpr.ofIdentExpr(0, variableName)) - .build()) - .build(); - - CelAbstractSyntaxTree mutatedAst = - MUTABLE_AST.replaceSubtreeWithNewBindMacro( - ast, - variableName, - CelExpr.ofConstantExpr(0, CelConstant.ofValue(3L)), - resultExpr, - CelNavigableAst.fromAst(ast).getRoot().id()); - - assertThat(mutatedAst.getSource().getMacroCalls()).hasSize(1); - assertThat(CEL_UNPARSER.unparse(mutatedAst)).isEqualTo("cel.bind(@r0, 3, @r0 + @r0)"); - assertThat(CEL.createProgram(CEL.check(mutatedAst).getAst()).eval()).isEqualTo(6); - assertConsistentMacroCalls(mutatedAst); - } - - @Test - public void replaceSubtreeWithNewBindMacro_nestedBindMacro_replaceComprehensionResult() - throws Exception { - // Arrange - CelAbstractSyntaxTree ast = CEL.compile("1 + 1").getAst(); - String variableName = "@r0"; - CelExpr resultExpr = - CelExpr.newBuilder() - .setCall( - CelCall.newBuilder() - .setFunction(Operator.ADD.getFunction()) - .addArgs( - CelExpr.ofIdentExpr(0, variableName), CelExpr.ofIdentExpr(0, variableName)) - .build()) - .build(); - - // Act - // Perform the initial replacement. (1 + 1) -> cel.bind(@r0, 3, @r0 + @r0) - CelAbstractSyntaxTree mutatedAst = - MUTABLE_AST.replaceSubtreeWithNewBindMacro( - ast, - variableName, - CelExpr.ofConstantExpr(0, CelConstant.ofValue(3L)), - resultExpr, - 2); // Replace + - String nestedVariableName = "@r1"; - // Construct a new result expression of the form @r0 + @r0 + @r1 + @r1 - resultExpr = - CelExpr.newBuilder() - .setCall( - CelCall.newBuilder() - .setFunction(Operator.ADD.getFunction()) - .addArgs( - CelExpr.newBuilder() - .setCall( - CelCall.newBuilder() - .setFunction(Operator.ADD.getFunction()) - .addArgs( - CelExpr.newBuilder() - .setCall( - CelCall.newBuilder() - .setFunction(Operator.ADD.getFunction()) - .addArgs( - CelExpr.ofIdentExpr(0, variableName), - CelExpr.ofIdentExpr(0, variableName)) - .build()) - .build(), - CelExpr.ofIdentExpr(0, nestedVariableName)) - .build()) - .build(), - CelExpr.ofIdentExpr(0, nestedVariableName)) - .build()) - .build(); - // Find the call node (_+_) in the comprehension's result - long exprIdToReplace = - CelNavigableAst.fromAst(mutatedAst) - .getRoot() - .children() - .filter( - node -> - node.getKind().equals(Kind.CALL) - && node.parent().get().getKind().equals(Kind.COMPREHENSION)) - .findAny() - .get() - .expr() - .id(); - // This should produce cel.bind(@r1, 1, cel.bind(@r0, 3, @r0 + @r0 + @r1 + @r1)) - mutatedAst = - MUTABLE_AST.replaceSubtreeWithNewBindMacro( - mutatedAst, - nestedVariableName, - CelExpr.ofConstantExpr(0, CelConstant.ofValue(1L)), - resultExpr, - exprIdToReplace); // Replace + - - assertThat(mutatedAst.getSource().getMacroCalls()).hasSize(2); - assertThat(CEL.createProgram(CEL.check(mutatedAst).getAst()).eval()).isEqualTo(8); - assertThat(CEL_UNPARSER.unparse(mutatedAst)) - .isEqualTo("cel.bind(@r0, 3, cel.bind(@r1, 1, @r0 + @r0 + @r1 + @r1))"); - assertConsistentMacroCalls(mutatedAst); - } - - @Test - public void replaceSubtreeWithNewBindMacro_replaceRootWithNestedBindMacro() throws Exception { - // Arrange - CelAbstractSyntaxTree ast = CEL.compile("1 + 1 + 3 + 3").getAst(); - String variableName = "@r0"; - CelExpr resultExpr = - CelExpr.newBuilder() - .setCall( - CelCall.newBuilder() - .setFunction(Operator.ADD.getFunction()) - .addArgs( - CelExpr.ofIdentExpr(0, variableName), CelExpr.ofIdentExpr(0, variableName)) - .build()) - .build(); - - // Act - // Perform the initial replacement. (1 + 1 + 3 + 3) -> cel.bind(@r0, 1, @r0 + @r0) + 3 + 3 - CelAbstractSyntaxTree mutatedAst = - MUTABLE_AST.replaceSubtreeWithNewBindMacro( - ast, - variableName, - CelExpr.ofConstantExpr(0, CelConstant.ofValue(1L)), - resultExpr, - 2); // Replace + - // Construct a new result expression of the form: - // cel.bind(@r1, 3, cel.bind(@r0, 1, @r0 + @r0) + @r1 + @r1) - String nestedVariableName = "@r1"; - CelExpr bindMacro = - CelNavigableAst.fromAst(mutatedAst) - .getRoot() - .descendants() - .filter(node -> node.getKind().equals(Kind.COMPREHENSION)) - .findAny() - .get() - .expr(); - resultExpr = - CelExpr.newBuilder() - .setCall( - CelCall.newBuilder() - .setFunction(Operator.ADD.getFunction()) - .addArgs( - CelExpr.newBuilder() - .setCall( - CelCall.newBuilder() - .setFunction(Operator.ADD.getFunction()) - .addArgs(bindMacro, CelExpr.ofIdentExpr(0, nestedVariableName)) - .build()) - .build(), - CelExpr.ofIdentExpr(0, nestedVariableName)) - .build()) - .build(); - // Replace the root with the new result and a bind macro inserted - mutatedAst = - MUTABLE_AST.replaceSubtreeWithNewBindMacro( - mutatedAst, - nestedVariableName, - CelExpr.ofConstantExpr(0, CelConstant.ofValue(3L)), - resultExpr, - CelNavigableAst.fromAst(mutatedAst).getRoot().id()); - - assertThat(mutatedAst.getSource().getMacroCalls()).hasSize(2); - assertThat(CEL.createProgram(CEL.check(mutatedAst).getAst()).eval()).isEqualTo(8); - assertThat(CEL_UNPARSER.unparse(mutatedAst)) - .isEqualTo("cel.bind(@r1, 3, cel.bind(@r0, 1, @r0 + @r0) + @r1 + @r1)"); - assertConsistentMacroCalls(mutatedAst); - } - - @Test - public void replaceSubtree_macroReplacedWithConstExpr_macroCallCleared() throws Exception { - CelAbstractSyntaxTree ast = - CEL.compile("[1].exists(x, x > 0) && [2].exists(x, x > 0)").getAst(); - CelAbstractSyntaxTree ast2 = CEL.compile("1").getAst(); - - CelAbstractSyntaxTree mutatedAst = - MUTABLE_AST.replaceSubtreeWithNewAst( - ast, ast2, CelNavigableAst.fromAst(ast).getRoot().id()); - - assertThat(mutatedAst.getSource().getMacroCalls()).isEmpty(); - assertThat(CEL_UNPARSER.unparse(mutatedAst)).isEqualTo("1"); - assertThat(CEL.createProgram(CEL.check(mutatedAst).getAst()).eval()).isEqualTo(1); - } - - @Test - public void globalCallExpr_replaceRoot() throws Exception { - // Tree shape (brackets are expr IDs): - // + [4] - // + [2] x [5] - // 1 [1] 2 [3] - CelAbstractSyntaxTree ast = CEL.compile("1 + 2 + x").getAst(); - - CelAbstractSyntaxTree replacedAst = - MUTABLE_AST.replaceSubtree( - ast, CelExpr.newBuilder().setConstant(CelConstant.ofValue(10)).build(), 4); - - assertThat(replacedAst.getExpr()).isEqualTo(CelExpr.ofConstantExpr(7, CelConstant.ofValue(10))); - } - - @Test - public void globalCallExpr_replaceLeaf() throws Exception { - // Tree shape (brackets are expr IDs): - // + [4] - // + [2] x [5] - // 1 [1] 2 [3] - CelAbstractSyntaxTree ast = CEL.compile("1 + 2 + x").getAst(); - - CelAbstractSyntaxTree replacedAst = - MUTABLE_AST.replaceSubtree( - ast, CelExpr.newBuilder().setConstant(CelConstant.ofValue(10)).build(), 1); - - assertThat(CEL_UNPARSER.unparse(replacedAst)).isEqualTo("10 + 2 + x"); - } - - @Test - public void globalCallExpr_replaceMiddleBranch() throws Exception { - // Tree shape (brackets are expr IDs): - // + [4] - // + [2] x [5] - // 1 [1] 2 [3] - CelAbstractSyntaxTree ast = CEL.compile("1 + 2 + x").getAst(); - - CelAbstractSyntaxTree replacedAst = - MUTABLE_AST.replaceSubtree( - ast, CelExpr.newBuilder().setConstant(CelConstant.ofValue(10)).build(), 2); - - assertThat(CEL_UNPARSER.unparse(replacedAst)).isEqualTo("10 + x"); - } - - @Test - public void globalCallExpr_replaceMiddleBranch_withCallExpr() throws Exception { - // Tree shape (brackets are expr IDs): - // + [4] - // + [2] x [5] - // 1 [1] 2 [3] - CelAbstractSyntaxTree ast = CEL.compile("1 + 2 + x").getAst(); - CelAbstractSyntaxTree ast2 = CEL.compile("4 + 5 + 6").getAst(); - - CelAbstractSyntaxTree replacedAst = MUTABLE_AST.replaceSubtree(ast, ast2.getExpr(), 2); - - assertThat(CEL_UNPARSER.unparse(replacedAst)).isEqualTo("4 + 5 + 6 + x"); - } - - @Test - public void memberCallExpr_replaceLeafTarget() throws Exception { - // Tree shape (brackets are expr IDs): - // func [2] - // 10 [1] func [4] - // 4 [3] 5 [5] - Cel cel = - CelFactory.standardCelBuilder() - .addFunctionDeclarations( - CelFunctionDecl.newFunctionDeclaration( - "func", - CelOverloadDecl.newMemberOverload( - "func_overload", SimpleType.INT, SimpleType.INT, SimpleType.INT))) - .build(); - CelAbstractSyntaxTree ast = cel.compile("10.func(4.func(5))").getAst(); - - CelAbstractSyntaxTree replacedAst = - MUTABLE_AST.replaceSubtree( - ast, CelExpr.newBuilder().setConstant(CelConstant.ofValue(20)).build(), 3); - - assertThat(CEL_UNPARSER.unparse(replacedAst)).isEqualTo("10.func(20.func(5))"); - } - - @Test - public void memberCallExpr_replaceLeafArgument() throws Exception { - // Tree shape (brackets are expr IDs): - // func [2] - // 10 [1] func [4] - // 4 [3] 5 [5] - Cel cel = - CelFactory.standardCelBuilder() - .addFunctionDeclarations( - CelFunctionDecl.newFunctionDeclaration( - "func", - CelOverloadDecl.newMemberOverload( - "func_overload", SimpleType.INT, SimpleType.INT, SimpleType.INT))) - .build(); - CelAbstractSyntaxTree ast = cel.compile("10.func(4.func(5))").getAst(); - - CelAbstractSyntaxTree replacedAst = - MUTABLE_AST.replaceSubtree( - ast, CelExpr.newBuilder().setConstant(CelConstant.ofValue(20)).build(), 5); - - assertThat(CEL_UNPARSER.unparse(replacedAst)).isEqualTo("10.func(4.func(20))"); - } - - @Test - public void memberCallExpr_replaceMiddleBranchTarget() throws Exception { - // Tree shape (brackets are expr IDs): - // func [2] - // 10 [1] func [4] - // 4 [3] 5 [5] - Cel cel = - CelFactory.standardCelBuilder() - .addFunctionDeclarations( - CelFunctionDecl.newFunctionDeclaration( - "func", - CelOverloadDecl.newMemberOverload( - "func_overload", SimpleType.INT, SimpleType.INT, SimpleType.INT))) - .build(); - CelAbstractSyntaxTree ast = cel.compile("10.func(4.func(5))").getAst(); - - CelAbstractSyntaxTree replacedAst = - MUTABLE_AST.replaceSubtree( - ast, CelExpr.newBuilder().setConstant(CelConstant.ofValue(20)).build(), 1); - - assertThat(CEL_UNPARSER.unparse(replacedAst)).isEqualTo("20.func(4.func(5))"); - } - - @Test - public void memberCallExpr_replaceMiddleBranchArgument() throws Exception { - // Tree shape (brackets are expr IDs): - // func [2] - // 10 [1] func [4] - // 4 [3] 5 [5] - Cel cel = - CelFactory.standardCelBuilder() - .addFunctionDeclarations( - CelFunctionDecl.newFunctionDeclaration( - "func", - CelOverloadDecl.newMemberOverload( - "func_overload", SimpleType.INT, SimpleType.INT, SimpleType.INT))) - .build(); - CelAbstractSyntaxTree ast = cel.compile("10.func(4.func(5))").getAst(); - - CelAbstractSyntaxTree replacedAst = - MUTABLE_AST.replaceSubtree( - ast, CelExpr.newBuilder().setConstant(CelConstant.ofValue(20)).build(), 4); - - assertThat(CEL_UNPARSER.unparse(replacedAst)).isEqualTo("10.func(20)"); - } - - @Test - public void select_replaceField() throws Exception { - // Tree shape (brackets are expr IDs): - // + [2] - // 5 [1] select [4] - // msg [3] - CelAbstractSyntaxTree ast = CEL.compile("5 + msg.single_int64").getAst(); - - CelAbstractSyntaxTree replacedAst = - MUTABLE_AST.replaceSubtree( - ast, - CelExpr.newBuilder() - .setSelect( - CelSelect.newBuilder() - .setField("single_sint32") - .setOperand( - CelExpr.newBuilder() - .setIdent(CelIdent.newBuilder().setName("test").build()) - .build()) - .build()) - .build(), - 4); - - assertThat(CEL_UNPARSER.unparse(replacedAst)).isEqualTo("5 + test.single_sint32"); - } - - @Test - public void select_replaceOperand() throws Exception { - // Tree shape (brackets are expr IDs): - // + [2] - // 5 [1] select [4] - // msg [3] - CelAbstractSyntaxTree ast = CEL.compile("5 + msg.single_int64").getAst(); - - CelAbstractSyntaxTree replacedAst = - MUTABLE_AST.replaceSubtree( - ast, - CelExpr.newBuilder().setIdent(CelIdent.newBuilder().setName("test").build()).build(), - 3); - - assertThat(CEL_UNPARSER.unparse(replacedAst)).isEqualTo("5 + test.single_int64"); - } - - @Test - public void list_replaceElement() throws Exception { - // Tree shape (brackets are expr IDs): - // list [1] - // 2 [2] 3 [3] 4 [4] - CelAbstractSyntaxTree ast = CEL.compile("[2, 3, 4]").getAst(); - - CelAbstractSyntaxTree replacedAst = - MUTABLE_AST.replaceSubtree( - ast, CelExpr.newBuilder().setConstant(CelConstant.ofValue(5)).build(), 4); - - assertThat(CEL_UNPARSER.unparse(replacedAst)).isEqualTo("[2, 3, 5]"); - } - - @Test - public void createStruct_replaceValue() throws Exception { - // Tree shape (brackets are expr IDs): - // TestAllTypes [1] - // single_int64 [2] - // 2 [3] - CelAbstractSyntaxTree ast = CEL.compile("TestAllTypes{single_int64: 2}").getAst(); - - CelAbstractSyntaxTree replacedAst = - MUTABLE_AST.replaceSubtree( - ast, CelExpr.newBuilder().setConstant(CelConstant.ofValue(5)).build(), 3); - - assertThat(CEL_UNPARSER.unparse(replacedAst)).isEqualTo("TestAllTypes{single_int64: 5}"); - } - - @Test - public void createMap_replaceKey() throws Exception { - // Tree shape (brackets are expr IDs): - // map [1] - // map_entry [2] - // 'a' [3] : 1 [4] - CelAbstractSyntaxTree ast = CEL.compile("{'a': 1}").getAst(); - - CelAbstractSyntaxTree replacedAst = - MUTABLE_AST.replaceSubtree( - ast, CelExpr.newBuilder().setConstant(CelConstant.ofValue(5)).build(), 3); - - assertThat(CEL_UNPARSER.unparse(replacedAst)).isEqualTo("{5: 1}"); - } - - @Test - public void createMap_replaceValue() throws Exception { - // Tree shape (brackets are expr IDs): - // map [1] - // map_entry [2] - // 'a' [3] : 1 [4] - CelAbstractSyntaxTree ast = CEL.compile("{'a': 1}").getAst(); - - CelAbstractSyntaxTree replacedAst = - MUTABLE_AST.replaceSubtree( - ast, CelExpr.newBuilder().setConstant(CelConstant.ofValue(5)).build(), 4); - - assertThat(CEL_UNPARSER.unparse(replacedAst)).isEqualTo("{\"a\": 5}"); - } - - @Test - public void comprehension_replaceIterRange() throws Exception { - CelAbstractSyntaxTree ast = CEL.compile("[true].exists(i, i)").getAst(); - - CelAbstractSyntaxTree replacedAst = - MUTABLE_AST.replaceSubtree( - ast, CelExpr.newBuilder().setConstant(CelConstant.ofValue(false)).build(), 2); - - assertThat(CEL_UNPARSER.unparse(replacedAst)).isEqualTo("[false].exists(i, i)"); - assertThat(CEL.createProgram(CEL.check(replacedAst).getAst()).eval()).isEqualTo(false); - assertConsistentMacroCalls(ast); - } - - @Test - public void comprehension_replaceAccuInit() throws Exception { - CelAbstractSyntaxTree ast = CEL.compile("[false].exists(i, i)").getAst(); - - CelAbstractSyntaxTree replacedAst = - MUTABLE_AST.replaceSubtree( - ast, CelExpr.newBuilder().setConstant(CelConstant.ofValue(true)).build(), 6); - - assertThat(CEL_UNPARSER.unparse(replacedAst)).isEqualTo("[false].exists(i, i)"); - assertThat(CEL.createProgram(CEL.check(replacedAst).getAst()).eval()).isEqualTo(true); - // Check that the init value of accumulator has actually been replaced. - assertThat(ast.getExpr().comprehension().accuInit().constant().booleanValue()).isFalse(); - assertThat(replacedAst.getExpr().comprehension().accuInit().constant().booleanValue()).isTrue(); - assertConsistentMacroCalls(ast); - } - - @Test - public void comprehension_replaceLoopStep() throws Exception { - CelAbstractSyntaxTree ast = CEL.compile("[false].exists(i, i)").getAst(); - - CelAbstractSyntaxTree replacedAst = - MUTABLE_AST.replaceSubtree( - ast, - CelExpr.newBuilder().setIdent(CelIdent.newBuilder().setName("test").build()).build(), - 5); - - assertThat(CEL_UNPARSER.unparse(replacedAst)).isEqualTo("[false].exists(i, test)"); - assertConsistentMacroCalls(ast); - } - - @Test - public void mangleComprehensionVariable_singleMacro() throws Exception { - CelAbstractSyntaxTree ast = CEL.compile("[false].exists(i, i)").getAst(); - - CelAbstractSyntaxTree mangledAst = - MUTABLE_AST.mangleComprehensionIdentifierNames(ast, "@c", "@x").ast(); - - assertThat(mangledAst.getExpr().toString()) - .isEqualTo( - "COMPREHENSION [13] {\n" - + " iter_var: @c0:0\n" - + " iter_range: {\n" - + " CREATE_LIST [1] {\n" - + " elements: {\n" - + " CONSTANT [2] { value: false }\n" - + " }\n" - + " }\n" - + " }\n" - + " accu_var: @x0:0\n" - + " accu_init: {\n" - + " CONSTANT [6] { value: false }\n" - + " }\n" - + " loop_condition: {\n" - + " CALL [9] {\n" - + " function: @not_strictly_false\n" - + " args: {\n" - + " CALL [8] {\n" - + " function: !_\n" - + " args: {\n" - + " IDENT [7] {\n" - + " name: @x0:0\n" - + " }\n" - + " }\n" - + " }\n" - + " }\n" - + " }\n" - + " }\n" - + " loop_step: {\n" - + " CALL [11] {\n" - + " function: _||_\n" - + " args: {\n" - + " IDENT [10] {\n" - + " name: @x0:0\n" - + " }\n" - + " IDENT [5] {\n" - + " name: @c0:0\n" - + " }\n" - + " }\n" - + " }\n" - + " }\n" - + " result: {\n" - + " IDENT [12] {\n" - + " name: @x0:0\n" - + " }\n" - + " }\n" - + "}"); - assertThat(CEL_UNPARSER.unparse(mangledAst)).isEqualTo("[false].exists(@c0:0, @c0:0)"); - assertThat(CEL.createProgram(CEL.check(mangledAst).getAst()).eval()).isEqualTo(false); - assertConsistentMacroCalls(ast); - } - - @Test - public void mangleComprehensionVariable_nestedMacroWithShadowedVariables() throws Exception { - CelAbstractSyntaxTree ast = CEL.compile("[x].exists(x, [x].exists(x, x == 1))").getAst(); - - CelAbstractSyntaxTree mangledAst = - MUTABLE_AST.mangleComprehensionIdentifierNames(ast, "@c", "@x").ast(); - - assertThat(mangledAst.getExpr().toString()) - .isEqualTo( - "COMPREHENSION [27] {\n" - + " iter_var: @c0:0\n" - + " iter_range: {\n" - + " CREATE_LIST [1] {\n" - + " elements: {\n" - + " IDENT [2] {\n" - + " name: x\n" - + " }\n" - + " }\n" - + " }\n" - + " }\n" - + " accu_var: @x0:0\n" - + " accu_init: {\n" - + " CONSTANT [20] { value: false }\n" - + " }\n" - + " loop_condition: {\n" - + " CALL [23] {\n" - + " function: @not_strictly_false\n" - + " args: {\n" - + " CALL [22] {\n" - + " function: !_\n" - + " args: {\n" - + " IDENT [21] {\n" - + " name: @x0:0\n" - + " }\n" - + " }\n" - + " }\n" - + " }\n" - + " }\n" - + " }\n" - + " loop_step: {\n" - + " CALL [25] {\n" - + " function: _||_\n" - + " args: {\n" - + " IDENT [24] {\n" - + " name: @x0:0\n" - + " }\n" - + " COMPREHENSION [19] {\n" - + " iter_var: @c1:0\n" - + " iter_range: {\n" - + " CREATE_LIST [5] {\n" - + " elements: {\n" - + " IDENT [6] {\n" - + " name: @c0:0\n" - + " }\n" - + " }\n" - + " }\n" - + " }\n" - + " accu_var: @x1:0\n" - + " accu_init: {\n" - + " CONSTANT [12] { value: false }\n" - + " }\n" - + " loop_condition: {\n" - + " CALL [15] {\n" - + " function: @not_strictly_false\n" - + " args: {\n" - + " CALL [14] {\n" - + " function: !_\n" - + " args: {\n" - + " IDENT [13] {\n" - + " name: @x1:0\n" - + " }\n" - + " }\n" - + " }\n" - + " }\n" - + " }\n" - + " }\n" - + " loop_step: {\n" - + " CALL [17] {\n" - + " function: _||_\n" - + " args: {\n" - + " IDENT [16] {\n" - + " name: @x1:0\n" - + " }\n" - + " CALL [10] {\n" - + " function: _==_\n" - + " args: {\n" - + " IDENT [9] {\n" - + " name: @c1:0\n" - + " }\n" - + " CONSTANT [11] { value: 1 }\n" - + " }\n" - + " }\n" - + " }\n" - + " }\n" - + " }\n" - + " result: {\n" - + " IDENT [18] {\n" - + " name: @x1:0\n" - + " }\n" - + " }\n" - + " }\n" - + " }\n" - + " }\n" - + " }\n" - + " result: {\n" - + " IDENT [26] {\n" - + " name: @x0:0\n" - + " }\n" - + " }\n" - + "}"); - assertThat(CEL_UNPARSER.unparse(mangledAst)) - .isEqualTo("[x].exists(@c0:0, [@c0:0].exists(@c1:0, @c1:0 == 1))"); - assertThat(CEL.createProgram(CEL.check(mangledAst).getAst()).eval(ImmutableMap.of("x", 1))) - .isEqualTo(true); - assertConsistentMacroCalls(ast); - } - - @Test - public void mangleComprehensionVariable_hasMacro_noOp() throws Exception { - CelAbstractSyntaxTree ast = CEL.compile("has(msg.single_int64)").getAst(); - - CelAbstractSyntaxTree mangledAst = - MUTABLE_AST.mangleComprehensionIdentifierNames(ast, "@c", "@x").ast(); - - assertThat(CEL_UNPARSER.unparse(mangledAst)).isEqualTo("has(msg.single_int64)"); - assertThat( - CEL.createProgram(CEL.check(mangledAst).getAst()) - .eval(ImmutableMap.of("msg", TestAllTypes.getDefaultInstance()))) - .isEqualTo(false); - assertConsistentMacroCalls(ast); - } - - @Test - public void replaceSubtree_iterationLimitReached_throws() throws Exception { - CelAbstractSyntaxTree ast = CEL.compile("true && false").getAst(); - MutableAst mutableAst = MutableAst.newInstance(1); - - IllegalStateException e = - assertThrows( - IllegalStateException.class, - () -> - mutableAst.replaceSubtree( - ast, CelExpr.ofConstantExpr(0, CelConstant.ofValue(false)), 1)); - - assertThat(e).hasMessageThat().isEqualTo("Max iteration count reached."); - } - - /** - * Asserts that the expressions that appears in source_info's macro calls are consistent with the - * actual expr nodes in the AST. - */ - private void assertConsistentMacroCalls(CelAbstractSyntaxTree ast) { - assertThat(ast.getSource().getMacroCalls()).isNotEmpty(); - ImmutableMap allExprs = - CelNavigableAst.fromAst(ast) - .getRoot() - .allNodes() - .map(CelNavigableExpr::expr) - .collect(toImmutableMap(CelExpr::id, node -> node, (expr1, expr2) -> expr1)); - for (CelExpr macroCall : ast.getSource().getMacroCalls().values()) { - assertThat(macroCall.id()).isEqualTo(0); - CelNavigableExpr.fromExpr(macroCall) - .descendants() - .map(CelNavigableExpr::expr) - .forEach( - node -> { - CelExpr e = allExprs.get(node.id()); - if (e != null) { - assertThat(node.id()).isEqualTo(e.id()); - if (e.exprKind().getKind().equals(Kind.COMPREHENSION)) { - assertThat(node.exprKind().getKind()).isEqualTo(Kind.NOT_SET); - } else { - assertThat(node.exprKind().getKind()).isEqualTo(e.exprKind().getKind()); - } - } - }); - } - } -} 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 5973bca8e..53d72de67 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel @@ -1,6 +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", @@ -8,31 +11,38 @@ java_library( srcs = glob(["*.java"]), resources = ["//optimizer/src/test/resources:baselines"], deps = [ - # "//java/com/google/testing/testsize:annotations", "//bundle:cel", - "//common", + "//common:cel_ast", + "//common:cel_source", "//common:compiler_common", + "//common:container", + "//common:mutable_ast", "//common:options", "//common/ast", - "//common/navigation", - "//common/resources/testdata/proto3:test_all_types_java_proto", + "//common/navigation:mutable_navigation", "//common/types", "//extensions", "//extensions:optional_library", + # "//java/com/google/testing/testsize:annotations", "//optimizer", - "//optimizer:mutable_ast", "//optimizer:optimization_exception", "//optimizer:optimizer_builder", "//optimizer/optimizers:common_subexpression_elimination", "//optimizer/optimizers:constant_folding", + "//optimizer/optimizers:inlining", "//parser:macro", - "//parser:operator", "//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", + "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", "@maven//:com_google_guava_guava", ], ) diff --git a/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java b/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java index 2ed564cc9..66f5a94d7 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java @@ -17,15 +17,22 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; +import com.google.common.collect.ImmutableList; +import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; import com.google.testing.junit.testparameterinjector.TestParameters; import dev.cel.bundle.Cel; -import dev.cel.bundle.CelFactory; +import dev.cel.bundle.CelBuilder; 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.types.ListType; import dev.cel.common.types.MapType; import dev.cel.common.types.SimpleType; +import dev.cel.expr.conformance.proto3.TestAllTypes; +import dev.cel.extensions.CelExtensions; import dev.cel.extensions.CelOptionalLibrary; import dev.cel.optimizer.CelOptimizationException; import dev.cel.optimizer.CelOptimizer; @@ -34,31 +41,74 @@ import dev.cel.parser.CelStandardMacro; import dev.cel.parser.CelUnparser; import dev.cel.parser.CelUnparserFactory; -import dev.cel.testing.testdata.proto3.TestAllTypesProto.TestAllTypes; +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 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)) - .addMessageTypes(TestAllTypes.getDescriptor()) - .setContainer("dev.cel.testing.testdata.proto3") - .addCompilerLibraries(CelOptionalLibrary.INSTANCE) - .addRuntimeLibraries(CelOptionalLibrary.INSTANCE) - .build(); - - private static final CelOptimizer CEL_OPTIMIZER = - CelOptimizerFactory.standardCelOptimizerBuilder(CEL) - .addAstOptimizers(ConstantFoldingOptimizer.getInstance()) + private static final CelOptions CEL_OPTIONS = + 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("list_var", ListType.create(SimpleType.STRING)) + .addVar("map_var", MapType.create(SimpleType.STRING, SimpleType.STRING)) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "get_true", + CelOverloadDecl.newGlobalOverload("get_true_overload", SimpleType.BOOL)), + CelFunctionDecl.newFunctionDeclaration( + "get_list", + CelOverloadDecl.newGlobalOverload( + "get_list_overload", + ListType.create(SimpleType.INT), + ListType.create(SimpleType.INT)))) + .addFunctionBindings( + CelFunctionBinding.from("get_true_overload", ImmutableList.of(), unused -> true)) + .addMessageTypes(TestAllTypes.getDescriptor()) + .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) + .setOptions(CEL_OPTIONS) + .addCompilerLibraries( + CelExtensions.bindings(), + CelOptionalLibrary.INSTANCE, + CelExtensions.math(CEL_OPTIONS), + CelExtensions.strings(), + CelExtensions.sets(CEL_OPTIONS), + CelExtensions.encoders(CEL_OPTIONS)) + .addRuntimeLibraries( + CelOptionalLibrary.INSTANCE, + CelExtensions.math(CEL_OPTIONS), + CelExtensions.strings(), + CelExtensions.sets(CEL_OPTIONS), + CelExtensions.encoders(CEL_OPTIONS)) + .build(); + } + @Test @TestParameters("{source: 'null', expected: 'null'}") @TestParameters("{source: '1 + 2', expected: '3'}") @@ -130,18 +180,23 @@ public class ConstantFoldingOptimizerTest { + " optional.of(1), ?y: optional.none()}'}") @TestParameters( "{source: 'TestAllTypes{single_int64: 1 + 2 + 3 + x}', " - + " expected: 'TestAllTypes{single_int64: 6 + x}'}") + + " expected: 'cel.expr.conformance.proto3.TestAllTypes{single_int64: 6 + x}'}") @TestParameters( "{source: 'TestAllTypes{?single_int64: optional.ofNonZeroValue(1)}', " - + " expected: 'TestAllTypes{single_int64: 1}'}") + + " expected: 'cel.expr.conformance.proto3.TestAllTypes{single_int64: 1}'}") @TestParameters( "{source: 'TestAllTypes{?single_int64: optional.ofNonZeroValue(0)}', " - + " expected: 'TestAllTypes{}'}") + + " expected: 'cel.expr.conformance.proto3.TestAllTypes{}'}") @TestParameters( "{source: 'TestAllTypes{?single_int64: optional.ofNonZeroValue(1), ?single_int32:" + " optional.of(4), ?single_uint64: optional.ofNonZeroValue(x)}', expected:" - + " 'TestAllTypes{single_int64: 1, single_int32: 4, ?single_uint64:" - + " optional.ofNonZeroValue(x)}'}") + + " 'cel.expr.conformance.proto3.TestAllTypes{single_int64: 1, single_int32: 4," + + " ?single_uint64: optional.ofNonZeroValue(x)}'}") + @TestParameters( + "{source: 'TestAllTypes{single_nested_message: TestAllTypes.NestedMessage{bb:" + + " 42}}.single_nested_message.bb', expected: '42'}") + @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'}") @TestParameters("{source: '{\"hello\": \"world\"}[\"hello\"] == x', expected: '\"world\" == x'}") @TestParameters("{source: '{\"hello\": \"world\"}.?hello', expected: 'optional.of(\"world\")'}") @@ -159,12 +214,53 @@ public class ConstantFoldingOptimizerTest { "{source: '{\"a\": dyn([1, 2]), \"b\": x}', expected: '{\"a\": [1, 2], \"b\": x}'}") @TestParameters("{source: 'map_var[?\"key\"]', expected: 'map_var[?\"key\"]'}") @TestParameters("{source: '\"abc\" in list_var', expected: '\"abc\" in list_var'}") + @TestParameters("{source: '[?optional.none(), [?optional.none()]]', expected: '[[]]'}") + @TestParameters("{source: 'math.greatest(1.0, 2, 3.0)', expected: '3.0'}") + @TestParameters("{source: '\"world\".charAt(1)', expected: '\"o\"'}") + @TestParameters("{source: 'base64.encode(b\"hello\")', expected: '\"aGVsbG8=\"'}") + @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: 'true == false', expected: 'false'}") + @TestParameters("{source: 'true == true', expected: 'true'}") + @TestParameters("{source: 'false == true', expected: 'false'}") + @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: 'true != false', expected: 'true'}") + @TestParameters("{source: 'true != true', expected: 'false'}") + @TestParameters("{source: 'false != true', expected: 'true'}") + @TestParameters("{source: 'false != false', expected: 'false'}") + @TestParameters("{source: '10 != 42', expected: 'true'}") + @TestParameters("{source: '42 != 42', expected: 'false'}") + @TestParameters("{source: '[\"foo\",\"bar\"] == [\"foo\",\"bar\"]', expected: 'true'}") + @TestParameters("{source: '[\"bar\",\"foo\"] == [\"foo\",\"bar\"]', expected: 'false'}") + @TestParameters("{source: 'duration(\"1h\") - duration(\"60m\")', expected: 'duration(\"0s\")'}") + @TestParameters( + "{source: 'duration(\"2h23m42s12ms42us92ns\") + duration(\"129481231298125ns\")', expected:" + + " 'duration(\"138103.243340217s\")'}") + @TestParameters( + "{source: 'timestamp(900000) - timestamp(100)', expected: 'duration(\"899900s\")'}") + @TestParameters( + "{source: 'timestamp(\"2000-01-01T00:02:03.2123Z\") + duration(\"25h2m32s42ms53us29ns\")'," + + " expected: 'timestamp(\"2000-01-02T01:04:35.254353029Z\")'}") + @TestParameters( + "{source: 'has({\"req\": \"Avail\"}.opt) ? ({\"req\": \"Avail\"}.req + \" \" +" + + " {\"req\": \"Avail\"}.opt) : {\"req\": \"Avail\"}.req', expected: '\"Avail\"'}") // TODO: Support folding lists with mixed types. This requires mutable lists. // @TestParameters("{source: 'dyn([1]) + [1.0]'}") public void constantFold_success(String source, String expected) throws Exception { - 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); } @@ -183,6 +279,10 @@ public void constantFold_success(String source, String expected) throws Exceptio @TestParameters( "{source: '[1, 2, 3].map(i, [1, 2, 3].map(j, i * j).filter(k, k % 2 == x))', " + "expected: '[1, 2, 3].map(i, [1, 2, 3].map(j, i * j).filter(k, k % 2 == x))'}") + @TestParameters("{source: '[1, 2, 3, 4].all(i, v, i < v)', expected: 'true'}") + @TestParameters( + "{source: '[1 + 1, 2 + 2, 3 + 3].all(i, v, i < 5 && v < x)', " + + "expected: '[2, 4, 6].all(i, v, i < 5 && v < x)'}") @TestParameters( "{source: '[{}, {\"a\": 1}, {\"b\": 2}].filter(m, has(m.a))', expected: '[{\"a\": 1}]'}") @TestParameters( @@ -198,17 +298,26 @@ public void constantFold_success(String source, String expected) throws Exceptio @TestParameters( "{source: '[{}, {\"a\": 1}, {\"b\": 2}].filter(m, has(x.a))', expected:" + " '[{}, {\"a\": 1}, {\"b\": 2}].filter(m, has(x.a))'}") + @TestParameters( + "{source: 'cel.bind(r0, [1, 2, 3], cel.bind(r1, 1 in r0 && 2 in x, r1))', expected:" + + " 'cel.bind(r0, [1, 2, 3], cel.bind(r1, 1 in r0 && 2 in x, r1))'}") + @TestParameters("{source: 'false ? false : cel.bind(a, x, a)', expected: 'cel.bind(a, x, a)'}") + @TestParameters( + "{source: 'cel.bind(myMap, {\"foo\": \"bar\"}, myMap[?\"foo\"].optMap(x, x + \"baz\"))', " + + "expected: 'optional.of(\"barbaz\")'}") public void constantFold_macros_macroCallMetadataPopulated(String source, String expected) throws Exception { Cel cel = - 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()) - .addCompilerLibraries(CelOptionalLibrary.INSTANCE) - .addRuntimeLibraries(CelOptionalLibrary.INSTANCE) + .setOptions(CEL_OPTIONS) + .addCompilerLibraries( + CelExtensions.bindings(), CelExtensions.optional(), CelExtensions.comprehensions()) + .addRuntimeLibraries(CelExtensions.optional(), CelExtensions.comprehensions()) .build(); CelOptimizer celOptimizer = CelOptimizerFactory.standardCelOptimizerBuilder(cel) @@ -241,15 +350,22 @@ public void constantFold_macros_macroCallMetadataPopulated(String source, String @TestParameters( "{source: '[{}, {\"a\": 1}, {\"b\": 2}].filter(m, has({\"a\": true}.a)) == " + " [{}, {\"a\": 1}, {\"b\": 2}]'}") + @TestParameters("{source: 'cel.bind(r0, [1, 2, 3], cel.bind(r1, 1 in r0 && 2 in r0, r1))'}") + @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()) - .addCompilerLibraries(CelOptionalLibrary.INSTANCE) + .setOptions( + CelOptions.current() + .enableHeterogeneousNumericComparisons(true) + .populateMacroCalls(false) + .build()) + .addCompilerLibraries(CelExtensions.bindings(), CelOptionalLibrary.INSTANCE) .addRuntimeLibraries(CelOptionalLibrary.INSTANCE) .build(); CelOptimizer celOptimizer = @@ -280,23 +396,64 @@ public void constantFold_macros_withoutMacroCallMetadata(String source) throws E @TestParameters("{source: 'optional.none()'}") @TestParameters("{source: '[optional.none()]'}") @TestParameters("{source: '[?x.?y]'}") - @TestParameters("{source: 'TestAllTypes{single_int32: x, repeated_int32: [1, 2, 3]}'}") + @TestParameters( + "{source: 'cel.expr.conformance.proto3.TestAllTypes{" + + "single_int32: x, repeated_int32: [1, 2, 3]}'}") + @TestParameters("{source: 'get_true() == get_true()'}") + @TestParameters("{source: 'get_true() == true'}") + @TestParameters("{source: 'x == x'}") + @TestParameters("{source: 'x == 42'}") + @TestParameters("{source: 'timestamp(100)'}") + @TestParameters("{source: 'duration(\"1h\")'}") + @TestParameters("{source: '[true].exists(x, x == get_true())'}") + @TestParameters("{source: 'get_list([1, 2]).map(x, x * 2)'}") + @TestParameters("{source: '[(x - 1 > 3) ? (x - 1) : 5].exists(x, x - 1 > 3)'}") public void constantFold_noOp(String source) throws Exception { - CelAbstractSyntaxTree ast = CEL.compile(source).getAst(); + CelAbstractSyntaxTree ast = 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(); + ConstantFoldingOptions options = + ConstantFoldingOptions.newBuilder().addFoldableFunctions("get_true").build(); + CelOptimizer optimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(cel) + .addAstOptimizers(ConstantFoldingOptimizer.newInstance(options)) + .build(); + + CelAbstractSyntaxTree optimizedAst = optimizer.optimize(ast); + + assertThat(CEL_UNPARSER.unparse(optimizedAst)).isEqualTo("true"); + } + + @Test + public void constantFold_withExpectedResultTypeSet_success() throws Exception { + Cel cel = runtimeFlavor.builder().setResultType(SimpleType.STRING).build(); + CelOptimizer optimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(cel) + .addAstOptimizers(ConstantFoldingOptimizer.getInstance()) + .build(); + CelAbstractSyntaxTree ast = cel.compile("string(!true)").getAst(); + + CelAbstractSyntaxTree optimizedAst = optimizer.optimize(ast); + + assertThat(CEL_UNPARSER.unparse(optimizedAst)).isEqualTo("\"false\""); + } + @Test 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) @@ -345,7 +502,7 @@ public void constantFold_withMacroCallPopulated_comprehensionsAreReplacedWithNot "CALL [0] {\n" + " function: map\n" + " target: {\n" - + " CREATE_LIST [3] {\n" + + " LIST [3] {\n" + " elements: {\n" + " CONSTANT [4] { value: 1 }\n" + " CONSTANT [5] { value: 2 }\n" @@ -366,13 +523,13 @@ 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( - "CREATE_LIST [1] {\n" + "LIST [1] {\n" + " elements: {\n" + " CONSTANT [2] { value: 1 }\n" + " CONSTANT [3] { value: 2 }\n" @@ -383,25 +540,26 @@ 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."); } + + } diff --git a/optimizer/src/test/java/dev/cel/optimizer/optimizers/InliningOptimizerTest.java b/optimizer/src/test/java/dev/cel/optimizer/optimizers/InliningOptimizerTest.java new file mode 100644 index 000000000..da2e9b745 --- /dev/null +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/InliningOptimizerTest.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.optimizer.optimizers; + +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.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.CelSource; +import dev.cel.common.ast.CelExpr; +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.CelOptimizationException; +import dev.cel.optimizer.CelOptimizer; +import dev.cel.optimizer.CelOptimizerFactory; +import dev.cel.optimizer.optimizers.InliningOptimizer.InlineVariable; +import dev.cel.optimizer.optimizers.InliningOptimizer.InliningOptions; +import dev.cel.optimizer.optimizers.SubexpressionOptimizer.SubexpressionOptimizerOptions; +import dev.cel.parser.CelStandardMacro; +import dev.cel.parser.CelUnparserFactory; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public class InliningOptimizerTest { + + private static final Cel CEL = + CelFactory.standardCelBuilder() + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .setContainer(CelContainer.ofName("google.expr.proto3.test")) + .addFileTypes(TestAllTypes.getDescriptor().getFile()) + .addCompilerLibraries(CelExtensions.bindings()) + .addVar("int_var", SimpleType.INT) + .addVar("dyn_var", SimpleType.DYN) + .addVar("msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())) + .addVar("wrapper_var", StructTypeReference.create("google.protobuf.Int64Value")) + .addVar( + "child", + StructTypeReference.create(TestAllTypes.NestedMessage.getDescriptor().getFullName())) + .addVar("shadowed_ident", SimpleType.INT) + .setOptions(CelOptions.current().populateMacroCalls(true).build()) + .build(); + + @Test + public void inlining_success(@TestParameter SuccessTestCase testCase) throws Exception { + CelAbstractSyntaxTree astToInline = CEL.compile(testCase.source).getAst(); + CelAbstractSyntaxTree replacementAst = CEL.compile(testCase.replacement).getAst(); + + CelOptimizer optimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(CEL) + .addAstOptimizers( + InliningOptimizer.newInstance( + InlineVariable.of(testCase.inlineVarName, replacementAst))) + .build(); + + CelAbstractSyntaxTree optimized = optimizer.optimize(astToInline); + + String unparsed = CelUnparserFactory.newUnparser().unparse(optimized); + assertThat(unparsed).isEqualTo(testCase.expected); + } + + @Test + public void inlining_noop(@TestParameter NoOpTestCase testCase) throws Exception { + CelAbstractSyntaxTree astToInline = CEL.compile(testCase.source).getAst(); + CelAbstractSyntaxTree replacementAst = CEL.compile(testCase.replacement).getAst(); + + CelOptimizer optimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(CEL) + .addAstOptimizers( + InliningOptimizer.newInstance(InlineVariable.of(testCase.varName, replacementAst))) + .build(); + + CelAbstractSyntaxTree optimized = optimizer.optimize(astToInline); + + String unparsed = CelUnparserFactory.newUnparser().unparse(optimized); + assertThat(unparsed).isEqualTo(testCase.source); + } + + private enum SuccessTestCase { + CONSTANT( + /* source= */ "int_var + 2 + int_var", + /* inlineVarName= */ "int_var", + /* replacementExpr= */ "1", + /* expected= */ "1 + 2 + 1"), + REPEATED( + /* source= */ "dyn_var + [dyn_var]", + /* inlineVarName= */ "dyn_var", + /* replacementExpr= */ "dyn([1, 2])", + /* expected= */ "dyn([1, 2]) + [dyn([1, 2])]"), + SELECT_WITH_MACRO( + /* source= */ "has(msg.single_any.processing_purpose) ?" + + " msg.single_any.processing_purpose.map(i, i * 2)[0] : 42", + /* inlineVarName= */ "msg.single_any.processing_purpose", + /* replacementExpr= */ "[1,2,3].map(i, i * 2)", + /* expected= */ "([1, 2, 3].map(i, i * 2).size() != 0) ? ([1, 2, 3].map(i, i * 2).map(i, i" + + " * 2)[0]) : 42"), + PRESENCE_WITH_SELECT_EXPR( + /* source= */ "has(msg.single_any)", + /* inlineVarName= */ "msg.single_any", + /* replacementExpr= */ "msg.single_int64_wrapper", + /* expected= */ "has(msg.single_int64_wrapper)"), + PRESENCE_WITH_IDENT_NOT_NULL_REWRITE( + /* source= */ "has(msg.single_int64_wrapper)", + /* inlineVarName= */ "msg.single_int64_wrapper", + /* replacementExpr= */ "wrapper_var", + /* expected= */ "wrapper_var != null"), + PRESENCE_WITH_LIST_SIZE_REWRITE( + /* source= */ "has(msg.single_any.processing_purpose)", + /* inlineVarName= */ "msg.single_any.processing_purpose", + /* replacementExpr= */ "[1, 2, 3]", + /* expected= */ "[1, 2, 3].size() != 0"), + PRESENCE_WITH_INT_LITERAL_REWRITE( + /* source= */ "has(msg.single_any.processing_purpose)", + /* inlineVarName= */ "msg.single_any.processing_purpose", + /* replacementExpr= */ "1", + /* expected= */ "1 != 0"), + PRESENCE_WITH_UINT_LITERAL_REWRITE( + /* source= */ "has(msg.single_any.processing_purpose)", + /* inlineVarName= */ "msg.single_any.processing_purpose", + /* replacementExpr= */ "1u", + /* expected= */ "1u != 0u"), + PRESENCE_WITH_DOUBLE_LITERAL_REWRITE( + /* source= */ "has(msg.single_any.processing_purpose)", + /* inlineVarName= */ "msg.single_any.processing_purpose", + /* replacementExpr= */ "1.5", + /* expected= */ "1.5 != 0.0"), + PRESENCE_WITH_BOOL_LITERAL_REWRITE( + /* source= */ "has(msg.single_any.processing_purpose)", + /* inlineVarName= */ "msg.single_any.processing_purpose", + /* replacementExpr= */ "true", + /* expected= */ "true != false"), + PRESENCE_WITH_STRING_LITERAL_REWRITE( + /* source= */ "has(msg.single_any.processing_purpose)", + /* inlineVarName= */ "msg.single_any.processing_purpose", + /* replacementExpr= */ "'foo'", + /* expected= */ "\"foo\".size() != 0"), + PRESENCE_WITH_BYTES_LITERAL_REWRITE( + /* source= */ "has(msg.single_any.processing_purpose)", + /* inlineVarName= */ "msg.single_any.processing_purpose", + /* replacementExpr= */ "b'abc'", + /* expected= */ "b\"\\141\\142\\143\".size() != 0"), + PRESENCE_WITH_TIMESTAMP_LITERAL_REWRITE( + /* source= */ "has(msg.single_any.processing_purpose)", + /* inlineVarName= */ "msg.single_any.processing_purpose", + /* replacementExpr= */ "timestamp(1)", + /* expected= */ "timestamp(1) != timestamp(0)"), + PRESENCE_WITH_DURATION_LITERAL_REWRITE( + /* source= */ "has(msg.single_any.processing_purpose)", + /* inlineVarName= */ "msg.single_any.processing_purpose", + /* replacementExpr= */ "duration('1h')", + /* expected= */ "duration(\"1h\") != duration(\"0\")"), + PRESENCE_WITH_PROTOBUF_MESSAGE_REWRITE( + /* source= */ "has(msg.single_any.processing_purpose)", + /* inlineVarName= */ "msg.single_any.processing_purpose", + /* replacementExpr= */ "cel.expr.conformance.proto3.TestAllTypes{single_int64: 1}", + /* expected= */ "cel.expr.conformance.proto3.TestAllTypes{single_int64: 1} !=" + + " cel.expr.conformance.proto3.TestAllTypes{}"), + NESTED_SELECT( + /* source= */ "msg.standalone_message.bb", + /* inlineVarName= */ "msg.standalone_message", + /* replacementExpr= */ "child", + /* expected= */ "child.bb"), + ; + + private final String source; + private final String inlineVarName; + private final String replacement; + private final String expected; + + SuccessTestCase(String source, String inlineVarName, String replacementExpr, String expected) { + this.source = source; + this.inlineVarName = inlineVarName; + this.replacement = replacementExpr; + this.expected = expected; + } + } + + private enum NoOpTestCase { + NO_INLINE_ITER_VAR("[0].exists(shadowed_ident, shadowed_ident == 0)", "shadowed_ident", "1"), + NO_INLINE_BIND_VAR("cel.bind(shadowed_ident, 2, shadowed_ident + 1)", "shadowed_ident", "1"), + ; + + private final String source; + private final String varName; + private final String replacement; + + NoOpTestCase(String source, String varName, String replacement) { + this.source = source; + this.varName = varName; + this.replacement = replacement; + } + } + + @Test + public void inline_exceededIterationLimit_throws() throws Exception { + String expression = "int_var + int_var + int_var"; + CelAbstractSyntaxTree astToInline = CEL.compile(expression).getAst(); + CelOptimizer optimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(CEL) + .addAstOptimizers( + InliningOptimizer.newInstance( + InliningOptions.newBuilder().maxIterationLimit(2).build(), + InlineVariable.of("int_var", CEL.compile("1").getAst()))) + .build(); + + CelOptimizationException e = + assertThrows(CelOptimizationException.class, () -> optimizer.optimize(astToInline)); + assertThat(e).hasMessageThat().contains("Max iteration count reached."); + } + + @Test + public void inlineVariableDecl_internalVar_throws() throws Exception { + IllegalArgumentException e = + assertThrows( + IllegalArgumentException.class, + () -> + InlineVariable.of( + "@internal_var", + CelAbstractSyntaxTree.newParsedAst( + CelExpr.ofNotSet(0L), CelSource.newBuilder().build()))); + assertThat(e).hasMessageThat().contains("Internal variables cannot be inlined: @internal_var"); + } + + @Test + public void inline_then_cse() throws Exception { + String source = + "has(msg.single_any.processing_purpose) ? " + + "msg.single_any.processing_purpose.map(i, i * 2)[0] : 42"; + CelAbstractSyntaxTree astToInline = CEL.compile(source).getAst(); + + CelOptimizer optimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(CEL) + .addAstOptimizers( + InliningOptimizer.newInstance( + InlineVariable.of( + "msg.single_any.processing_purpose", + CEL.compile("[1,2,3].map(i, i * 2)").getAst())), + SubexpressionOptimizer.newInstance( + SubexpressionOptimizerOptions.newBuilder().populateMacroCalls(true).build())) + .build(); + + CelAbstractSyntaxTree optimized = optimizer.optimize(astToInline); + + String unparsed = CelUnparserFactory.newUnparser().unparse(optimized); + assertThat(unparsed) + .isEqualTo( + "cel.@block([[1, 2, 3].map(@it:0:0, @it:0:0 * 2)], " + + "(@index0.size() != 0) ? (@index0.map(@it:1:0, @it:1:0 * 2)[0]) : 42)"); + } + + @Test + public void allowInliningDependentVariables_inOrder_dependentVariablesInlined() throws Exception { + String source = "int_var + dyn_var"; + CelAbstractSyntaxTree astToInline = CEL.compile(source).getAst(); + + CelOptimizer optimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(CEL) + .addAstOptimizers( + InliningOptimizer.newInstance( + InlineVariable.of("int_var", CEL.compile("dyn_var").getAst()), + InlineVariable.of("dyn_var", CEL.compile("2").getAst()))) + .build(); + + CelAbstractSyntaxTree optimized = optimizer.optimize(astToInline); + + String unparsed = CelUnparserFactory.newUnparser().unparse(optimized); + assertThat(unparsed).isEqualTo("2 + 2"); + } + + @Test + public void allowInliningDependentVariables_reverseOrder_inliningIsIndependent() + throws Exception { + String source = "int_var + dyn_var"; + CelAbstractSyntaxTree astToInline = CEL.compile(source).getAst(); + + CelOptimizer optimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(CEL) + .addAstOptimizers( + InliningOptimizer.newInstance( + InlineVariable.of("dyn_var", CEL.compile("2").getAst()), + InlineVariable.of("int_var", CEL.compile("dyn_var").getAst()))) + .build(); + + CelAbstractSyntaxTree optimized = optimizer.optimize(astToInline); + + String unparsed = CelUnparserFactory.newUnparser().unparse(optimized); + assertThat(unparsed).isEqualTo("dyn_var + 2"); + } +} 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 278a79976..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,25 +24,26 @@ // 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; import dev.cel.common.CelOptions; import dev.cel.common.types.OptionalType; import dev.cel.common.types.SimpleType; import dev.cel.common.types.StructTypeReference; +import dev.cel.expr.conformance.proto3.NestedTestAllTypes; +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.SubexpressionOptimizer.SubexpressionOptimizerOptions; import dev.cel.parser.CelStandardMacro; import dev.cel.parser.CelUnparser; import dev.cel.parser.CelUnparserFactory; -import dev.cel.runtime.CelRuntime.CelFunctionBinding; +import dev.cel.runtime.CelFunctionBinding; import dev.cel.testing.BaselineTestCase; -import dev.cel.testing.testdata.proto3.TestAllTypesProto.NestedTestAllTypes; -import dev.cel.testing.testdata.proto3.TestAllTypesProto.TestAllTypes; +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,12 +105,10 @@ 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() .populateMacroCalls(true) - .enableCelBlock(true) .addEliminableFunctions("pure_custom_func") .build(); @@ -80,6 +116,7 @@ public class SubexpressionOptimizerBaselineTest extends BaselineTestCase { @Before public void setUp() { + this.cel = setupCelEnv(runtimeFlavor.builder()); overriddenBaseFilePath = ""; } @@ -91,41 +128,102 @@ 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(); - Object expectedEvalResult = - CEL.createProgram(ast) - .eval(ImmutableMap.of("msg", TEST_ALL_TYPES_INPUT, "x", 5L, "opt_x", Optional.of(5L))); + 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); + + 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.celOptimizer.optimize(ast); + CelAbstractSyntaxTree optimizedAst = cseTestOptimizer.newCseOptimizer(cel).optimize(ast); + CelAbstractSyntaxTree parsedOnlyOptimizedAst = + CelAbstractSyntaxTree.newParsedAst(optimizedAst.getExpr(), optimizedAst.getSource()); - Object optimizedEvalResult = - CEL.createProgram(optimizedAst) - .eval(ImmutableMap.of("msg", TEST_ALL_TYPES_INPUT, "x", 5L, "opt_x", Optional.of(5L))); + 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 = cseTestOptimizer.celOptimizer.optimize(ast); + CelAbstractSyntaxTree optimizedAst; + try { + 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) + .eval( + ImmutableMap.of( + "msg", TEST_ALL_TYPES_INPUT, "x", 5L, "y", 6L, "opt_x", Optional.of(5L))); + testOutput().println("Result: " + optimizedEvalResult); + resultPrinted = true; + } + try { + testOutput().printf("[%s]: %s", optimizerName, CEL_UNPARSER.unparse(optimizedAst)); + } catch (RuntimeException e) { + testOutput().printf("[%s]: Unparse Error: %s", optimizerName, e); + } + testOutput().println(); + } + testOutput().println(); + } + } + + @Test + public void constfold_before_subexpression_unparsed() throws Exception { + 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(); + boolean resultPrinted = false; + for (CseTestOptimizer cseTestOptimizer : EnumSet.allOf(CseTestOptimizer.class)) { + String optimizerName = cseTestOptimizer.name(); + CelAbstractSyntaxTree optimizedAst = + 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; } @@ -144,77 +242,22 @@ public void 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.celOptimizer.optimize(ast); + CelAbstractSyntaxTree ast = cel.compile(cseTestCase.source).getAst(); + CelAbstractSyntaxTree optimizedAst = + newCseOptimizer(cel, cseTestOptimizer.option).optimize(ast); testOutput().println(optimizedAst.getExpr()); } } - @Test - public void populateMacroCallsDisabled_macroMapUnpopulated(@TestParameter CseTestCase testCase) - throws Exception { - skipBaselineVerification(); - Cel cel = newCelBuilder().build(); - CelOptimizer celOptimizerWithBinds = - newCseOptimizer( - cel, - SubexpressionOptimizerOptions.newBuilder() - .populateMacroCalls(false) - .enableCelBlock(false) - .build()); - CelOptimizer celOptimizerWithBlocks = - newCseOptimizer( - cel, - SubexpressionOptimizerOptions.newBuilder() - .populateMacroCalls(false) - .enableCelBlock(true) - .build()); - CelOptimizer celOptimizerWithFlattenedBlocks = - newCseOptimizer( - cel, - SubexpressionOptimizerOptions.newBuilder() - .populateMacroCalls(false) - .enableCelBlock(true) - .subexpressionMaxRecursionDepth(1) - .build()); - CelAbstractSyntaxTree originalAst = cel.compile(testCase.source).getAst(); - - CelAbstractSyntaxTree astOptimizedWithBinds = celOptimizerWithBinds.optimize(originalAst); - CelAbstractSyntaxTree astOptimizedWithBlocks = celOptimizerWithBlocks.optimize(originalAst); - CelAbstractSyntaxTree astOptimizedWithFlattenedBlocks = - celOptimizerWithFlattenedBlocks.optimize(originalAst); - - assertThat(astOptimizedWithBinds.getSource().getMacroCalls()).isEmpty(); - assertThat(astOptimizedWithBlocks.getSource().getMacroCalls()).isEmpty(); - assertThat(astOptimizedWithFlattenedBlocks.getSource().getMacroCalls()).isEmpty(); - } - - @Test - public void large_expressions_bind_cascaded() throws Exception { - CelOptimizer celOptimizer = - newCseOptimizer( - CEL, - SubexpressionOptimizerOptions.newBuilder() - .populateMacroCalls(true) - .enableCelBlock(false) - .build()); - - runLargeTestCases(celOptimizer); - } - @Test public void large_expressions_block_common_subexpr() throws Exception { CelOptimizer celOptimizer = newCseOptimizer( - CEL, - SubexpressionOptimizerOptions.newBuilder() - .populateMacroCalls(true) - .enableCelBlock(true) - .build()); + cel, SubexpressionOptimizerOptions.newBuilder().populateMacroCalls(true).build()); runLargeTestCases(celOptimizer); } @@ -223,10 +266,9 @@ 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) - .enableCelBlock(true) .subexpressionMaxRecursionDepth(1) .build()); @@ -237,10 +279,9 @@ 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) - .enableCelBlock(true) .subexpressionMaxRecursionDepth(2) .build()); @@ -251,10 +292,9 @@ 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) - .enableCelBlock(true) .subexpressionMaxRecursionDepth(3) .build()); @@ -262,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); @@ -284,32 +323,6 @@ private void runLargeTestCases(CelOptimizer celOptimizer) throws Exception { } } - private static CelBuilder newCelBuilder() { - return CelFactory.standardCelBuilder() - .addMessageTypes(TestAllTypes.getDescriptor()) - .setContainer("dev.cel.testing.testdata.proto3") - .setStandardMacros(CelStandardMacro.STANDARD_MACROS) - .setOptions( - CelOptions.current().enableTimestampEpoch(true).populateMacroCalls(true).build()) - .addCompilerLibraries(CelOptionalLibrary.INSTANCE, CelExtensions.bindings()) - .addRuntimeLibraries(CelOptionalLibrary.INSTANCE) - .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("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)) @@ -318,7 +331,6 @@ private static CelOptimizer newCseOptimizer(Cel cel, SubexpressionOptimizerOptio @SuppressWarnings("Immutable") // Test only private enum CseTestOptimizer { - CASCADED_BINDS(OPTIMIZER_COMMON_OPTIONS.toBuilder().enableCelBlock(false).build()), BLOCK_COMMON_SUBEXPR_ONLY(OPTIMIZER_COMMON_OPTIONS), BLOCK_RECURSION_DEPTH_1( OPTIMIZER_COMMON_OPTIONS.toBuilder().subexpressionMaxRecursionDepth(1).build()), @@ -339,10 +351,23 @@ private enum CseTestOptimizer { BLOCK_RECURSION_DEPTH_9( OPTIMIZER_COMMON_OPTIONS.toBuilder().subexpressionMaxRecursionDepth(9).build()); - private final CelOptimizer celOptimizer; + private final SubexpressionOptimizerOptions option; CseTestOptimizer(SubexpressionOptimizerOptions option) { - this.celOptimizer = newCseOptimizer(CEL, option); + 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(); } } @@ -417,15 +442,46 @@ private enum CseTestCase { MULTIPLE_MACROS_2( "[[1].exists(i, i > 0)] + [[1].exists(j, j > 0)] + [['a'].exists(k, k == 'a')] +" + " [['a'].exists(l, l == 'a')] == [true, true, true, true]"), + MULTIPLE_MACROS_3( + "[1].exists(i, i > 0) && [1].exists(j, j > 0) && [1].exists(k, k > 1) && [2].exists(l, l >" + + " 1)"), + MULTIPLE_MACROS_COMP_V2_1( + // Note that all of these have different iteration variables, but they are still logically + // the same. + "size([[1].exists(i,x,i > 0 && x >= 0)]) + size([[1].exists(j, y, j > 0 && y >= 0)]) + " + + "size([[2].exists(k,z,k > 1 && z >= 0)]) + size([[2].exists(l, w, l > 1 && w >= 0)]) " + + " == 4"), + MULTIPLE_MACROS_COMP_V2_2( + "[1].exists(i, x, i > 0 && x > 0) && [1].exists(j, y, j > 0 && y > 0) && " + + "[1].exists(k, z, k > 1 && z > 0) && [2].exists(l, w, l > 1 && w > 0)"), NESTED_MACROS("[1,2,3].map(i, [1, 2, 3].map(i, i + 1)) == [[2, 3, 4], [2, 3, 4], [2, 3, 4]]"), NESTED_MACROS_2("[1, 2].map(y, [1, 2, 3].filter(x, x == y)) == [[1], [2]]"), + NESTED_MACROS_3("[1,2,3].map(i, i * 2) + [1,2,3].map(i, i * 2).map(i, i * 2)"), + NESTED_MACROS_COMP_V2_1( + "[1,2,3].transformList(i, v, [1, 2, 3].transformList(i, v, i + v + 1)) == " + + "[[2, 4, 6], [2, 4, 6], [2, 4, 6]]"), + NESTED_MACROS_COMP_V2_2( + "[1, 2].transformList(i, y, [1, 2, 3].filter(x, x == y && i < y)) == [[1], [2]]"), + ADJACENT_NESTED_MACROS( + "[1,2,3].map(i, [1, 2, 3].map(i, i + 1)) == [1,2,3].map(j, [1, 2, 3].map(j, j + 1))"), + ADJACENT_NESTED_MACROS_COMP_V2( + "[1,2,3].transformMap(i, x, [1, 2, 3].transformMap(i, x, i + x + 1)) ==" + + " [1,2,3].transformMap(j, y, [1, 2, 3].transformMap(j, y, j + y + 1))"), INCLUSION_LIST("1 in [1,2,3] && 2 in [1,2,3] && 3 in [3, [1,2,3]] && 1 in [1,2,3]"), INCLUSION_MAP("2 in {'a': 1, 2: {true: false}, 3: {true: false}}"), MACRO_ITER_VAR_NOT_REFERENCED( "[1,2].map(i, [1, 2].map(i, [3,4])) == [[[3, 4], [3, 4]], [[3, 4], [3, 4]]]"), + MACRO_ITER_VAR2_NOT_REFERENCED( + "[1,2].transformList(i, v, [1, 2].transformList(i, v, [3,4])) == " + + "[[[3, 4], [3, 4]], [[3, 4], [3, 4]]]"), MACRO_SHADOWED_VARIABLE("[x - 1 > 3 ? x - 1 : 5].exists(x, x - 1 > 3) || x - 1 > 3"), MACRO_SHADOWED_VARIABLE_2("[\"foo\", \"bar\"].map(x, [x + x, x + x]).map(x, [x + x, x + x])"), + MACRO_SHADOWED_VARIABLE_COMP_V2_1( + "[x - y - 1 > 3 ? x - y - 1 : 5].exists(x, y, x - y - 1 > 3) || x - y - 1 > 3"), + MACRO_SHADOWED_VARIABLE_COMP_V2_2( + "[\"foo\", \"bar\"].transformMap(x, y, [x + x, y + y]).transformMap(x, y, [x + x, y + y])"), PRESENCE_TEST("has({'a': true}.a) && {'a':true}['a']"), + PRESENCE_TEST_2("has({'a': true}.a) && has({'a': true}.a)"), PRESENCE_TEST_WITH_TERNARY( "(has(msg.oneof_type.payload) ? msg.oneof_type.payload.single_int64 : 0) == 10"), PRESENCE_TEST_WITH_TERNARY_2( 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 3668d776e..e7387d7d8 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerTest.java +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerTest.java @@ -18,6 +18,7 @@ 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; @@ -28,6 +29,7 @@ import dev.cel.bundle.CelFactory; 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.CelSource.Extension; @@ -35,70 +37,103 @@ import dev.cel.common.CelSource.Extension.Version; import dev.cel.common.CelValidationException; import dev.cel.common.CelVarDecl; -import dev.cel.common.ast.CelConstant; -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.common.navigation.CelNavigableMutableAst; +import dev.cel.common.navigation.CelNavigableMutableExpr; import dev.cel.common.types.ListType; import dev.cel.common.types.SimpleType; import dev.cel.common.types.StructTypeReference; +import dev.cel.expr.conformance.proto3.TestAllTypes; import dev.cel.extensions.CelExtensions; import dev.cel.optimizer.CelOptimizationException; import dev.cel.optimizer.CelOptimizer; import dev.cel.optimizer.CelOptimizerFactory; -import dev.cel.optimizer.MutableAst; import dev.cel.optimizer.optimizers.SubexpressionOptimizer.SubexpressionOptimizerOptions; import dev.cel.parser.CelStandardMacro; import dev.cel.parser.CelUnparser; import dev.cel.parser.CelUnparserFactory; -import dev.cel.parser.Operator; +import dev.cel.runtime.CelAttributePattern; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.CelFunctionBinding; import dev.cel.runtime.CelRuntime; -import dev.cel.runtime.CelRuntime.CelFunctionBinding; import dev.cel.runtime.CelRuntimeFactory; -import dev.cel.testing.testdata.proto3.TestAllTypesProto.TestAllTypes; -import java.util.Optional; +import dev.cel.runtime.CelUnknownSet; +import dev.cel.runtime.PartialVars; +import dev.cel.runtime.Program; +import dev.cel.testing.CelRuntimeFlavor; +import java.util.List; import java.util.concurrent.atomic.AtomicInteger; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(TestParameterInjector.class) public class SubexpressionOptimizerTest { - private 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(); @@ -106,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( @@ -117,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(); } @@ -130,17 +164,55 @@ public void cse_resultTypeSet_celBlockOptimizationSuccess() throws Exception { CelOptimizerFactory.standardCelOptimizerBuilder(cel) .addAstOptimizers( SubexpressionOptimizer.newInstance( - SubexpressionOptimizerOptions.newBuilder().enableCelBlock(true).build())) + 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\")"), @@ -159,7 +231,8 @@ private enum CseNoOpTestCase { NESTED_FUNCTION("int(timestamp(int(timestamp(1000000000))))"), // This cannot be optimized. Extracting the common subexpression would presence test // the bound identifier (e.g: has(@r0)), which is not valid. - UNOPTIMIZABLE_TERNARY("has(msg.single_any) ? msg.single_any : 10"); + UNOPTIMIZABLE_TERNARY("has(msg.single_any) ? msg.single_any : 10"), + MACRO("[1, 2, 3].exists(x, x > 0)"); private final String source; @@ -170,14 +243,10 @@ 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) - .enableCelBlock(false) - .build()) + newCseOptimizer(SubexpressionOptimizerOptions.newBuilder().populateMacroCalls(true).build()) .optimize(ast); assertThat(ast.getExpr()).isEqualTo(optimizedAst.getExpr()); @@ -186,27 +255,57 @@ 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) - .enableCelBlock(true) - .build()) + newCseOptimizer(SubexpressionOptimizerOptions.newBuilder().populateMacroCalls(true).build()) .optimize(ast); assertThat(ast.getExpr()).isEqualTo(optimizedAst.getExpr()); assertThat(CEL_UNPARSER.unparse(optimizedAst)).isEqualTo(testCase.source); } + @Test + public void cse_withComprehensionStructureRetained() throws Exception { + CelAbstractSyntaxTree ast = + cel.compile("['foo'].map(x, [x+x]) + ['foo'].map(x, [x+x, x+x])").getAst(); + CelOptimizer celOptimizer = + newCseOptimizer( + SubexpressionOptimizerOptions.newBuilder().populateMacroCalls(true).build()); + + CelAbstractSyntaxTree optimizedAst = celOptimizer.optimize(ast); + + assertThat(CEL_UNPARSER.unparse(optimizedAst)) + .isEqualTo( + "cel.@block([[\"foo\"]], @index0.map(@it:0:0, [@it:0:0 + @it:0:0]) +" + + " @index0.map(@it:0:0, [@it:0:0 + @it:0:0, @it:0:0 + @it:0:0]))"); + } + + @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") + .getAst(); + CelOptimizer optimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(cel) + .addAstOptimizers( + ConstantFoldingOptimizer.getInstance(), + SubexpressionOptimizer.newInstance( + SubexpressionOptimizerOptions.newBuilder().build())) + .build(); + + CelAbstractSyntaxTree optimizedAst = optimizer.optimize(ast); + + assertThat(CEL_UNPARSER.unparse(optimizedAst)).isEqualTo("6 + x"); + } + @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()), @@ -215,44 +314,29 @@ public void cse_applyConstFoldingAfter() throws Exception { CelAbstractSyntaxTree optimizedAst = optimizer.optimize(ast); - assertThat(optimizedAst.getExpr()) - .isEqualTo( - CelExpr.ofCallExpr( - 1L, - Optional.empty(), - Operator.ADD.getFunction(), - ImmutableList.of( - CelExpr.ofConstantExpr(2L, CelConstant.ofValue(6L)), - CelExpr.ofIdentExpr(3L, "x")))); - assertThat(CEL_UNPARSER.unparse(optimizedAst)).isEqualTo("6 + x"); + assertThat(CEL_UNPARSER.unparse(optimizedAst)) + .isEqualTo("cel.@block([1, 2], @index0 + @index0 + @index1 + @index1 + x)"); } @Test - @TestParameters("{enableCelBlock: false, unparsed: 'cel.bind(@r0, size(x), @r0 + @r0)'}") - @TestParameters("{enableCelBlock: true, unparsed: 'cel.@block([size(x)], @index0 + @index0)'}") - public void cse_applyConstFoldingAfter_nothingToFold(boolean enableCelBlock, String unparsed) - throws Exception { - CelAbstractSyntaxTree ast = CEL.compile("size(x) + size(x)").getAst(); + public void cse_applyConstFoldingAfter_nothingToFold() throws Exception { + CelAbstractSyntaxTree ast = cel.compile("size(x) + size(x)").getAst(); CelOptimizer optimizer = - CelOptimizerFactory.standardCelOptimizerBuilder(CEL) + CelOptimizerFactory.standardCelOptimizerBuilder(cel) .addAstOptimizers( SubexpressionOptimizer.newInstance( - SubexpressionOptimizerOptions.newBuilder() - .populateMacroCalls(true) - .enableCelBlock(enableCelBlock) - .build()), + SubexpressionOptimizerOptions.newBuilder().populateMacroCalls(true).build()), ConstantFoldingOptimizer.getInstance()) .build(); CelAbstractSyntaxTree optimizedAst = optimizer.optimize(ast); - assertThat(CEL_UNPARSER.unparse(optimizedAst)).isEqualTo(unparsed); + assertThat(CEL_UNPARSER.unparse(optimizedAst)) + .isEqualTo("cel.@block([size(x)], @index0 + @index0)"); } @Test - @TestParameters("{enableCelBlock: false}") - @TestParameters("{enableCelBlock: true}") - public void iterationLimitReached_throws(boolean enableCelBlock) throws Exception { + public void iterationLimitReached_throws() throws Exception { StringBuilder largeExprBuilder = new StringBuilder(); int iterationLimit = 100; for (int i = 0; i < iterationLimit; i++) { @@ -261,7 +345,7 @@ public void iterationLimitReached_throws(boolean enableCelBlock) throws Exceptio largeExprBuilder.append("+"); } } - CelAbstractSyntaxTree ast = CEL.compile(largeExprBuilder.toString()).getAst(); + CelAbstractSyntaxTree ast = cel.compile(largeExprBuilder.toString()).getAst(); CelOptimizationException e = assertThrows( @@ -270,7 +354,6 @@ public void iterationLimitReached_throws(boolean enableCelBlock) throws Exceptio newCseOptimizer( SubexpressionOptimizerOptions.newBuilder() .iterationLimit(iterationLimit) - .enableCelBlock(enableCelBlock) .build()) .optimize(ast)); assertThat(e).hasMessageThat().isEqualTo("Optimization failure: Max iteration count reached."); @@ -278,15 +361,12 @@ public void iterationLimitReached_throws(boolean enableCelBlock) throws Exceptio @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) - .enableCelBlock(true) - .build()), + SubexpressionOptimizerOptions.newBuilder().populateMacroCalls(true).build()), ConstantFoldingOptimizer.getInstance()) .build(); @@ -316,7 +396,20 @@ 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).isNotNull(); + } + + @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(); } @@ -376,6 +469,31 @@ public void lazyEval_blockIndexEvaluatedOnlyOnce() throws Exception { assertThat(invocation.get()).isEqualTo(1); } + @Test + @SuppressWarnings({"Immutable", "unchecked"}) // Test only + public void lazyEval_withinComprehension_blockIndexEvaluatedOnlyOnce() throws Exception { + AtomicInteger invocation = new AtomicInteger(); + CelRuntime celRuntime = + CelRuntimeFactory.standardCelRuntimeBuilder() + .addMessageTypes(TestAllTypes.getDescriptor()) + .addFunctionBindings( + CelFunctionBinding.from( + "get_true_overload", + ImmutableList.of(), + arg -> { + invocation.getAndIncrement(); + return true; + })) + .build(); + CelAbstractSyntaxTree ast = + compileUsingInternalFunctions("cel.block([get_true()], [1,2,3].map(x, x < 0 || index0))"); + + List result = (List) celRuntime.createProgram(ast).eval(); + + assertThat(result).containsExactly(true, true, true); + assertThat(invocation.get()).isEqualTo(1); + } + @Test @SuppressWarnings("Immutable") // Test only public void lazyEval_multipleBlockIndices_inResultExpr() throws Exception { @@ -447,9 +565,9 @@ public void lazyEval_nestedComprehension_indexReferencedInNestedScopes() throws // Equivalent of [true, false, true].map(c0, [c0].map(c1, [c0, c1, true])) CelAbstractSyntaxTree ast = compileUsingInternalFunctions( - "cel.block([c0, c1, get_true()], [index2, false, index2].map(c0, [c0].map(c1, [index0," - + " index1, index2]))) == [[[true, true, true]], [[false, false, true]], [[true," - + " true, true]]]"); + "cel.block([true, false, get_true()], [index2, false, index2].map(c0, [c0].map(c1, [c0," + + " c1, index2]))) == [[[true, true, true]], [[false, false, true]], [[true, true," + + " true]]]"); boolean result = (boolean) celRuntime.createProgram(ast).eval(); @@ -481,59 +599,131 @@ public void blockIndex_invalidArgument_throws() { assertThat(e).hasMessageThat().contains("undeclared reference"); } + @Test + public void verifyOptimizedAstCorrectness_twoCelBlocks_throws() throws Exception { + CelAbstractSyntaxTree ast = + compileUsingInternalFunctions("cel.block([1, 2], cel.block([2], 3))"); + + VerifyException e = + assertThrows( + VerifyException.class, () -> SubexpressionOptimizer.verifyOptimizedAstCorrectness(ast)); + assertThat(e) + .hasMessageThat() + .isEqualTo("Expected 1 cel.block function to be present but found 2"); + } + + @Test + public void verifyOptimizedAstCorrectness_celBlockNotAtRoot_throws() throws Exception { + CelAbstractSyntaxTree ast = compileUsingInternalFunctions("1 + cel.block([1, 2], index0)"); + + VerifyException e = + assertThrows( + VerifyException.class, () -> SubexpressionOptimizer.verifyOptimizedAstCorrectness(ast)); + assertThat(e).hasMessageThat().isEqualTo("Expected cel.block to be present at root"); + } + + @Test + public void verifyOptimizedAstCorrectness_blockContainsNoIndexResult_throws() throws Exception { + CelAbstractSyntaxTree ast = compileUsingInternalFunctions("cel.block([1, index0], 2)"); + + VerifyException e = + assertThrows( + VerifyException.class, () -> SubexpressionOptimizer.verifyOptimizedAstCorrectness(ast)); + assertThat(e) + .hasMessageThat() + .isEqualTo("Expected at least one reference of index in cel.block result"); + } + + @Test + @TestParameters("{source: 'cel.block([], index0)'}") + @TestParameters("{source: 'cel.block([1, 2], index2)'}") + public void verifyOptimizedAstCorrectness_indexOutOfBounds_throws(String source) + throws Exception { + CelAbstractSyntaxTree ast = compileUsingInternalFunctions(source); + + VerifyException e = + assertThrows( + VerifyException.class, () -> SubexpressionOptimizer.verifyOptimizedAstCorrectness(ast)); + assertThat(e) + .hasMessageThat() + .contains("Illegal block index found. The index value must be less than"); + } + + @Test + @TestParameters("{source: 'cel.block([index0], index0)'}") + @TestParameters("{source: 'cel.block([1, index1, 2], index2)'}") + @TestParameters("{source: 'cel.block([1, 2, index2], index2)'}") + @TestParameters("{source: 'cel.block([index2, 1, 2], index2)'}") + public void verifyOptimizedAstCorrectness_indexIsNotForwardReferencing_throws(String source) + throws Exception { + CelAbstractSyntaxTree ast = compileUsingInternalFunctions(source); + + VerifyException e = + assertThrows( + VerifyException.class, () -> SubexpressionOptimizer.verifyOptimizedAstCorrectness(ast)); + assertThat(e) + .hasMessageThat() + .contains("Illegal block index found. The index value must be less than"); + } + + @Test + 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"); + } + + @Test + public void block_lazyEvaluationContainsError_cleansUpCycleState() throws Exception { + CelAbstractSyntaxTree ast = + compileUsingInternalFunctions( + "cel.block([1/0 > 0], (index0 && false) || (index0 && true))"); + + CelEvaluationException e = + assertThrows(CelEvaluationException.class, () -> cel.createProgram(ast).eval()); + + assertThat(e).hasMessageThat().contains("/ by zero"); + assertThat(e).hasMessageThat().doesNotContain("Cycle detected"); + } + /** * 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 { - MutableAst mutableAst = MutableAst.newInstance(1000); - CelAbstractSyntaxTree astToModify = CEL_FOR_EVALUATING_BLOCK.compile(expression).getAst(); - while (true) { - CelExpr celExpr = - CelNavigableAst.fromAst(astToModify) - .getRoot() - .allNodes() - .filter(node -> node.getKind().equals(Kind.CALL)) - .map(CelNavigableExpr::expr) - .filter(expr -> expr.call().function().equals("cel.block")) - .findAny() - .orElse(null); - if (celExpr == null) { - break; - } - astToModify = - mutableAst.replaceSubtree( - astToModify, - celExpr.toBuilder() - .setCall(celExpr.call().toBuilder().setFunction("cel.@block").build()) - .build(), - celExpr.id()); - } - - while (true) { - CelExpr celExpr = - CelNavigableAst.fromAst(astToModify) - .getRoot() - .allNodes() - .filter(node -> node.getKind().equals(Kind.IDENT)) - .map(CelNavigableExpr::expr) - .filter(expr -> expr.ident().name().startsWith("index")) - .findAny() - .orElse(null); - if (celExpr == null) { - break; - } - String internalIdentName = "@" + celExpr.ident().name(); - astToModify = - mutableAst.replaceSubtree( - astToModify, - celExpr.toBuilder() - .setIdent(celExpr.ident().toBuilder().setName(internalIdentName).build()) - .build(), - celExpr.id()); + CelAbstractSyntaxTree astToModify = celForEvaluatingBlock.compile(expression).getAst(); + CelMutableAst mutableAst = CelMutableAst.fromCelAst(astToModify); + CelNavigableMutableAst.fromAst(mutableAst) + .getRoot() + .allNodes() + .filter(node -> node.getKind().equals(Kind.CALL)) + .map(CelNavigableMutableExpr::expr) + .filter(expr -> expr.call().function().equals("cel.block")) + .forEach(expr -> expr.call().setFunction("cel.@block")); + + CelNavigableMutableAst.fromAst(mutableAst) + .getRoot() + .allNodes() + .filter(node -> node.getKind().equals(Kind.IDENT)) + .map(CelNavigableMutableExpr::expr) + .filter(expr -> expr.ident().name().startsWith("index")) + .forEach( + indexExpr -> { + String internalIdentName = "@" + indexExpr.ident().name(); + indexExpr.ident().setName(internalIdentName); + }); + + if (parsedOnly) { + return mutableAst.toParsedAst(); } + return celForEvaluatingBlock.check(mutableAst.toParsedAst()).getAst(); + } - return CEL_FOR_EVALUATING_BLOCK.check(astToModify).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 new file mode 100644 index 000000000..9139c7a35 --- /dev/null +++ b/optimizer/src/test/resources/constfold_before_subexpression_unparsed.baseline @@ -0,0 +1,794 @@ +Test case: SIZE_1 +Source: size([1,2]) + size([1,2]) + 1 == 5 +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: true +[BLOCK_RECURSION_DEPTH_1]: true +[BLOCK_RECURSION_DEPTH_2]: true +[BLOCK_RECURSION_DEPTH_3]: true +[BLOCK_RECURSION_DEPTH_4]: true +[BLOCK_RECURSION_DEPTH_5]: true +[BLOCK_RECURSION_DEPTH_6]: true +[BLOCK_RECURSION_DEPTH_7]: true +[BLOCK_RECURSION_DEPTH_8]: true +[BLOCK_RECURSION_DEPTH_9]: true + +Test case: SIZE_2 +Source: 2 + size([1,2]) + size([1,2]) + 1 == 7 +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: true +[BLOCK_RECURSION_DEPTH_1]: true +[BLOCK_RECURSION_DEPTH_2]: true +[BLOCK_RECURSION_DEPTH_3]: true +[BLOCK_RECURSION_DEPTH_4]: true +[BLOCK_RECURSION_DEPTH_5]: true +[BLOCK_RECURSION_DEPTH_6]: true +[BLOCK_RECURSION_DEPTH_7]: true +[BLOCK_RECURSION_DEPTH_8]: true +[BLOCK_RECURSION_DEPTH_9]: true + +Test case: SIZE_3 +Source: size([0]) + size([0]) + size([1,2]) + size([1,2]) == 6 +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: true +[BLOCK_RECURSION_DEPTH_1]: true +[BLOCK_RECURSION_DEPTH_2]: true +[BLOCK_RECURSION_DEPTH_3]: true +[BLOCK_RECURSION_DEPTH_4]: true +[BLOCK_RECURSION_DEPTH_5]: true +[BLOCK_RECURSION_DEPTH_6]: true +[BLOCK_RECURSION_DEPTH_7]: true +[BLOCK_RECURSION_DEPTH_8]: true +[BLOCK_RECURSION_DEPTH_9]: true + +Test case: SIZE_4 +Source: 5 + size([0]) + size([0]) + size([1,2]) + size([1,2]) + size([1,2,3]) + size([1,2,3]) == 17 +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: true +[BLOCK_RECURSION_DEPTH_1]: true +[BLOCK_RECURSION_DEPTH_2]: true +[BLOCK_RECURSION_DEPTH_3]: true +[BLOCK_RECURSION_DEPTH_4]: true +[BLOCK_RECURSION_DEPTH_5]: true +[BLOCK_RECURSION_DEPTH_6]: true +[BLOCK_RECURSION_DEPTH_7]: true +[BLOCK_RECURSION_DEPTH_8]: true +[BLOCK_RECURSION_DEPTH_9]: true + +Test case: TIMESTAMP +Source: timestamp(int(timestamp(1000000000))).getFullYear() + timestamp(int(timestamp(75))).getFullYear() + timestamp(int(timestamp(50))).getFullYear() + timestamp(int(timestamp(1000000000))).getFullYear() + timestamp(int(timestamp(50))).getSeconds() + timestamp(int(timestamp(200))).getFullYear() + timestamp(int(timestamp(200))).getFullYear() + timestamp(int(timestamp(75))).getMinutes() + timestamp(int(timestamp(1000000000))).getFullYear() == 13934 +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: true +[BLOCK_RECURSION_DEPTH_1]: true +[BLOCK_RECURSION_DEPTH_2]: true +[BLOCK_RECURSION_DEPTH_3]: true +[BLOCK_RECURSION_DEPTH_4]: true +[BLOCK_RECURSION_DEPTH_5]: true +[BLOCK_RECURSION_DEPTH_6]: true +[BLOCK_RECURSION_DEPTH_7]: true +[BLOCK_RECURSION_DEPTH_8]: true +[BLOCK_RECURSION_DEPTH_9]: true + +Test case: MAP_INDEX +Source: {"a": 2}["a"] + {"a": 2}["a"] * {"a": 2}["a"] == 6 +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: true +[BLOCK_RECURSION_DEPTH_1]: true +[BLOCK_RECURSION_DEPTH_2]: true +[BLOCK_RECURSION_DEPTH_3]: true +[BLOCK_RECURSION_DEPTH_4]: true +[BLOCK_RECURSION_DEPTH_5]: true +[BLOCK_RECURSION_DEPTH_6]: true +[BLOCK_RECURSION_DEPTH_7]: true +[BLOCK_RECURSION_DEPTH_8]: true +[BLOCK_RECURSION_DEPTH_9]: true + +Test case: NESTED_MAP_CONSTRUCTION +Source: {'a': {'b': 1}, 'c': {'b': 1}, 'd': {'e': {'b': 1}}, 'e': {'e': {'b': 1}}} +=====> +Result: {a={b=1}, c={b=1}, d={e={b=1}}, e={e={b=1}}} +[BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([{"b": 1}, {"e": @index0}], {"a": @index0, "c": @index0, "d": @index1, "e": @index1}) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([{"b": 1}, {"e": @index0}], {"a": @index0, "c": @index0, "d": @index1, "e": @index1}) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([{"e": {"b": 1}}, {"b": 1}], {"a": @index1, "c": @index1, "d": @index0, "e": @index0}) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([{"e": {"b": 1}}, {"b": 1}], {"a": @index1, "c": @index1, "d": @index0, "e": @index0}) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([{"e": {"b": 1}}, {"b": 1}], {"a": @index1, "c": @index1, "d": @index0, "e": @index0}) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([{"e": {"b": 1}}, {"b": 1}], {"a": @index1, "c": @index1, "d": @index0, "e": @index0}) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([{"e": {"b": 1}}, {"b": 1}], {"a": @index1, "c": @index1, "d": @index0, "e": @index0}) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([{"e": {"b": 1}}, {"b": 1}], {"a": @index1, "c": @index1, "d": @index0, "e": @index0}) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([{"e": {"b": 1}}, {"b": 1}], {"a": @index1, "c": @index1, "d": @index0, "e": @index0}) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([{"e": {"b": 1}}, {"b": 1}], {"a": @index1, "c": @index1, "d": @index0, "e": @index0}) + +Test case: NESTED_LIST_CONSTRUCTION +Source: [1, [1,2,3,4], 2, [1,2,3,4], 5, [1,2,3,4], 7, [[1,2], [1,2,3,4]], [1,2]] +=====> +Result: [1, [1, 2, 3, 4], 2, [1, 2, 3, 4], 5, [1, 2, 3, 4], 7, [[1, 2], [1, 2, 3, 4]], [1, 2]] +[BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([[1, 2, 3, 4], [1, 2]], [1, @index0, 2, @index0, 5, @index0, 7, [@index1, @index0], @index1]) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([[1, 2, 3, 4], [1, 2], [@index1, @index0]], [1, @index0, 2, @index0, 5, @index0, 7, @index2, @index1]) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([[1, 2, 3, 4], [1, 2]], [1, @index0, 2, @index0, 5, @index0, 7, [@index1, @index0], @index1]) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([[1, 2, 3, 4], [1, 2]], [1, @index0, 2, @index0, 5, @index0, 7, [@index1, @index0], @index1]) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([[1, 2, 3, 4], [1, 2]], [1, @index0, 2, @index0, 5, @index0, 7, [@index1, @index0], @index1]) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([[1, 2, 3, 4], [1, 2]], [1, @index0, 2, @index0, 5, @index0, 7, [@index1, @index0], @index1]) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([[1, 2, 3, 4], [1, 2]], [1, @index0, 2, @index0, 5, @index0, 7, [@index1, @index0], @index1]) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([[1, 2, 3, 4], [1, 2]], [1, @index0, 2, @index0, 5, @index0, 7, [@index1, @index0], @index1]) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([[1, 2, 3, 4], [1, 2]], [1, @index0, 2, @index0, 5, @index0, 7, [@index1, @index0], @index1]) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([[1, 2, 3, 4], [1, 2]], [1, @index0, 2, @index0, 5, @index0, 7, [@index1, @index0], @index1]) + +Test case: SELECT +Source: msg.single_int64 + msg.single_int64 == 6 +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([msg.single_int64], @index0 + @index0 == 6) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([msg.single_int64, @index0 + @index0], @index1 == 6) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([msg.single_int64], @index0 + @index0 == 6) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([msg.single_int64], @index0 + @index0 == 6) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([msg.single_int64], @index0 + @index0 == 6) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([msg.single_int64], @index0 + @index0 == 6) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([msg.single_int64], @index0 + @index0 == 6) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([msg.single_int64], @index0 + @index0 == 6) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([msg.single_int64], @index0 + @index0 == 6) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([msg.single_int64], @index0 + @index0 == 6) + +Test case: SELECT_NESTED_1 +Source: msg.oneof_type.payload.single_int64 + msg.oneof_type.payload.single_int32 + msg.oneof_type.payload.single_int64 + msg.single_int64 + msg.oneof_type.payload.oneof_type.payload.single_int64 == 31 +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([msg.oneof_type.payload, @index0.single_int64], @index1 + @index0.single_int32 + @index1 + msg.single_int64 + @index0.oneof_type.payload.single_int64 == 31) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, @index1.single_int32, @index2 + @index3, @index4 + @index2, msg.single_int64, @index5 + @index6, @index1.oneof_type, @index8.payload, @index9.single_int64, @index7 + @index10], @index11 == 31) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([msg.oneof_type.payload, @index0.single_int64, @index1 + @index0.single_int32, @index2 + @index1 + msg.single_int64, @index0.oneof_type.payload, @index3 + @index4.single_int64], @index5 == 31) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([msg.oneof_type.payload.single_int64, msg.oneof_type.payload, @index0 + @index1.single_int32 + @index0, @index1.oneof_type.payload.single_int64, @index2 + msg.single_int64 + @index3], @index4 == 31) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([msg.oneof_type.payload.single_int64, msg.oneof_type.payload, @index0 + @index1.single_int32 + @index0 + msg.single_int64, @index2 + @index1.oneof_type.payload.single_int64], @index3 == 31) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([msg.oneof_type.payload.single_int64, msg.oneof_type.payload, @index0 + @index1.single_int32 + @index0 + msg.single_int64 + @index1.oneof_type.payload.single_int64], @index2 == 31) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([msg.oneof_type.payload.single_int64, msg.oneof_type.payload], @index0 + @index1.single_int32 + @index0 + msg.single_int64 + @index1.oneof_type.payload.single_int64 == 31) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([msg.oneof_type.payload.single_int64, msg.oneof_type.payload], @index0 + @index1.single_int32 + @index0 + msg.single_int64 + @index1.oneof_type.payload.single_int64 == 31) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([msg.oneof_type.payload.single_int64, msg.oneof_type.payload], @index0 + @index1.single_int32 + @index0 + msg.single_int64 + @index1.oneof_type.payload.single_int64 == 31) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([msg.oneof_type.payload.single_int64, msg.oneof_type.payload], @index0 + @index1.single_int32 + @index0 + msg.single_int64 + @index1.oneof_type.payload.single_int64 == 31) + +Test case: SELECT_NESTED_2 +Source: true || msg.oneof_type.payload.oneof_type.payload.oneof_type.payload.oneof_type.payload.single_bool || msg.oneof_type.payload.oneof_type.payload.oneof_type.child.child.payload.single_bool +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: true +[BLOCK_RECURSION_DEPTH_1]: true +[BLOCK_RECURSION_DEPTH_2]: true +[BLOCK_RECURSION_DEPTH_3]: true +[BLOCK_RECURSION_DEPTH_4]: true +[BLOCK_RECURSION_DEPTH_5]: true +[BLOCK_RECURSION_DEPTH_6]: true +[BLOCK_RECURSION_DEPTH_7]: true +[BLOCK_RECURSION_DEPTH_8]: true +[BLOCK_RECURSION_DEPTH_9]: true + +Test case: SELECT_NESTED_MESSAGE_MAP_INDEX_1 +Source: msg.oneof_type.payload.map_int32_int64[1] + msg.oneof_type.payload.map_int32_int64[1] + msg.oneof_type.payload.map_int32_int64[1] == 15 +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([msg.oneof_type.payload.map_int32_int64[1]], @index0 + @index0 + @index0 == 15) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([msg.oneof_type, @index0.payload, @index1.map_int32_int64, @index2[1], @index3 + @index3, @index4 + @index3], @index5 == 15) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([msg.oneof_type.payload, @index0.map_int32_int64[1], @index1 + @index1 + @index1], @index2 == 15) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([msg.oneof_type.payload.map_int32_int64, @index0[1]], @index1 + @index1 + @index1 == 15) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([msg.oneof_type.payload.map_int32_int64[1]], @index0 + @index0 + @index0 == 15) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([msg.oneof_type.payload.map_int32_int64[1]], @index0 + @index0 + @index0 == 15) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([msg.oneof_type.payload.map_int32_int64[1]], @index0 + @index0 + @index0 == 15) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([msg.oneof_type.payload.map_int32_int64[1]], @index0 + @index0 + @index0 == 15) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([msg.oneof_type.payload.map_int32_int64[1]], @index0 + @index0 + @index0 == 15) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([msg.oneof_type.payload.map_int32_int64[1]], @index0 + @index0 + @index0 == 15) + +Test case: SELECT_NESTED_MESSAGE_MAP_INDEX_2 +Source: msg.oneof_type.payload.map_int32_int64[0] + msg.oneof_type.payload.map_int32_int64[1] + msg.oneof_type.payload.map_int32_int64[2] == 8 +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([msg.oneof_type.payload.map_int32_int64], @index0[0] + @index0[1] + @index0[2] == 8) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([msg.oneof_type, @index0.payload, @index1.map_int32_int64, @index2[0], @index2[1], @index3 + @index4, @index2[2], @index5 + @index6], @index7 == 8) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([msg.oneof_type.payload, @index0.map_int32_int64, @index1[0] + @index1[1], @index2 + @index1[2]], @index3 == 8) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([msg.oneof_type.payload.map_int32_int64, @index0[0] + @index0[1] + @index0[2]], @index1 == 8) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([msg.oneof_type.payload.map_int32_int64], @index0[0] + @index0[1] + @index0[2] == 8) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([msg.oneof_type.payload.map_int32_int64], @index0[0] + @index0[1] + @index0[2] == 8) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([msg.oneof_type.payload.map_int32_int64], @index0[0] + @index0[1] + @index0[2] == 8) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([msg.oneof_type.payload.map_int32_int64], @index0[0] + @index0[1] + @index0[2] == 8) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([msg.oneof_type.payload.map_int32_int64], @index0[0] + @index0[1] + @index0[2] == 8) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([msg.oneof_type.payload.map_int32_int64], @index0[0] + @index0[1] + @index0[2] == 8) + +Test case: SELECT_NESTED_NO_COMMON_SUBEXPR +Source: msg.oneof_type.payload.oneof_type.payload.oneof_type.payload.oneof_type.payload.single_int64 +=====> +Result: 0 +[BLOCK_COMMON_SUBEXPR_ONLY]: msg.oneof_type.payload.oneof_type.payload.oneof_type.payload.oneof_type.payload.single_int64 +[BLOCK_RECURSION_DEPTH_1]: cel.@block([msg.oneof_type, @index0.payload, @index1.oneof_type, @index2.payload, @index3.oneof_type, @index4.payload, @index5.oneof_type, @index6.payload], @index7.single_int64) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([msg.oneof_type.payload, @index0.oneof_type.payload, @index1.oneof_type.payload, @index2.oneof_type.payload], @index3.single_int64) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([msg.oneof_type.payload.oneof_type, @index0.payload.oneof_type.payload], @index1.oneof_type.payload.single_int64) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([msg.oneof_type.payload.oneof_type.payload, @index0.oneof_type.payload.oneof_type.payload], @index1.single_int64) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([msg.oneof_type.payload.oneof_type.payload.oneof_type], @index0.payload.oneof_type.payload.single_int64) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([msg.oneof_type.payload.oneof_type.payload.oneof_type.payload], @index0.oneof_type.payload.single_int64) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([msg.oneof_type.payload.oneof_type.payload.oneof_type.payload.oneof_type], @index0.payload.single_int64) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([msg.oneof_type.payload.oneof_type.payload.oneof_type.payload.oneof_type.payload], @index0.single_int64) +[BLOCK_RECURSION_DEPTH_9]: msg.oneof_type.payload.oneof_type.payload.oneof_type.payload.oneof_type.payload.single_int64 + +Test case: TERNARY +Source: (msg.single_int64 > 0 ? msg.single_int64 : 0) == 3 +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([msg.single_int64], ((@index0 > 0) ? @index0 : 0) == 3) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([msg.single_int64, @index0 > 0, @index1 ? @index0 : 0], @index2 == 3) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([msg.single_int64, (@index0 > 0) ? @index0 : 0], @index1 == 3) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([msg.single_int64], ((@index0 > 0) ? @index0 : 0) == 3) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([msg.single_int64], ((@index0 > 0) ? @index0 : 0) == 3) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([msg.single_int64], ((@index0 > 0) ? @index0 : 0) == 3) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([msg.single_int64], ((@index0 > 0) ? @index0 : 0) == 3) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([msg.single_int64], ((@index0 > 0) ? @index0 : 0) == 3) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([msg.single_int64], ((@index0 > 0) ? @index0 : 0) == 3) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([msg.single_int64], ((@index0 > 0) ? @index0 : 0) == 3) + +Test case: TERNARY_BIND_RHS_ONLY +Source: false ? false : (msg.single_int64) + ((msg.single_int64 + 1) * 2) == 11 +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([msg.single_int64], @index0 + (@index0 + 1) * 2 == 11) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([msg.single_int64, @index0 + 1, @index1 * 2, @index0 + @index2], @index3 == 11) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([msg.single_int64, (@index0 + 1) * 2], @index0 + @index1 == 11) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([msg.single_int64, @index0 + (@index0 + 1) * 2], @index1 == 11) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([msg.single_int64], @index0 + (@index0 + 1) * 2 == 11) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([msg.single_int64], @index0 + (@index0 + 1) * 2 == 11) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([msg.single_int64], @index0 + (@index0 + 1) * 2 == 11) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([msg.single_int64], @index0 + (@index0 + 1) * 2 == 11) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([msg.single_int64], @index0 + (@index0 + 1) * 2 == 11) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([msg.single_int64], @index0 + (@index0 + 1) * 2 == 11) + +Test case: NESTED_TERNARY +Source: (msg.single_int64 > 0 ? (msg.single_int32 > 0 ? msg.single_int64 + msg.single_int32 : 0) : 0) == 8 +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([msg.single_int64, msg.single_int32], ((@index0 > 0) ? ((@index1 > 0) ? (@index0 + @index1) : 0) : 0) == 8) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([msg.single_int64, msg.single_int32, @index0 > 0, @index1 > 0, @index0 + @index1, @index3 ? @index4 : 0, @index2 ? @index5 : 0], @index6 == 8) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([msg.single_int64, msg.single_int32, (@index1 > 0) ? (@index0 + @index1) : 0, (@index0 > 0) ? @index2 : 0], @index3 == 8) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([msg.single_int64, msg.single_int32, (@index0 > 0) ? ((@index1 > 0) ? (@index0 + @index1) : 0) : 0], @index2 == 8) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([msg.single_int64, msg.single_int32], ((@index0 > 0) ? ((@index1 > 0) ? (@index0 + @index1) : 0) : 0) == 8) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([msg.single_int64, msg.single_int32], ((@index0 > 0) ? ((@index1 > 0) ? (@index0 + @index1) : 0) : 0) == 8) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([msg.single_int64, msg.single_int32], ((@index0 > 0) ? ((@index1 > 0) ? (@index0 + @index1) : 0) : 0) == 8) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([msg.single_int64, msg.single_int32], ((@index0 > 0) ? ((@index1 > 0) ? (@index0 + @index1) : 0) : 0) == 8) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([msg.single_int64, msg.single_int32], ((@index0 > 0) ? ((@index1 > 0) ? (@index0 + @index1) : 0) : 0) == 8) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([msg.single_int64, msg.single_int32], ((@index0 > 0) ? ((@index1 > 0) ? (@index0 + @index1) : 0) : 0) == 8) + +Test case: MULTIPLE_MACROS_1 +Source: size([[1].exists(i, i > 0)]) + size([[1].exists(j, j > 0)]) + size([[2].exists(k, k > 1)]) + size([[2].exists(l, l > 1)]) == 4 +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: true +[BLOCK_RECURSION_DEPTH_1]: true +[BLOCK_RECURSION_DEPTH_2]: true +[BLOCK_RECURSION_DEPTH_3]: true +[BLOCK_RECURSION_DEPTH_4]: true +[BLOCK_RECURSION_DEPTH_5]: true +[BLOCK_RECURSION_DEPTH_6]: true +[BLOCK_RECURSION_DEPTH_7]: true +[BLOCK_RECURSION_DEPTH_8]: true +[BLOCK_RECURSION_DEPTH_9]: true + +Test case: MULTIPLE_MACROS_2 +Source: [[1].exists(i, i > 0)] + [[1].exists(j, j > 0)] + [['a'].exists(k, k == 'a')] + [['a'].exists(l, l == 'a')] == [true, true, true, true] +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: true +[BLOCK_RECURSION_DEPTH_1]: true +[BLOCK_RECURSION_DEPTH_2]: true +[BLOCK_RECURSION_DEPTH_3]: true +[BLOCK_RECURSION_DEPTH_4]: true +[BLOCK_RECURSION_DEPTH_5]: true +[BLOCK_RECURSION_DEPTH_6]: true +[BLOCK_RECURSION_DEPTH_7]: true +[BLOCK_RECURSION_DEPTH_8]: true +[BLOCK_RECURSION_DEPTH_9]: true + +Test case: MULTIPLE_MACROS_3 +Source: [1].exists(i, i > 0) && [1].exists(j, j > 0) && [1].exists(k, k > 1) && [2].exists(l, l > 1) +=====> +Result: false +[BLOCK_COMMON_SUBEXPR_ONLY]: false +[BLOCK_RECURSION_DEPTH_1]: false +[BLOCK_RECURSION_DEPTH_2]: false +[BLOCK_RECURSION_DEPTH_3]: false +[BLOCK_RECURSION_DEPTH_4]: false +[BLOCK_RECURSION_DEPTH_5]: false +[BLOCK_RECURSION_DEPTH_6]: false +[BLOCK_RECURSION_DEPTH_7]: false +[BLOCK_RECURSION_DEPTH_8]: false +[BLOCK_RECURSION_DEPTH_9]: false + +Test case: MULTIPLE_MACROS_COMP_V2_1 +Source: size([[1].exists(i,x,i > 0 && x >= 0)]) + size([[1].exists(j, y, j > 0 && y >= 0)]) + size([[2].exists(k,z,k > 1 && z >= 0)]) + size([[2].exists(l, w, l > 1 && w >= 0)]) == 4 +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: true +[BLOCK_RECURSION_DEPTH_1]: true +[BLOCK_RECURSION_DEPTH_2]: true +[BLOCK_RECURSION_DEPTH_3]: true +[BLOCK_RECURSION_DEPTH_4]: true +[BLOCK_RECURSION_DEPTH_5]: true +[BLOCK_RECURSION_DEPTH_6]: true +[BLOCK_RECURSION_DEPTH_7]: true +[BLOCK_RECURSION_DEPTH_8]: true +[BLOCK_RECURSION_DEPTH_9]: true + +Test case: MULTIPLE_MACROS_COMP_V2_2 +Source: [1].exists(i, x, i > 0 && x > 0) && [1].exists(j, y, j > 0 && y > 0) && [1].exists(k, z, k > 1 && z > 0) && [2].exists(l, w, l > 1 && w > 0) +=====> +Result: false +[BLOCK_COMMON_SUBEXPR_ONLY]: false +[BLOCK_RECURSION_DEPTH_1]: false +[BLOCK_RECURSION_DEPTH_2]: false +[BLOCK_RECURSION_DEPTH_3]: false +[BLOCK_RECURSION_DEPTH_4]: false +[BLOCK_RECURSION_DEPTH_5]: false +[BLOCK_RECURSION_DEPTH_6]: false +[BLOCK_RECURSION_DEPTH_7]: false +[BLOCK_RECURSION_DEPTH_8]: false +[BLOCK_RECURSION_DEPTH_9]: false + +Test case: NESTED_MACROS +Source: [1,2,3].map(i, [1, 2, 3].map(i, i + 1)) == [[2, 3, 4], [2, 3, 4], [2, 3, 4]] +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: true +[BLOCK_RECURSION_DEPTH_1]: true +[BLOCK_RECURSION_DEPTH_2]: true +[BLOCK_RECURSION_DEPTH_3]: true +[BLOCK_RECURSION_DEPTH_4]: true +[BLOCK_RECURSION_DEPTH_5]: true +[BLOCK_RECURSION_DEPTH_6]: true +[BLOCK_RECURSION_DEPTH_7]: true +[BLOCK_RECURSION_DEPTH_8]: true +[BLOCK_RECURSION_DEPTH_9]: true + +Test case: NESTED_MACROS_2 +Source: [1, 2].map(y, [1, 2, 3].filter(x, x == y)) == [[1], [2]] +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: true +[BLOCK_RECURSION_DEPTH_1]: true +[BLOCK_RECURSION_DEPTH_2]: true +[BLOCK_RECURSION_DEPTH_3]: true +[BLOCK_RECURSION_DEPTH_4]: true +[BLOCK_RECURSION_DEPTH_5]: true +[BLOCK_RECURSION_DEPTH_6]: true +[BLOCK_RECURSION_DEPTH_7]: true +[BLOCK_RECURSION_DEPTH_8]: true +[BLOCK_RECURSION_DEPTH_9]: true + +Test case: NESTED_MACROS_3 +Source: [1,2,3].map(i, i * 2) + [1,2,3].map(i, i * 2).map(i, i * 2) +=====> +Result: [2, 4, 6, 4, 8, 12] +[BLOCK_COMMON_SUBEXPR_ONLY]: [2, 4, 6, 4, 8, 12] +[BLOCK_RECURSION_DEPTH_1]: [2, 4, 6, 4, 8, 12] +[BLOCK_RECURSION_DEPTH_2]: [2, 4, 6, 4, 8, 12] +[BLOCK_RECURSION_DEPTH_3]: [2, 4, 6, 4, 8, 12] +[BLOCK_RECURSION_DEPTH_4]: [2, 4, 6, 4, 8, 12] +[BLOCK_RECURSION_DEPTH_5]: [2, 4, 6, 4, 8, 12] +[BLOCK_RECURSION_DEPTH_6]: [2, 4, 6, 4, 8, 12] +[BLOCK_RECURSION_DEPTH_7]: [2, 4, 6, 4, 8, 12] +[BLOCK_RECURSION_DEPTH_8]: [2, 4, 6, 4, 8, 12] +[BLOCK_RECURSION_DEPTH_9]: [2, 4, 6, 4, 8, 12] + +Test case: NESTED_MACROS_COMP_V2_1 +Source: [1,2,3].transformList(i, v, [1, 2, 3].transformList(i, v, i + v + 1)) == [[2, 4, 6], [2, 4, 6], [2, 4, 6]] +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: true +[BLOCK_RECURSION_DEPTH_1]: true +[BLOCK_RECURSION_DEPTH_2]: true +[BLOCK_RECURSION_DEPTH_3]: true +[BLOCK_RECURSION_DEPTH_4]: true +[BLOCK_RECURSION_DEPTH_5]: true +[BLOCK_RECURSION_DEPTH_6]: true +[BLOCK_RECURSION_DEPTH_7]: true +[BLOCK_RECURSION_DEPTH_8]: true +[BLOCK_RECURSION_DEPTH_9]: true + +Test case: NESTED_MACROS_COMP_V2_2 +Source: [1, 2].transformList(i, y, [1, 2, 3].filter(x, x == y && i < y)) == [[1], [2]] +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: true +[BLOCK_RECURSION_DEPTH_1]: true +[BLOCK_RECURSION_DEPTH_2]: true +[BLOCK_RECURSION_DEPTH_3]: true +[BLOCK_RECURSION_DEPTH_4]: true +[BLOCK_RECURSION_DEPTH_5]: true +[BLOCK_RECURSION_DEPTH_6]: true +[BLOCK_RECURSION_DEPTH_7]: true +[BLOCK_RECURSION_DEPTH_8]: true +[BLOCK_RECURSION_DEPTH_9]: true + +Test case: ADJACENT_NESTED_MACROS +Source: [1,2,3].map(i, [1, 2, 3].map(i, i + 1)) == [1,2,3].map(j, [1, 2, 3].map(j, j + 1)) +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: true +[BLOCK_RECURSION_DEPTH_1]: true +[BLOCK_RECURSION_DEPTH_2]: true +[BLOCK_RECURSION_DEPTH_3]: true +[BLOCK_RECURSION_DEPTH_4]: true +[BLOCK_RECURSION_DEPTH_5]: true +[BLOCK_RECURSION_DEPTH_6]: true +[BLOCK_RECURSION_DEPTH_7]: true +[BLOCK_RECURSION_DEPTH_8]: true +[BLOCK_RECURSION_DEPTH_9]: true + +Test case: ADJACENT_NESTED_MACROS_COMP_V2 +Source: [1,2,3].transformMap(i, x, [1, 2, 3].transformMap(i, x, i + x + 1)) == [1,2,3].transformMap(j, y, [1, 2, 3].transformMap(j, y, j + y + 1)) +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: true +[BLOCK_RECURSION_DEPTH_1]: true +[BLOCK_RECURSION_DEPTH_2]: true +[BLOCK_RECURSION_DEPTH_3]: true +[BLOCK_RECURSION_DEPTH_4]: true +[BLOCK_RECURSION_DEPTH_5]: true +[BLOCK_RECURSION_DEPTH_6]: true +[BLOCK_RECURSION_DEPTH_7]: true +[BLOCK_RECURSION_DEPTH_8]: true +[BLOCK_RECURSION_DEPTH_9]: true + +Test case: INCLUSION_LIST +Source: 1 in [1,2,3] && 2 in [1,2,3] && 3 in [3, [1,2,3]] && 1 in [1,2,3] +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: true +[BLOCK_RECURSION_DEPTH_1]: true +[BLOCK_RECURSION_DEPTH_2]: true +[BLOCK_RECURSION_DEPTH_3]: true +[BLOCK_RECURSION_DEPTH_4]: true +[BLOCK_RECURSION_DEPTH_5]: true +[BLOCK_RECURSION_DEPTH_6]: true +[BLOCK_RECURSION_DEPTH_7]: true +[BLOCK_RECURSION_DEPTH_8]: true +[BLOCK_RECURSION_DEPTH_9]: true + +Test case: INCLUSION_MAP +Source: 2 in {'a': 1, 2: {true: false}, 3: {true: false}} +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: true +[BLOCK_RECURSION_DEPTH_1]: true +[BLOCK_RECURSION_DEPTH_2]: true +[BLOCK_RECURSION_DEPTH_3]: true +[BLOCK_RECURSION_DEPTH_4]: true +[BLOCK_RECURSION_DEPTH_5]: true +[BLOCK_RECURSION_DEPTH_6]: true +[BLOCK_RECURSION_DEPTH_7]: true +[BLOCK_RECURSION_DEPTH_8]: true +[BLOCK_RECURSION_DEPTH_9]: true + +Test case: MACRO_ITER_VAR_NOT_REFERENCED +Source: [1,2].map(i, [1, 2].map(i, [3,4])) == [[[3, 4], [3, 4]], [[3, 4], [3, 4]]] +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: true +[BLOCK_RECURSION_DEPTH_1]: true +[BLOCK_RECURSION_DEPTH_2]: true +[BLOCK_RECURSION_DEPTH_3]: true +[BLOCK_RECURSION_DEPTH_4]: true +[BLOCK_RECURSION_DEPTH_5]: true +[BLOCK_RECURSION_DEPTH_6]: true +[BLOCK_RECURSION_DEPTH_7]: true +[BLOCK_RECURSION_DEPTH_8]: true +[BLOCK_RECURSION_DEPTH_9]: true + +Test case: MACRO_ITER_VAR2_NOT_REFERENCED +Source: [1,2].transformList(i, v, [1, 2].transformList(i, v, [3,4])) == [[[3, 4], [3, 4]], [[3, 4], [3, 4]]] +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: true +[BLOCK_RECURSION_DEPTH_1]: true +[BLOCK_RECURSION_DEPTH_2]: true +[BLOCK_RECURSION_DEPTH_3]: true +[BLOCK_RECURSION_DEPTH_4]: true +[BLOCK_RECURSION_DEPTH_5]: true +[BLOCK_RECURSION_DEPTH_6]: true +[BLOCK_RECURSION_DEPTH_7]: true +[BLOCK_RECURSION_DEPTH_8]: true +[BLOCK_RECURSION_DEPTH_9]: true + +Test case: MACRO_SHADOWED_VARIABLE +Source: [x - 1 > 3 ? x - 1 : 5].exists(x, x - 1 > 3) || x - 1 > 3 +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([x - 1, @index0 > 3], [@index1 ? @index0 : 5].exists(@it:0:0, @it:0:0 - 1 > 3) || @index1) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([x - 1, @index0 > 3, @index1 ? @index0 : 5, [@index2]], @index3.exists(@it:0:0, @it:0:0 - 1 > 3) || @index1) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([x - 1 > 3, @index0 ? (x - 1) : 5, [@index1]], @index2.exists(@it:0:0, @it:0:0 - 1 > 3) || @index0) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([x - 1 > 3, [@index0 ? (x - 1) : 5]], @index1.exists(@it:0:0, @it:0:0 - 1 > 3) || @index0) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([x - 1 > 3], [@index0 ? (x - 1) : 5].exists(@it:0:0, @it:0:0 - 1 > 3) || @index0) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([x - 1 > 3], [@index0 ? (x - 1) : 5].exists(@it:0:0, @it:0:0 - 1 > 3) || @index0) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([x - 1 > 3], [@index0 ? (x - 1) : 5].exists(@it:0:0, @it:0:0 - 1 > 3) || @index0) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([x - 1 > 3], [@index0 ? (x - 1) : 5].exists(@it:0:0, @it:0:0 - 1 > 3) || @index0) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([x - 1 > 3], [@index0 ? (x - 1) : 5].exists(@it:0:0, @it:0:0 - 1 > 3) || @index0) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([x - 1 > 3], [@index0 ? (x - 1) : 5].exists(@it:0:0, @it:0:0 - 1 > 3) || @index0) + +Test case: MACRO_SHADOWED_VARIABLE_2 +Source: ["foo", "bar"].map(x, [x + x, x + x]).map(x, [x + x, x + x]) +=====> +Result: [[[foofoo, foofoo, foofoo, foofoo], [foofoo, foofoo, foofoo, foofoo]], [[barbar, barbar, barbar, barbar], [barbar, barbar, barbar, barbar]]] +[BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([["foofoo", "foofoo", "foofoo", "foofoo"], ["barbar", "barbar", "barbar", "barbar"]], [[@index0, @index0], [@index1, @index1]]) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([["foofoo", "foofoo", "foofoo", "foofoo"], ["barbar", "barbar", "barbar", "barbar"], [@index0, @index0], [@index1, @index1]], [@index2, @index3]) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([["foofoo", "foofoo", "foofoo", "foofoo"], ["barbar", "barbar", "barbar", "barbar"]], [[@index0, @index0], [@index1, @index1]]) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([["foofoo", "foofoo", "foofoo", "foofoo"], ["barbar", "barbar", "barbar", "barbar"]], [[@index0, @index0], [@index1, @index1]]) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([["foofoo", "foofoo", "foofoo", "foofoo"], ["barbar", "barbar", "barbar", "barbar"]], [[@index0, @index0], [@index1, @index1]]) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([["foofoo", "foofoo", "foofoo", "foofoo"], ["barbar", "barbar", "barbar", "barbar"]], [[@index0, @index0], [@index1, @index1]]) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([["foofoo", "foofoo", "foofoo", "foofoo"], ["barbar", "barbar", "barbar", "barbar"]], [[@index0, @index0], [@index1, @index1]]) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([["foofoo", "foofoo", "foofoo", "foofoo"], ["barbar", "barbar", "barbar", "barbar"]], [[@index0, @index0], [@index1, @index1]]) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([["foofoo", "foofoo", "foofoo", "foofoo"], ["barbar", "barbar", "barbar", "barbar"]], [[@index0, @index0], [@index1, @index1]]) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([["foofoo", "foofoo", "foofoo", "foofoo"], ["barbar", "barbar", "barbar", "barbar"]], [[@index0, @index0], [@index1, @index1]]) + +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: 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) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([x - y - 1 > 3, @index0 ? (x - y - 1) : 5, [@index1]], @index2.exists(@it:0:0, @it2:0:0, @it:0:0 - @it2:0:0 - 1 > 3) || @index0) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([x - y - 1 > 3, [@index0 ? (x - y - 1) : 5]], @index1.exists(@it:0:0, @it2:0:0, @it:0:0 - @it2:0:0 - 1 > 3) || @index0) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([x - y - 1 > 3], [@index0 ? (x - y - 1) : 5].exists(@it:0:0, @it2:0:0, @it:0:0 - @it2:0:0 - 1 > 3) || @index0) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([x - y - 1 > 3], [@index0 ? (x - y - 1) : 5].exists(@it:0:0, @it2:0:0, @it:0:0 - @it2:0:0 - 1 > 3) || @index0) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([x - y - 1 > 3], [@index0 ? (x - y - 1) : 5].exists(@it:0:0, @it2:0:0, @it:0:0 - @it2:0:0 - 1 > 3) || @index0) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([x - y - 1 > 3], [@index0 ? (x - y - 1) : 5].exists(@it:0:0, @it2:0:0, @it:0:0 - @it2:0:0 - 1 > 3) || @index0) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([x - y - 1 > 3], [@index0 ? (x - y - 1) : 5].exists(@it:0:0, @it2:0:0, @it:0:0 - @it2:0:0 - 1 > 3) || @index0) + +Test case: MACRO_SHADOWED_VARIABLE_COMP_V2_2 +Source: ["foo", "bar"].transformMap(x, y, [x + x, y + y]).transformMap(x, y, [x + x, y + y]) +=====> +Result: {0=[0, [0, foofoo, 0, foofoo]], 1=[2, [2, barbar, 2, barbar]]} +[BLOCK_COMMON_SUBEXPR_ONLY]: {0: [0, [0, "foofoo", 0, "foofoo"]], 1: [2, [2, "barbar", 2, "barbar"]]} +[BLOCK_RECURSION_DEPTH_1]: cel.@block([[0, "foofoo", 0, "foofoo"], [0, @index0], [2, "barbar", 2, "barbar"], [2, @index2]], {0: @index1, 1: @index3}) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([[0, [0, "foofoo", 0, "foofoo"]], [2, [2, "barbar", 2, "barbar"]]], {0: @index0, 1: @index1}) +[BLOCK_RECURSION_DEPTH_3]: {0: [0, [0, "foofoo", 0, "foofoo"]], 1: [2, [2, "barbar", 2, "barbar"]]} +[BLOCK_RECURSION_DEPTH_4]: {0: [0, [0, "foofoo", 0, "foofoo"]], 1: [2, [2, "barbar", 2, "barbar"]]} +[BLOCK_RECURSION_DEPTH_5]: {0: [0, [0, "foofoo", 0, "foofoo"]], 1: [2, [2, "barbar", 2, "barbar"]]} +[BLOCK_RECURSION_DEPTH_6]: {0: [0, [0, "foofoo", 0, "foofoo"]], 1: [2, [2, "barbar", 2, "barbar"]]} +[BLOCK_RECURSION_DEPTH_7]: {0: [0, [0, "foofoo", 0, "foofoo"]], 1: [2, [2, "barbar", 2, "barbar"]]} +[BLOCK_RECURSION_DEPTH_8]: {0: [0, [0, "foofoo", 0, "foofoo"]], 1: [2, [2, "barbar", 2, "barbar"]]} +[BLOCK_RECURSION_DEPTH_9]: {0: [0, [0, "foofoo", 0, "foofoo"]], 1: [2, [2, "barbar", 2, "barbar"]]} + +Test case: PRESENCE_TEST +Source: has({'a': true}.a) && {'a':true}['a'] +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: true +[BLOCK_RECURSION_DEPTH_1]: true +[BLOCK_RECURSION_DEPTH_2]: true +[BLOCK_RECURSION_DEPTH_3]: true +[BLOCK_RECURSION_DEPTH_4]: true +[BLOCK_RECURSION_DEPTH_5]: true +[BLOCK_RECURSION_DEPTH_6]: true +[BLOCK_RECURSION_DEPTH_7]: true +[BLOCK_RECURSION_DEPTH_8]: true +[BLOCK_RECURSION_DEPTH_9]: true + +Test case: PRESENCE_TEST_2 +Source: has({'a': true}.a) && has({'a': true}.a) +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: true +[BLOCK_RECURSION_DEPTH_1]: true +[BLOCK_RECURSION_DEPTH_2]: true +[BLOCK_RECURSION_DEPTH_3]: true +[BLOCK_RECURSION_DEPTH_4]: true +[BLOCK_RECURSION_DEPTH_5]: true +[BLOCK_RECURSION_DEPTH_6]: true +[BLOCK_RECURSION_DEPTH_7]: true +[BLOCK_RECURSION_DEPTH_8]: true +[BLOCK_RECURSION_DEPTH_9]: true + +Test case: PRESENCE_TEST_WITH_TERNARY +Source: (has(msg.oneof_type.payload) ? msg.oneof_type.payload.single_int64 : 0) == 10 +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([msg.oneof_type], (has(@index0.payload) ? @index0.payload.single_int64 : 0) == 10) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([msg.oneof_type, has(@index0.payload), @index0.payload, @index2.single_int64, @index1 ? @index3 : 0], @index4 == 10) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([msg.oneof_type, @index0.payload.single_int64, has(@index0.payload) ? @index1 : 0], @index2 == 10) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([msg.oneof_type, has(@index0.payload) ? @index0.payload.single_int64 : 0], @index1 == 10) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([msg.oneof_type], (has(@index0.payload) ? @index0.payload.single_int64 : 0) == 10) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([msg.oneof_type], (has(@index0.payload) ? @index0.payload.single_int64 : 0) == 10) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([msg.oneof_type], (has(@index0.payload) ? @index0.payload.single_int64 : 0) == 10) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([msg.oneof_type], (has(@index0.payload) ? @index0.payload.single_int64 : 0) == 10) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([msg.oneof_type], (has(@index0.payload) ? @index0.payload.single_int64 : 0) == 10) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([msg.oneof_type], (has(@index0.payload) ? @index0.payload.single_int64 : 0) == 10) + +Test case: PRESENCE_TEST_WITH_TERNARY_2 +Source: (has(msg.oneof_type.payload) ? msg.oneof_type.payload.single_int64 : msg.oneof_type.payload.single_int64 * 0) == 10 +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([msg.oneof_type, @index0.payload.single_int64], (has(@index0.payload) ? @index1 : (@index1 * 0)) == 10) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, has(@index0.payload), @index2 * 0, @index3 ? @index2 : @index4], @index5 == 10) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([msg.oneof_type.payload, @index0.single_int64, has(msg.oneof_type.payload), @index2 ? @index1 : (@index1 * 0)], @index3 == 10) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([msg.oneof_type.payload.single_int64, has(msg.oneof_type.payload) ? @index0 : (@index0 * 0)], @index1 == 10) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([msg.oneof_type.payload.single_int64], (has(msg.oneof_type.payload) ? @index0 : (@index0 * 0)) == 10) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([msg.oneof_type.payload.single_int64], (has(msg.oneof_type.payload) ? @index0 : (@index0 * 0)) == 10) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([msg.oneof_type.payload.single_int64], (has(msg.oneof_type.payload) ? @index0 : (@index0 * 0)) == 10) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([msg.oneof_type.payload.single_int64], (has(msg.oneof_type.payload) ? @index0 : (@index0 * 0)) == 10) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([msg.oneof_type.payload.single_int64], (has(msg.oneof_type.payload) ? @index0 : (@index0 * 0)) == 10) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([msg.oneof_type.payload.single_int64], (has(msg.oneof_type.payload) ? @index0 : (@index0 * 0)) == 10) + +Test case: PRESENCE_TEST_WITH_TERNARY_3 +Source: (has(msg.oneof_type.payload.single_int64) ? msg.oneof_type.payload.single_int64 : msg.oneof_type.payload.single_int64 * 0) == 10 +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([msg.oneof_type.payload, @index0.single_int64], (has(@index0.single_int64) ? @index1 : (@index1 * 0)) == 10) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, has(@index1.single_int64), @index2 * 0, @index3 ? @index2 : @index4], @index5 == 10) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([msg.oneof_type.payload, @index0.single_int64, has(@index0.single_int64) ? @index1 : (@index1 * 0)], @index2 == 10) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([msg.oneof_type.payload.single_int64, has(msg.oneof_type.payload.single_int64)], (@index1 ? @index0 : (@index0 * 0)) == 10) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([msg.oneof_type.payload.single_int64, has(msg.oneof_type.payload.single_int64) ? @index0 : (@index0 * 0)], @index1 == 10) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([msg.oneof_type.payload.single_int64], (has(msg.oneof_type.payload.single_int64) ? @index0 : (@index0 * 0)) == 10) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([msg.oneof_type.payload.single_int64], (has(msg.oneof_type.payload.single_int64) ? @index0 : (@index0 * 0)) == 10) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([msg.oneof_type.payload.single_int64], (has(msg.oneof_type.payload.single_int64) ? @index0 : (@index0 * 0)) == 10) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([msg.oneof_type.payload.single_int64], (has(msg.oneof_type.payload.single_int64) ? @index0 : (@index0 * 0)) == 10) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([msg.oneof_type.payload.single_int64], (has(msg.oneof_type.payload.single_int64) ? @index0 : (@index0 * 0)) == 10) + +Test case: PRESENCE_TEST_WITH_TERNARY_NESTED +Source: (has(msg.oneof_type) && has(msg.oneof_type.payload) && has(msg.oneof_type.payload.single_int64)) ? ((has(msg.oneof_type.payload.map_string_string) && has(msg.oneof_type.payload.map_string_string.key)) ? msg.oneof_type.payload.map_string_string.key == 'A' : false) : false +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([msg.oneof_type, @index0.payload, @index1.map_string_string], (has(msg.oneof_type) && has(@index0.payload) && has(@index1.single_int64)) ? ((has(@index1.map_string_string) && has(@index2.key)) ? (@index2.key == "A") : false) : false) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([msg.oneof_type, @index0.payload, @index1.map_string_string, has(msg.oneof_type), has(@index0.payload), @index3 && @index4, has(@index1.single_int64), @index5 && @index6, has(@index1.map_string_string), has(@index2.key), @index8 && @index9, @index2.key, @index11 == "A", @index10 ? @index12 : false], @index7 ? @index13 : false) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([msg.oneof_type.payload, @index0.map_string_string, has(msg.oneof_type.payload), has(msg.oneof_type) && @index2, @index3 && has(@index0.single_int64), has(@index0.map_string_string) && has(@index1.key), @index1.key == "A"], @index4 ? (@index5 ? @index6 : false) : false) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([msg.oneof_type.payload.map_string_string, msg.oneof_type.payload, has(msg.oneof_type) && has(msg.oneof_type.payload), (has(@index1.map_string_string) && has(@index0.key)) ? (@index0.key == "A") : false], (@index2 && has(@index1.single_int64)) ? @index3 : false) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([msg.oneof_type.payload.map_string_string, msg.oneof_type.payload, has(msg.oneof_type) && has(msg.oneof_type.payload) && has(@index1.single_int64)], @index2 ? ((has(@index1.map_string_string) && has(@index0.key)) ? (@index0.key == "A") : false) : false) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([msg.oneof_type.payload.map_string_string, msg.oneof_type.payload], (has(msg.oneof_type) && has(msg.oneof_type.payload) && has(@index1.single_int64)) ? ((has(@index1.map_string_string) && has(@index0.key)) ? (@index0.key == "A") : false) : false) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([msg.oneof_type.payload.map_string_string, msg.oneof_type.payload], (has(msg.oneof_type) && has(msg.oneof_type.payload) && has(@index1.single_int64)) ? ((has(@index1.map_string_string) && has(@index0.key)) ? (@index0.key == "A") : false) : false) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([msg.oneof_type.payload.map_string_string, msg.oneof_type.payload], (has(msg.oneof_type) && has(msg.oneof_type.payload) && has(@index1.single_int64)) ? ((has(@index1.map_string_string) && has(@index0.key)) ? (@index0.key == "A") : false) : false) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([msg.oneof_type.payload.map_string_string, msg.oneof_type.payload], (has(msg.oneof_type) && has(msg.oneof_type.payload) && has(@index1.single_int64)) ? ((has(@index1.map_string_string) && has(@index0.key)) ? (@index0.key == "A") : false) : false) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([msg.oneof_type.payload.map_string_string, msg.oneof_type.payload], (has(msg.oneof_type) && has(msg.oneof_type.payload) && has(@index1.single_int64)) ? ((has(@index1.map_string_string) && has(@index0.key)) ? (@index0.key == "A") : false) : false) + +Test case: OPTIONAL_LIST +Source: [10, ?optional.none(), [?optional.none(), ?opt_x], [?optional.none(), ?opt_x]] == [10, [5], [5]] +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([[?opt_x], [5]], [10, @index0, @index0] == [10, @index1, @index1]) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([[?opt_x], [5], [10, @index0, @index0], [10, @index1, @index1]], @index2 == @index3) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([[?opt_x], [5]], [10, @index0, @index0] == [10, @index1, @index1]) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([[?opt_x], [5]], [10, @index0, @index0] == [10, @index1, @index1]) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([[?opt_x], [5]], [10, @index0, @index0] == [10, @index1, @index1]) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([[?opt_x], [5]], [10, @index0, @index0] == [10, @index1, @index1]) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([[?opt_x], [5]], [10, @index0, @index0] == [10, @index1, @index1]) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([[?opt_x], [5]], [10, @index0, @index0] == [10, @index1, @index1]) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([[?opt_x], [5]], [10, @index0, @index0] == [10, @index1, @index1]) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([[?opt_x], [5]], [10, @index0, @index0] == [10, @index1, @index1]) + +Test case: OPTIONAL_MAP +Source: {?'hello': optional.of('hello')}['hello'] + {?'hello': optional.of('hello')}['hello'] == 'hellohello' +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: true +[BLOCK_RECURSION_DEPTH_1]: true +[BLOCK_RECURSION_DEPTH_2]: true +[BLOCK_RECURSION_DEPTH_3]: true +[BLOCK_RECURSION_DEPTH_4]: true +[BLOCK_RECURSION_DEPTH_5]: true +[BLOCK_RECURSION_DEPTH_6]: true +[BLOCK_RECURSION_DEPTH_7]: true +[BLOCK_RECURSION_DEPTH_8]: true +[BLOCK_RECURSION_DEPTH_9]: true + +Test case: OPTIONAL_MAP_CHAINED +Source: {?'key': optional.of('test')}[?'bogus'].or({'key': 'test'}[?'bogus']).orValue({'key': 'test'}['key']) == 'test' +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: true +[BLOCK_RECURSION_DEPTH_1]: true +[BLOCK_RECURSION_DEPTH_2]: true +[BLOCK_RECURSION_DEPTH_3]: true +[BLOCK_RECURSION_DEPTH_4]: true +[BLOCK_RECURSION_DEPTH_5]: true +[BLOCK_RECURSION_DEPTH_6]: true +[BLOCK_RECURSION_DEPTH_7]: true +[BLOCK_RECURSION_DEPTH_8]: true +[BLOCK_RECURSION_DEPTH_9]: true + +Test case: OPTIONAL_MESSAGE +Source: TestAllTypes{?single_int64: optional.ofNonZeroValue(1), ?single_int32: optional.of(4)}.single_int32 + TestAllTypes{?single_int64: optional.ofNonZeroValue(1), ?single_int32: optional.of(4)}.single_int64 == 5 +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: true +[BLOCK_RECURSION_DEPTH_1]: true +[BLOCK_RECURSION_DEPTH_2]: true +[BLOCK_RECURSION_DEPTH_3]: true +[BLOCK_RECURSION_DEPTH_4]: true +[BLOCK_RECURSION_DEPTH_5]: true +[BLOCK_RECURSION_DEPTH_6]: true +[BLOCK_RECURSION_DEPTH_7]: true +[BLOCK_RECURSION_DEPTH_8]: true +[BLOCK_RECURSION_DEPTH_9]: true + +Test case: CALL +Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('h' + 'e' + 'l' + 'l' + 'o') +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: true +[BLOCK_RECURSION_DEPTH_1]: true +[BLOCK_RECURSION_DEPTH_2]: true +[BLOCK_RECURSION_DEPTH_3]: true +[BLOCK_RECURSION_DEPTH_4]: true +[BLOCK_RECURSION_DEPTH_5]: true +[BLOCK_RECURSION_DEPTH_6]: true +[BLOCK_RECURSION_DEPTH_7]: true +[BLOCK_RECURSION_DEPTH_8]: true +[BLOCK_RECURSION_DEPTH_9]: true + +Test case: CALL_ARGUMENT_NESTED_NO_COMMON_SUBEXPR +Source: 'hello world'.matches('h' + 'e' + 'l' + 'l' + 'o') +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: true +[BLOCK_RECURSION_DEPTH_1]: true +[BLOCK_RECURSION_DEPTH_2]: true +[BLOCK_RECURSION_DEPTH_3]: true +[BLOCK_RECURSION_DEPTH_4]: true +[BLOCK_RECURSION_DEPTH_5]: true +[BLOCK_RECURSION_DEPTH_6]: true +[BLOCK_RECURSION_DEPTH_7]: true +[BLOCK_RECURSION_DEPTH_8]: true +[BLOCK_RECURSION_DEPTH_9]: true + +Test case: CALL_TARGET_NESTED_NO_COMMON_SUBEXPR +Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('hello') +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: true +[BLOCK_RECURSION_DEPTH_1]: true +[BLOCK_RECURSION_DEPTH_2]: true +[BLOCK_RECURSION_DEPTH_3]: true +[BLOCK_RECURSION_DEPTH_4]: true +[BLOCK_RECURSION_DEPTH_5]: true +[BLOCK_RECURSION_DEPTH_6]: true +[BLOCK_RECURSION_DEPTH_7]: true +[BLOCK_RECURSION_DEPTH_8]: true +[BLOCK_RECURSION_DEPTH_9]: true + +Test case: CALL_BOTH_ARGUMENT_TARGET_NESTED_NO_COMMON_SUBEXPR +Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('w' + 'o' + 'r' + 'l' + 'd') +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: true +[BLOCK_RECURSION_DEPTH_1]: true +[BLOCK_RECURSION_DEPTH_2]: true +[BLOCK_RECURSION_DEPTH_3]: true +[BLOCK_RECURSION_DEPTH_4]: true +[BLOCK_RECURSION_DEPTH_5]: true +[BLOCK_RECURSION_DEPTH_6]: true +[BLOCK_RECURSION_DEPTH_7]: true +[BLOCK_RECURSION_DEPTH_8]: true +[BLOCK_RECURSION_DEPTH_9]: true + +Test case: CUSTOM_FUNCTION_INELIMINABLE +Source: non_pure_custom_func(msg.oneof_type.payload.single_int64) + non_pure_custom_func(msg.oneof_type.payload.single_int32) + non_pure_custom_func(msg.oneof_type.payload.single_int64) + non_pure_custom_func(msg.single_int64) +=====> +Result: 31 +[BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([msg.oneof_type.payload, @index0.single_int64], non_pure_custom_func(@index1) + non_pure_custom_func(@index0.single_int32) + non_pure_custom_func(@index1) + non_pure_custom_func(msg.single_int64)) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64], non_pure_custom_func(@index2) + non_pure_custom_func(@index1.single_int32) + non_pure_custom_func(@index2) + non_pure_custom_func(msg.single_int64)) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([msg.oneof_type.payload, @index0.single_int64], non_pure_custom_func(@index1) + non_pure_custom_func(@index0.single_int32) + non_pure_custom_func(@index1) + non_pure_custom_func(msg.single_int64)) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([msg.oneof_type.payload.single_int64], non_pure_custom_func(@index0) + non_pure_custom_func(msg.oneof_type.payload.single_int32) + non_pure_custom_func(@index0) + non_pure_custom_func(msg.single_int64)) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([msg.oneof_type.payload.single_int64], non_pure_custom_func(@index0) + non_pure_custom_func(msg.oneof_type.payload.single_int32) + non_pure_custom_func(@index0) + non_pure_custom_func(msg.single_int64)) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([msg.oneof_type.payload.single_int64], non_pure_custom_func(@index0) + non_pure_custom_func(msg.oneof_type.payload.single_int32) + non_pure_custom_func(@index0) + non_pure_custom_func(msg.single_int64)) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([msg.oneof_type.payload.single_int64], non_pure_custom_func(@index0) + non_pure_custom_func(msg.oneof_type.payload.single_int32) + non_pure_custom_func(@index0) + non_pure_custom_func(msg.single_int64)) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([msg.oneof_type.payload.single_int64], non_pure_custom_func(@index0) + non_pure_custom_func(msg.oneof_type.payload.single_int32) + non_pure_custom_func(@index0) + non_pure_custom_func(msg.single_int64)) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([msg.oneof_type.payload.single_int64], non_pure_custom_func(@index0) + non_pure_custom_func(msg.oneof_type.payload.single_int32) + non_pure_custom_func(@index0) + non_pure_custom_func(msg.single_int64)) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([msg.oneof_type.payload.single_int64], non_pure_custom_func(@index0) + non_pure_custom_func(msg.oneof_type.payload.single_int32) + non_pure_custom_func(@index0) + non_pure_custom_func(msg.single_int64)) + +Test case: CUSTOM_FUNCTION_ELIMINABLE +Source: pure_custom_func(msg.oneof_type.payload.single_int64) + pure_custom_func(msg.oneof_type.payload.single_int32) + pure_custom_func(msg.oneof_type.payload.single_int64) + pure_custom_func(msg.single_int64) +=====> +Result: 31 +[BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([msg.oneof_type.payload, pure_custom_func(@index0.single_int64)], @index1 + pure_custom_func(@index0.single_int32) + @index1 + pure_custom_func(msg.single_int64)) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, pure_custom_func(@index2), @index1.single_int32, pure_custom_func(@index4), @index3 + @index5, @index6 + @index3, msg.single_int64, pure_custom_func(@index8)], @index7 + @index9) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([msg.oneof_type.payload, pure_custom_func(@index0.single_int64), pure_custom_func(@index0.single_int32), @index1 + @index2 + @index1, pure_custom_func(msg.single_int64)], @index3 + @index4) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([msg.oneof_type.payload.single_int64, pure_custom_func(@index0), msg.oneof_type.payload.single_int32, @index1 + pure_custom_func(@index2) + @index1], @index3 + pure_custom_func(msg.single_int64)) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([pure_custom_func(msg.oneof_type.payload.single_int64), pure_custom_func(msg.oneof_type.payload.single_int32)], @index0 + @index1 + @index0 + pure_custom_func(msg.single_int64)) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([pure_custom_func(msg.oneof_type.payload.single_int64), @index0 + pure_custom_func(msg.oneof_type.payload.single_int32)], @index1 + @index0 + pure_custom_func(msg.single_int64)) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([pure_custom_func(msg.oneof_type.payload.single_int64), @index0 + pure_custom_func(msg.oneof_type.payload.single_int32) + @index0], @index1 + pure_custom_func(msg.single_int64)) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([pure_custom_func(msg.oneof_type.payload.single_int64)], @index0 + pure_custom_func(msg.oneof_type.payload.single_int32) + @index0 + pure_custom_func(msg.single_int64)) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([pure_custom_func(msg.oneof_type.payload.single_int64)], @index0 + pure_custom_func(msg.oneof_type.payload.single_int32) + @index0 + pure_custom_func(msg.single_int64)) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([pure_custom_func(msg.oneof_type.payload.single_int64)], @index0 + pure_custom_func(msg.oneof_type.payload.single_int32) + @index0 + pure_custom_func(msg.single_int64)) \ No newline at end of file diff --git a/optimizer/src/test/resources/large_expressions_bind_cascaded.baseline b/optimizer/src/test/resources/large_expressions_bind_cascaded.baseline deleted file mode 100644 index b39eebd06..000000000 --- a/optimizer/src/test/resources/large_expressions_bind_cascaded.baseline +++ /dev/null @@ -1,17 +0,0 @@ -Test case: CALC_FOUR_COMMON_SUBEXPR -Source: [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] -=====> -Result: [1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4] -Unparsed: cel.bind(@r3, [1, 2, 3, 4], cel.bind(@r2, [1, 2, 3], cel.bind(@r1, [1, 2], cel.bind(@r0, [1], @r0 + @r1 + @r2 + @r3 + @r0 + @r1 + @r2 + @r3 + @r0 + @r1 + @r2 + @r3 + @r0 + @r1 + @r2 + @r3 + @r0 + @r1 + @r2 + @r3 + @r0 + @r1 + @r2 + @r3 + @r0 + @r1 + @r2 + @r3 + @r0 + @r1 + @r2 + @r3 + @r0 + @r1 + @r2 + @r3 + @r0 + @r1 + @r2 + @r3 + @r0 + @r1 + @r2 + @r3 + @r0 + @r1 + @r2 + @r3 + @r0 + @r1 + @r2 + @r3 + @r0 + @r1 + @r2 + @r3 + @r0 + @r1 + @r2 + @r3 + @r0 + @r1 + @r2 + @r3 + @r0 + @r1 + @r2 + @r3 + @r0 + @r1 + @r2 + @r3 + @r0 + @r1 + @r2 + @r3 + @r0 + @r1 + @r2 + @r3 + @r0 + @r1 + @r2 + @r3 + @r0 + @r1 + @r2 + @r3 + @r0 + @r1 + @r2 + @r3 + @r0 + @r1 + @r2 + @r3 + @r0 + @r1 + @r2 + @r3 + @r0 + @r1 + @r2 + @r3 + @r0 + @r1 + @r2 + @r3 + @r0 + @r1 + @r2 + @r3 + @r0 + @r1 + @r2 + @r3 + @r0 + @r1 + @r2 + @r3 + @r0 + @r1 + @r2 + @r3 + @r0 + @r1 + @r2 + @r3 + @r0 + @r1 + @r2 + @r3 + @r0 + @r1 + @r2 + @r3 + @r0 + @r1 + @r2 + @r3 + @r0 + @r1 + @r2 + @r3 + @r0 + @r1 + @r2 + @r3 + @r0 + @r1 + @r2 + @r3 + @r0 + @r1 + @r2 + @r3 + @r0) + @r1) + @r2) + @r3) - -Test case: CALC_ALL_COMMON_SUBEXPR -Source: [0, 1] + [0, 1] + [1, 2] + [1, 2] + [2, 3] + [2, 3] + [3, 4] + [3, 4] + [4, 5] + [4, 5] + [5, 6] + [5, 6] + [6, 7] + [6, 7] + [7, 8] + [7, 8] + [8, 9] + [8, 9] + [9, 10] + [9, 10] + [10, 11] + [10, 11] + [11, 12] + [11, 12] + [12, 13] + [12, 13] + [13, 14] + [13, 14] + [14, 15] + [14, 15] + [15, 16] + [15, 16] + [16, 17] + [16, 17] + [17, 18] + [17, 18] + [18, 19] + [18, 19] + [19, 20] + [19, 20] + [20, 21] + [20, 21] + [21, 22] + [21, 22] + [22, 23] + [22, 23] + [23, 24] + [23, 24] + [24, 25] + [24, 25] + [25, 26] + [25, 26] + [26, 27] + [26, 27] + [27, 28] + [27, 28] + [28, 29] + [28, 29] + [29, 30] + [29, 30] + [30, 31] + [30, 31] + [31, 32] + [31, 32] + [32, 33] + [32, 33] + [33, 34] + [33, 34] + [34, 35] + [34, 35] + [35, 36] + [35, 36] + [36, 37] + [36, 37] + [37, 38] + [37, 38] + [38, 39] + [38, 39] + [39, 40] + [39, 40] + [40, 41] + [40, 41] + [41, 42] + [41, 42] + [42, 43] + [42, 43] + [43, 44] + [43, 44] + [44, 45] + [44, 45] + [45, 46] + [45, 46] + [46, 47] + [46, 47] + [47, 48] + [47, 48] + [48, 49] + [48, 49] + [49, 50] + [49, 50] -=====> -Result: [0, 1, 0, 1, 1, 2, 1, 2, 2, 3, 2, 3, 3, 4, 3, 4, 4, 5, 4, 5, 5, 6, 5, 6, 6, 7, 6, 7, 7, 8, 7, 8, 8, 9, 8, 9, 9, 10, 9, 10, 10, 11, 10, 11, 11, 12, 11, 12, 12, 13, 12, 13, 13, 14, 13, 14, 14, 15, 14, 15, 15, 16, 15, 16, 16, 17, 16, 17, 17, 18, 17, 18, 18, 19, 18, 19, 19, 20, 19, 20, 20, 21, 20, 21, 21, 22, 21, 22, 22, 23, 22, 23, 23, 24, 23, 24, 24, 25, 24, 25, 25, 26, 25, 26, 26, 27, 26, 27, 27, 28, 27, 28, 28, 29, 28, 29, 29, 30, 29, 30, 30, 31, 30, 31, 31, 32, 31, 32, 32, 33, 32, 33, 33, 34, 33, 34, 34, 35, 34, 35, 35, 36, 35, 36, 36, 37, 36, 37, 37, 38, 37, 38, 38, 39, 38, 39, 39, 40, 39, 40, 40, 41, 40, 41, 41, 42, 41, 42, 42, 43, 42, 43, 43, 44, 43, 44, 44, 45, 44, 45, 45, 46, 45, 46, 46, 47, 46, 47, 47, 48, 47, 48, 48, 49, 48, 49, 49, 50, 49, 50] -Unparsed: cel.bind(@r49, [49, 50], cel.bind(@r48, [48, 49], cel.bind(@r47, [47, 48], cel.bind(@r46, [46, 47], cel.bind(@r45, [45, 46], cel.bind(@r44, [44, 45], cel.bind(@r43, [43, 44], cel.bind(@r42, [42, 43], cel.bind(@r41, [41, 42], cel.bind(@r40, [40, 41], cel.bind(@r39, [39, 40], cel.bind(@r38, [38, 39], cel.bind(@r37, [37, 38], cel.bind(@r36, [36, 37], cel.bind(@r35, [35, 36], cel.bind(@r34, [34, 35], cel.bind(@r33, [33, 34], cel.bind(@r32, [32, 33], cel.bind(@r31, [31, 32], cel.bind(@r30, [30, 31], cel.bind(@r29, [29, 30], cel.bind(@r28, [28, 29], cel.bind(@r27, [27, 28], cel.bind(@r26, [26, 27], cel.bind(@r25, [25, 26], cel.bind(@r24, [24, 25], cel.bind(@r23, [23, 24], cel.bind(@r22, [22, 23], cel.bind(@r21, [21, 22], cel.bind(@r20, [20, 21], cel.bind(@r19, [19, 20], cel.bind(@r18, [18, 19], cel.bind(@r17, [17, 18], cel.bind(@r16, [16, 17], cel.bind(@r15, [15, 16], cel.bind(@r14, [14, 15], cel.bind(@r13, [13, 14], cel.bind(@r12, [12, 13], cel.bind(@r11, [11, 12], cel.bind(@r10, [10, 11], cel.bind(@r9, [9, 10], cel.bind(@r8, [8, 9], cel.bind(@r7, [7, 8], cel.bind(@r6, [6, 7], cel.bind(@r5, [5, 6], cel.bind(@r4, [4, 5], cel.bind(@r3, [3, 4], cel.bind(@r2, [2, 3], cel.bind(@r1, [1, 2], cel.bind(@r0, [0, 1], @r0 + @r0) + @r1 + @r1) + @r2 + @r2) + @r3 + @r3) + @r4 + @r4) + @r5 + @r5) + @r6 + @r6) + @r7 + @r7) + @r8 + @r8) + @r9 + @r9) + @r10 + @r10) + @r11 + @r11) + @r12 + @r12) + @r13 + @r13) + @r14 + @r14) + @r15 + @r15) + @r16 + @r16) + @r17 + @r17) + @r18 + @r18) + @r19 + @r19) + @r20 + @r20) + @r21 + @r21) + @r22 + @r22) + @r23 + @r23) + @r24 + @r24) + @r25 + @r25) + @r26 + @r26) + @r27 + @r27) + @r28 + @r28) + @r29 + @r29) + @r30 + @r30) + @r31 + @r31) + @r32 + @r32) + @r33 + @r33) + @r34 + @r34) + @r35 + @r35) + @r36 + @r36) + @r37 + @r37) + @r38 + @r38) + @r39 + @r39) + @r40 + @r40) + @r41 + @r41) + @r42 + @r42) + @r43 + @r43) + @r44 + @r44) + @r45 + @r45) + @r46 + @r46) + @r47 + @r47) + @r48 + @r48) + @r49 + @r49) - -Test case: NESTED_MACROS -Source: [1,2,3].map(i, [1, 2, 3].map(i, [1, 2, 3].map(i, [1, 2, 3].map(i, [1, 2, 3].map(i, [1, 2, 3].map(i, [1, 2, 3].map(i, [1, 2, 3].map(i, [1, 2, 3])))))))) -=====> -Result: [[[[[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]]], [[[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]]], [[[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]]]], [[[[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]]], [[[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]]], [[[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]]]], [[[[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]]], [[[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]]], [[[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]]]]] -Unparsed: cel.bind(@r0, [1, 2, 3], @r0.map(@c0:0, @r0.map(@c1:0, @r0.map(@c2:0, @r0.map(@c3:0, @r0.map(@c4:0, @r0.map(@c5:0, @r0.map(@c6:0, @r0.map(@c7:0, @r0))))))))) diff --git a/optimizer/src/test/resources/large_expressions_block_common_subexpr.baseline b/optimizer/src/test/resources/large_expressions_block_common_subexpr.baseline index 42c6e9e1a..fc5498563 100644 --- a/optimizer/src/test/resources/large_expressions_block_common_subexpr.baseline +++ b/optimizer/src/test/resources/large_expressions_block_common_subexpr.baseline @@ -14,4 +14,4 @@ Test case: NESTED_MACROS Source: [1,2,3].map(i, [1, 2, 3].map(i, [1, 2, 3].map(i, [1, 2, 3].map(i, [1, 2, 3].map(i, [1, 2, 3].map(i, [1, 2, 3].map(i, [1, 2, 3].map(i, [1, 2, 3])))))))) =====> Result: [[[[[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]]], [[[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]]], [[[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]]]], [[[[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]]], [[[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]]], [[[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]]]], [[[[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]]], [[[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]]], [[[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]]]]] -Unparsed: cel.@block([[1, 2, 3]], @index0.map(@c0:0, @index0.map(@c1:0, @index0.map(@c2:0, @index0.map(@c3:0, @index0.map(@c4:0, @index0.map(@c5:0, @index0.map(@c6:0, @index0.map(@c7:0, @index0))))))))) +Unparsed: cel.@block([[1, 2, 3]], @index0.map(@it:7:0, @index0.map(@it:6:0, @index0.map(@it:5:0, @index0.map(@it:4:0, @index0.map(@it:3:0, @index0.map(@it:2:0, @index0.map(@it:1:0, @index0.map(@it:0:0, @index0))))))))) \ No newline at end of file diff --git a/optimizer/src/test/resources/large_expressions_block_recursion_depth_1.baseline b/optimizer/src/test/resources/large_expressions_block_recursion_depth_1.baseline index f391e5f87..5d7b20381 100644 --- a/optimizer/src/test/resources/large_expressions_block_recursion_depth_1.baseline +++ b/optimizer/src/test/resources/large_expressions_block_recursion_depth_1.baseline @@ -14,4 +14,4 @@ Test case: NESTED_MACROS Source: [1,2,3].map(i, [1, 2, 3].map(i, [1, 2, 3].map(i, [1, 2, 3].map(i, [1, 2, 3].map(i, [1, 2, 3].map(i, [1, 2, 3].map(i, [1, 2, 3].map(i, [1, 2, 3])))))))) =====> Result: [[[[[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]]], [[[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]]], [[[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]]]], [[[[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]]], [[[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]]], [[[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]]]], [[[[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]]], [[[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]]], [[[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]]]]] -Unparsed: cel.@block([[1, 2, 3], [@index0], @x7:0 + @index1], @index0.map(@c0:0, @index0.map(@c1:0, @index0.map(@c2:0, @index0.map(@c3:0, @index0.map(@c4:0, @index0.map(@c5:0, @index0.map(@c6:0, @index0.map(@c7:0, @index0))))))))) +Unparsed: cel.@block([[1, 2, 3], [@index0]], @index0.map(@it:7:0, @index0.map(@it:6:0, @index0.map(@it:5:0, @index0.map(@it:4:0, @index0.map(@it:3:0, @index0.map(@it:2:0, @index0.map(@it:1:0, @index0.map(@it:0:0, @index0))))))))) \ No newline at end of file diff --git a/optimizer/src/test/resources/large_expressions_block_recursion_depth_2.baseline b/optimizer/src/test/resources/large_expressions_block_recursion_depth_2.baseline index d77db7b6f..aba464e22 100644 --- a/optimizer/src/test/resources/large_expressions_block_recursion_depth_2.baseline +++ b/optimizer/src/test/resources/large_expressions_block_recursion_depth_2.baseline @@ -14,4 +14,4 @@ Test case: NESTED_MACROS Source: [1,2,3].map(i, [1, 2, 3].map(i, [1, 2, 3].map(i, [1, 2, 3].map(i, [1, 2, 3].map(i, [1, 2, 3].map(i, [1, 2, 3].map(i, [1, 2, 3].map(i, [1, 2, 3])))))))) =====> Result: [[[[[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]]], [[[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]]], [[[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]]]], [[[[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]]], [[[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]]], [[[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]]]], [[[[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]]], [[[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]]], [[[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]]]]] -Unparsed: cel.@block([[1, 2, 3], @x7:0 + [@index0], @index0.map(@c7:0, @index0), @x6:0 + [@index2], @index0.map(@c6:0, @index2), @x5:0 + [@index4], @index0.map(@c5:0, @index4), @x4:0 + [@index6], @index0.map(@c4:0, @index6), @x3:0 + [@index8], @index0.map(@c3:0, @index8), @x2:0 + [@index10], @index0.map(@c2:0, @index10), @x1:0 + [@index12], @index0.map(@c1:0, @index12), @x0:0 + [@index14]], @index0.map(@c0:0, @index14)) +Unparsed: cel.@block([[1, 2, 3], [@index0], @index0.map(@it:0:0, @index0), [@index2], @index0.map(@it:1:0, @index2), [@index4], @index0.map(@it:2:0, @index4), [@index6], @index0.map(@it:3:0, @index6), [@index8], @index0.map(@it:4:0, @index8), [@index10], @index0.map(@it:5:0, @index10), [@index12], @index0.map(@it:6:0, @index12), [@index14]], @index0.map(@it:7:0, @index14)) \ No newline at end of file diff --git a/optimizer/src/test/resources/large_expressions_block_recursion_depth_3.baseline b/optimizer/src/test/resources/large_expressions_block_recursion_depth_3.baseline index 832173f88..e5a542ac4 100644 --- a/optimizer/src/test/resources/large_expressions_block_recursion_depth_3.baseline +++ b/optimizer/src/test/resources/large_expressions_block_recursion_depth_3.baseline @@ -2,16 +2,16 @@ Test case: CALC_FOUR_COMMON_SUBEXPR Source: [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] + [1] + [1,2] + [1,2,3] + [1,2,3,4] =====> Result: [1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 1, 2, 1, 2, 3, 1, 2, 3, 4] -Unparsed: cel.@block([[1], [1, 2], [1, 2, 3], [1, 2, 3, 4], @index0 + @index1 + @index2 + @index3, @index4 + @index0 + @index1 + @index2, @index5 + @index3 + @index0 + @index1, @index6 + @index2 + @index3 + @index0, @index7 + @index1 + @index2 + @index3, @index8 + @index0 + @index1 + @index2, @index9 + @index3 + @index0 + @index1, @index10 + @index2 + @index3 + @index0, @index11 + @index1 + @index2 + @index3, @index12 + @index0 + @index1 + @index2, @index13 + @index3 + @index0 + @index1, @index14 + @index2 + @index3 + @index0, @index15 + @index1 + @index2 + @index3, @index16 + @index0 + @index1 + @index2, @index17 + @index3 + @index0 + @index1, @index18 + @index2 + @index3 + @index0, @index19 + @index1 + @index2 + @index3, @index20 + @index0 + @index1 + @index2, @index21 + @index3 + @index0 + @index1, @index22 + @index2 + @index3 + @index0, @index23 + @index1 + @index2 + @index3, @index24 + @index0 + @index1 + @index2, @index25 + @index3 + @index0 + @index1, @index26 + @index2 + @index3 + @index0, @index27 + @index1 + @index2 + @index3, @index28 + @index0 + @index1 + @index2, @index29 + @index3 + @index0 + @index1, @index30 + @index2 + @index3 + @index0, @index31 + @index1 + @index2 + @index3, @index32 + @index0 + @index1 + @index2, @index33 + @index3 + @index0 + @index1, @index34 + @index2 + @index3 + @index0, @index35 + @index1 + @index2 + @index3, @index36 + @index0 + @index1 + @index2, @index37 + @index3 + @index0 + @index1, @index38 + @index2 + @index3 + @index0, @index39 + @index1 + @index2 + @index3, @index40 + @index0 + @index1 + @index2, @index41 + @index3 + @index0 + @index1, @index42 + @index2 + @index3 + @index0, @index43 + @index1 + @index2 + @index3, @index44 + @index0 + @index1 + @index2, @index45 + @index3 + @index0 + @index1, @index46 + @index2 + @index3 + @index0, @index47 + @index1 + @index2 + @index3, @index48 + @index0 + @index1 + @index2, @index49 + @index3 + @index0 + @index1, @index50 + @index2 + @index3 + @index0, @index51 + @index1 + @index2 + @index3, @index52 + @index0 + @index1 + @index2, @index53 + @index3 + @index0 + @index1, @index54 + @index2 + @index3 + @index0, @index55 + @index1 + @index2], @index56 + @index3) +Unparsed: cel.@block([[1], [1, 2], [1, 2, 3], [1, 2, 3, 4], @index0 + @index1 + @index2 + @index3, @index4 + @index0 + @index1 + @index2, @index5 + @index3 + @index0 + @index1, @index6 + @index2 + @index3 + @index0, @index7 + @index1 + @index2 + @index3, @index8 + @index0 + @index1 + @index2, @index9 + @index3 + @index0 + @index1, @index10 + @index2 + @index3 + @index0, @index11 + @index1 + @index2 + @index3, @index12 + @index0 + @index1 + @index2, @index13 + @index3 + @index0 + @index1, @index14 + @index2 + @index3 + @index0, @index15 + @index1 + @index2 + @index3, @index16 + @index0 + @index1 + @index2, @index17 + @index3 + @index0 + @index1, @index18 + @index2 + @index3 + @index0, @index19 + @index1 + @index2 + @index3, @index20 + @index0 + @index1 + @index2, @index21 + @index3 + @index0 + @index1, @index22 + @index2 + @index3 + @index0, @index23 + @index1 + @index2 + @index3, @index24 + @index0 + @index1 + @index2, @index25 + @index3 + @index0 + @index1, @index26 + @index2 + @index3 + @index0, @index27 + @index1 + @index2 + @index3, @index28 + @index0 + @index1 + @index2, @index29 + @index3 + @index0 + @index1, @index30 + @index2 + @index3 + @index0, @index31 + @index1 + @index2 + @index3, @index32 + @index0 + @index1 + @index2, @index33 + @index3 + @index0 + @index1, @index34 + @index2 + @index3 + @index0, @index35 + @index1 + @index2 + @index3, @index36 + @index0 + @index1 + @index2, @index37 + @index3 + @index0 + @index1, @index38 + @index2 + @index3 + @index0, @index39 + @index1 + @index2 + @index3, @index40 + @index0 + @index1 + @index2, @index41 + @index3 + @index0 + @index1, @index42 + @index2 + @index3 + @index0, @index43 + @index1 + @index2 + @index3, @index44 + @index0 + @index1 + @index2, @index45 + @index3 + @index0 + @index1, @index46 + @index2 + @index3 + @index0, @index47 + @index1 + @index2 + @index3, @index48 + @index0 + @index1 + @index2, @index49 + @index3 + @index0 + @index1, @index50 + @index2 + @index3 + @index0, @index51 + @index1 + @index2 + @index3, @index52 + @index0 + @index1 + @index2, @index53 + @index3 + @index0 + @index1, @index54 + @index2 + @index3 + @index0], @index55 + @index1 + @index2 + @index3) Test case: CALC_ALL_COMMON_SUBEXPR Source: [0, 1] + [0, 1] + [1, 2] + [1, 2] + [2, 3] + [2, 3] + [3, 4] + [3, 4] + [4, 5] + [4, 5] + [5, 6] + [5, 6] + [6, 7] + [6, 7] + [7, 8] + [7, 8] + [8, 9] + [8, 9] + [9, 10] + [9, 10] + [10, 11] + [10, 11] + [11, 12] + [11, 12] + [12, 13] + [12, 13] + [13, 14] + [13, 14] + [14, 15] + [14, 15] + [15, 16] + [15, 16] + [16, 17] + [16, 17] + [17, 18] + [17, 18] + [18, 19] + [18, 19] + [19, 20] + [19, 20] + [20, 21] + [20, 21] + [21, 22] + [21, 22] + [22, 23] + [22, 23] + [23, 24] + [23, 24] + [24, 25] + [24, 25] + [25, 26] + [25, 26] + [26, 27] + [26, 27] + [27, 28] + [27, 28] + [28, 29] + [28, 29] + [29, 30] + [29, 30] + [30, 31] + [30, 31] + [31, 32] + [31, 32] + [32, 33] + [32, 33] + [33, 34] + [33, 34] + [34, 35] + [34, 35] + [35, 36] + [35, 36] + [36, 37] + [36, 37] + [37, 38] + [37, 38] + [38, 39] + [38, 39] + [39, 40] + [39, 40] + [40, 41] + [40, 41] + [41, 42] + [41, 42] + [42, 43] + [42, 43] + [43, 44] + [43, 44] + [44, 45] + [44, 45] + [45, 46] + [45, 46] + [46, 47] + [46, 47] + [47, 48] + [47, 48] + [48, 49] + [48, 49] + [49, 50] + [49, 50] =====> Result: [0, 1, 0, 1, 1, 2, 1, 2, 2, 3, 2, 3, 3, 4, 3, 4, 4, 5, 4, 5, 5, 6, 5, 6, 6, 7, 6, 7, 7, 8, 7, 8, 8, 9, 8, 9, 9, 10, 9, 10, 10, 11, 10, 11, 11, 12, 11, 12, 12, 13, 12, 13, 13, 14, 13, 14, 14, 15, 14, 15, 15, 16, 15, 16, 16, 17, 16, 17, 17, 18, 17, 18, 18, 19, 18, 19, 19, 20, 19, 20, 20, 21, 20, 21, 21, 22, 21, 22, 22, 23, 22, 23, 23, 24, 23, 24, 24, 25, 24, 25, 25, 26, 25, 26, 26, 27, 26, 27, 27, 28, 27, 28, 28, 29, 28, 29, 29, 30, 29, 30, 30, 31, 30, 31, 31, 32, 31, 32, 32, 33, 32, 33, 33, 34, 33, 34, 34, 35, 34, 35, 35, 36, 35, 36, 36, 37, 36, 37, 37, 38, 37, 38, 38, 39, 38, 39, 39, 40, 39, 40, 40, 41, 40, 41, 41, 42, 41, 42, 42, 43, 42, 43, 43, 44, 43, 44, 44, 45, 44, 45, 45, 46, 45, 46, 46, 47, 46, 47, 47, 48, 47, 48, 48, 49, 48, 49, 49, 50, 49, 50] -Unparsed: cel.@block([[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7], [7, 8], [8, 9], [9, 10], [10, 11], [11, 12], [12, 13], [13, 14], [14, 15], [15, 16], [16, 17], [17, 18], [18, 19], [19, 20], [20, 21], [21, 22], [22, 23], [23, 24], [24, 25], [25, 26], [26, 27], [27, 28], [28, 29], [29, 30], [30, 31], [31, 32], [32, 33], [33, 34], [34, 35], [35, 36], [36, 37], [37, 38], [38, 39], [39, 40], [40, 41], [41, 42], [42, 43], [43, 44], [44, 45], [45, 46], [46, 47], [47, 48], [48, 49], [49, 50], @index0 + @index0 + @index1 + @index1, @index50 + @index2 + @index2 + @index3, @index51 + @index3 + @index4 + @index4, @index52 + @index5 + @index5 + @index6, @index53 + @index6 + @index7 + @index7, @index54 + @index8 + @index8 + @index9, @index55 + @index9 + @index10 + @index10, @index56 + @index11 + @index11 + @index12, @index57 + @index12 + @index13 + @index13, @index58 + @index14 + @index14 + @index15, @index59 + @index15 + @index16 + @index16, @index60 + @index17 + @index17 + @index18, @index61 + @index18 + @index19 + @index19, @index62 + @index20 + @index20 + @index21, @index63 + @index21 + @index22 + @index22, @index64 + @index23 + @index23 + @index24, @index65 + @index24 + @index25 + @index25, @index66 + @index26 + @index26 + @index27, @index67 + @index27 + @index28 + @index28, @index68 + @index29 + @index29 + @index30, @index69 + @index30 + @index31 + @index31, @index70 + @index32 + @index32 + @index33, @index71 + @index33 + @index34 + @index34, @index72 + @index35 + @index35 + @index36, @index73 + @index36 + @index37 + @index37, @index74 + @index38 + @index38 + @index39, @index75 + @index39 + @index40 + @index40, @index76 + @index41 + @index41 + @index42, @index77 + @index42 + @index43 + @index43, @index78 + @index44 + @index44 + @index45, @index79 + @index45 + @index46 + @index46, @index80 + @index47 + @index47 + @index48, @index81 + @index48 + @index49], @index82 + @index49) +Unparsed: cel.@block([[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7], [7, 8], [8, 9], [9, 10], [10, 11], [11, 12], [12, 13], [13, 14], [14, 15], [15, 16], [16, 17], [17, 18], [18, 19], [19, 20], [20, 21], [21, 22], [22, 23], [23, 24], [24, 25], [25, 26], [26, 27], [27, 28], [28, 29], [29, 30], [30, 31], [31, 32], [32, 33], [33, 34], [34, 35], [35, 36], [36, 37], [37, 38], [38, 39], [39, 40], [40, 41], [41, 42], [42, 43], [43, 44], [44, 45], [45, 46], [46, 47], [47, 48], [48, 49], [49, 50], @index0 + @index0 + @index1 + @index1, @index50 + @index2 + @index2 + @index3, @index51 + @index3 + @index4 + @index4, @index52 + @index5 + @index5 + @index6, @index53 + @index6 + @index7 + @index7, @index54 + @index8 + @index8 + @index9, @index55 + @index9 + @index10 + @index10, @index56 + @index11 + @index11 + @index12, @index57 + @index12 + @index13 + @index13, @index58 + @index14 + @index14 + @index15, @index59 + @index15 + @index16 + @index16, @index60 + @index17 + @index17 + @index18, @index61 + @index18 + @index19 + @index19, @index62 + @index20 + @index20 + @index21, @index63 + @index21 + @index22 + @index22, @index64 + @index23 + @index23 + @index24, @index65 + @index24 + @index25 + @index25, @index66 + @index26 + @index26 + @index27, @index67 + @index27 + @index28 + @index28, @index68 + @index29 + @index29 + @index30, @index69 + @index30 + @index31 + @index31, @index70 + @index32 + @index32 + @index33, @index71 + @index33 + @index34 + @index34, @index72 + @index35 + @index35 + @index36, @index73 + @index36 + @index37 + @index37, @index74 + @index38 + @index38 + @index39, @index75 + @index39 + @index40 + @index40, @index76 + @index41 + @index41 + @index42, @index77 + @index42 + @index43 + @index43, @index78 + @index44 + @index44 + @index45, @index79 + @index45 + @index46 + @index46, @index80 + @index47 + @index47 + @index48], @index81 + @index48 + @index49 + @index49) Test case: NESTED_MACROS Source: [1,2,3].map(i, [1, 2, 3].map(i, [1, 2, 3].map(i, [1, 2, 3].map(i, [1, 2, 3].map(i, [1, 2, 3].map(i, [1, 2, 3].map(i, [1, 2, 3].map(i, [1, 2, 3])))))))) =====> Result: [[[[[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]]], [[[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]]], [[[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]]]], [[[[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]]], [[[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]]], [[[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]]]], [[[[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]]], [[[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]]], [[[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]], [[[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]], [[[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]], [[[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]], [[[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3], [1, 2, 3]]]]]]]]] -Unparsed: cel.@block([[1, 2, 3], @index0.map(@c7:0, @index0), @index0.map(@c6:0, @index1), @index0.map(@c5:0, @index2), @index0.map(@c4:0, @index3), @index0.map(@c3:0, @index4), @index0.map(@c2:0, @index5), @index0.map(@c1:0, @index6), @x0:0 + [@index7]], @index0.map(@c0:0, @index7)) +Unparsed: cel.@block([[1, 2, 3], @index0.map(@it:0:0, @index0), @index0.map(@it:1:0, @index1), @index0.map(@it:2:0, @index2), @index0.map(@it:3:0, @index3), @index0.map(@it:4:0, @index4), @index0.map(@it:5:0, @index5), @index0.map(@it:6:0, @index6)], @index0.map(@it:7:0, @index7)) \ No newline at end of file diff --git a/optimizer/src/test/resources/subexpression_ast_block_common_subexpr_comprehension_structure_retained.baseline b/optimizer/src/test/resources/subexpression_ast_block_common_subexpr_comprehension_structure_retained.baseline new file mode 100644 index 000000000..61dc23350 --- /dev/null +++ b/optimizer/src/test/resources/subexpression_ast_block_common_subexpr_comprehension_structure_retained.baseline @@ -0,0 +1,3422 @@ +Test case: SIZE_1 +Source: size([1,2]) + size([1,2]) + 1 == 5 +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + CALL [3] { + function: size + args: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + CONSTANT [6] { value: 2 } + } + } + } + } + } + } + CALL [7] { + function: _==_ + args: { + CALL [8] { + function: _+_ + args: { + CALL [9] { + function: _+_ + args: { + IDENT [10] { + name: @index0 + } + IDENT [11] { + name: @index0 + } + } + } + CONSTANT [12] { value: 1 } + } + } + CONSTANT [13] { value: 5 } + } + } + } +} +Test case: SIZE_2 +Source: 2 + size([1,2]) + size([1,2]) + 1 == 7 +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + CALL [3] { + function: size + args: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + CONSTANT [6] { value: 2 } + } + } + } + } + } + } + CALL [7] { + function: _==_ + args: { + CALL [8] { + function: _+_ + args: { + CALL [9] { + function: _+_ + args: { + CALL [10] { + function: _+_ + args: { + CONSTANT [11] { value: 2 } + IDENT [12] { + name: @index0 + } + } + } + IDENT [13] { + name: @index0 + } + } + } + CONSTANT [14] { value: 1 } + } + } + CONSTANT [15] { value: 7 } + } + } + } +} +Test case: SIZE_3 +Source: size([0]) + size([0]) + size([1,2]) + size([1,2]) == 6 +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + CALL [3] { + function: size + args: { + LIST [4] { + elements: { + CONSTANT [5] { value: 0 } + } + } + } + } + CALL [6] { + function: size + args: { + LIST [7] { + elements: { + CONSTANT [8] { value: 1 } + CONSTANT [9] { value: 2 } + } + } + } + } + } + } + CALL [10] { + function: _==_ + args: { + CALL [11] { + function: _+_ + args: { + CALL [12] { + function: _+_ + args: { + CALL [13] { + function: _+_ + args: { + IDENT [14] { + name: @index0 + } + IDENT [15] { + name: @index0 + } + } + } + IDENT [16] { + name: @index1 + } + } + } + IDENT [17] { + name: @index1 + } + } + } + CONSTANT [18] { value: 6 } + } + } + } +} +Test case: SIZE_4 +Source: 5 + size([0]) + size([0]) + size([1,2]) + size([1,2]) + size([1,2,3]) + size([1,2,3]) == 17 +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + CALL [3] { + function: size + args: { + LIST [4] { + elements: { + CONSTANT [5] { value: 0 } + } + } + } + } + CALL [6] { + function: size + args: { + LIST [7] { + elements: { + CONSTANT [8] { value: 1 } + CONSTANT [9] { value: 2 } + } + } + } + } + CALL [10] { + function: size + args: { + LIST [11] { + elements: { + CONSTANT [12] { value: 1 } + CONSTANT [13] { value: 2 } + CONSTANT [14] { value: 3 } + } + } + } + } + } + } + CALL [15] { + function: _==_ + args: { + CALL [16] { + function: _+_ + args: { + CALL [17] { + function: _+_ + args: { + CALL [18] { + function: _+_ + args: { + CALL [19] { + function: _+_ + args: { + CALL [20] { + function: _+_ + args: { + CALL [21] { + function: _+_ + args: { + CONSTANT [22] { value: 5 } + IDENT [23] { + name: @index0 + } + } + } + IDENT [24] { + name: @index0 + } + } + } + IDENT [25] { + name: @index1 + } + } + } + IDENT [26] { + name: @index1 + } + } + } + IDENT [27] { + name: @index2 + } + } + } + IDENT [28] { + name: @index2 + } + } + } + CONSTANT [29] { value: 17 } + } + } + } +} +Test case: TIMESTAMP +Source: timestamp(int(timestamp(1000000000))).getFullYear() + timestamp(int(timestamp(75))).getFullYear() + timestamp(int(timestamp(50))).getFullYear() + timestamp(int(timestamp(1000000000))).getFullYear() + timestamp(int(timestamp(50))).getSeconds() + timestamp(int(timestamp(200))).getFullYear() + timestamp(int(timestamp(200))).getFullYear() + timestamp(int(timestamp(75))).getMinutes() + timestamp(int(timestamp(1000000000))).getFullYear() == 13934 +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + CALL [3] { + function: getFullYear + target: { + CALL [4] { + function: timestamp + args: { + CALL [5] { + function: int + args: { + CALL [6] { + function: timestamp + args: { + CONSTANT [7] { value: 1000000000 } + } + } + } + } + } + } + } + args: { + } + } + CALL [8] { + function: timestamp + args: { + CALL [9] { + function: int + args: { + CALL [10] { + function: timestamp + args: { + CONSTANT [11] { value: 50 } + } + } + } + } + } + } + CALL [12] { + function: getFullYear + target: { + CALL [13] { + function: timestamp + args: { + CALL [14] { + function: int + args: { + CALL [15] { + function: timestamp + args: { + CONSTANT [16] { value: 200 } + } + } + } + } + } + } + } + args: { + } + } + CALL [17] { + function: timestamp + args: { + CALL [18] { + function: int + args: { + CALL [19] { + function: timestamp + args: { + CONSTANT [20] { value: 75 } + } + } + } + } + } + } + } + } + CALL [21] { + function: _==_ + args: { + CALL [22] { + function: _+_ + args: { + CALL [23] { + function: _+_ + args: { + CALL [24] { + function: _+_ + args: { + CALL [25] { + function: _+_ + args: { + CALL [26] { + function: _+_ + args: { + CALL [27] { + function: _+_ + args: { + CALL [28] { + function: _+_ + args: { + CALL [29] { + function: _+_ + args: { + IDENT [30] { + name: @index0 + } + CALL [31] { + function: getFullYear + target: { + IDENT [32] { + name: @index3 + } + } + args: { + } + } + } + } + CALL [33] { + function: getFullYear + target: { + IDENT [34] { + name: @index1 + } + } + args: { + } + } + } + } + IDENT [35] { + name: @index0 + } + } + } + CALL [36] { + function: getSeconds + target: { + IDENT [37] { + name: @index1 + } + } + args: { + } + } + } + } + IDENT [38] { + name: @index2 + } + } + } + IDENT [39] { + name: @index2 + } + } + } + CALL [40] { + function: getMinutes + target: { + IDENT [41] { + name: @index3 + } + } + args: { + } + } + } + } + IDENT [42] { + name: @index0 + } + } + } + CONSTANT [43] { value: 13934 } + } + } + } +} +Test case: MAP_INDEX +Source: {"a": 2}["a"] + {"a": 2}["a"] * {"a": 2}["a"] == 6 +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + CALL [3] { + function: _[_] + args: { + MAP [4] { + MAP_ENTRY [5] { + key: { + CONSTANT [6] { value: "a" } + } + value: { + CONSTANT [7] { value: 2 } + } + } + } + CONSTANT [8] { value: "a" } + } + } + } + } + CALL [9] { + function: _==_ + args: { + CALL [10] { + function: _+_ + args: { + IDENT [11] { + name: @index0 + } + CALL [12] { + function: _*_ + args: { + IDENT [13] { + name: @index0 + } + IDENT [14] { + name: @index0 + } + } + } + } + } + CONSTANT [15] { value: 6 } + } + } + } +} +Test case: NESTED_MAP_CONSTRUCTION +Source: {'a': {'b': 1}, 'c': {'b': 1}, 'd': {'e': {'b': 1}}, 'e': {'e': {'b': 1}}} +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + MAP [3] { + MAP_ENTRY [4] { + key: { + CONSTANT [5] { value: "b" } + } + value: { + CONSTANT [6] { value: 1 } + } + } + } + MAP [7] { + MAP_ENTRY [8] { + key: { + CONSTANT [9] { value: "e" } + } + value: { + IDENT [10] { + name: @index0 + } + } + } + } + } + } + MAP [11] { + MAP_ENTRY [12] { + key: { + CONSTANT [13] { value: "a" } + } + value: { + IDENT [14] { + name: @index0 + } + } + } + MAP_ENTRY [15] { + key: { + CONSTANT [16] { value: "c" } + } + value: { + IDENT [17] { + name: @index0 + } + } + } + MAP_ENTRY [18] { + key: { + CONSTANT [19] { value: "d" } + } + value: { + IDENT [20] { + name: @index1 + } + } + } + MAP_ENTRY [21] { + key: { + CONSTANT [22] { value: "e" } + } + value: { + IDENT [23] { + name: @index1 + } + } + } + } + } +} +Test case: NESTED_LIST_CONSTRUCTION +Source: [1, [1,2,3,4], 2, [1,2,3,4], 5, [1,2,3,4], 7, [[1,2], [1,2,3,4]], [1,2]] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + CONSTANT [5] { value: 2 } + CONSTANT [6] { value: 3 } + CONSTANT [7] { value: 4 } + } + } + LIST [8] { + elements: { + CONSTANT [9] { value: 1 } + CONSTANT [10] { value: 2 } + } + } + } + } + LIST [11] { + elements: { + CONSTANT [12] { value: 1 } + IDENT [13] { + name: @index0 + } + CONSTANT [14] { value: 2 } + IDENT [15] { + name: @index0 + } + CONSTANT [16] { value: 5 } + IDENT [17] { + name: @index0 + } + CONSTANT [18] { value: 7 } + LIST [19] { + elements: { + IDENT [20] { + name: @index1 + } + IDENT [21] { + name: @index0 + } + } + } + IDENT [22] { + name: @index1 + } + } + } + } +} +Test case: SELECT +Source: msg.single_int64 + msg.single_int64 == 6 +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + SELECT [3] { + IDENT [4] { + name: msg + }.single_int64 + } + } + } + CALL [5] { + function: _==_ + args: { + CALL [6] { + function: _+_ + args: { + IDENT [7] { + name: @index0 + } + IDENT [8] { + name: @index0 + } + } + } + CONSTANT [9] { value: 6 } + } + } + } +} +Test case: SELECT_NESTED_1 +Source: msg.oneof_type.payload.single_int64 + msg.oneof_type.payload.single_int32 + msg.oneof_type.payload.single_int64 + msg.single_int64 + msg.oneof_type.payload.oneof_type.payload.single_int64 == 31 +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + SELECT [3] { + SELECT [4] { + IDENT [5] { + name: msg + }.oneof_type + }.payload + } + SELECT [6] { + IDENT [7] { + name: @index0 + }.single_int64 + } + } + } + CALL [8] { + function: _==_ + args: { + CALL [9] { + function: _+_ + args: { + CALL [10] { + function: _+_ + args: { + CALL [11] { + function: _+_ + args: { + CALL [12] { + function: _+_ + args: { + IDENT [13] { + name: @index1 + } + SELECT [14] { + IDENT [15] { + name: @index0 + }.single_int32 + } + } + } + IDENT [16] { + name: @index1 + } + } + } + SELECT [17] { + IDENT [18] { + name: msg + }.single_int64 + } + } + } + SELECT [19] { + SELECT [20] { + SELECT [21] { + IDENT [22] { + name: @index0 + }.oneof_type + }.payload + }.single_int64 + } + } + } + CONSTANT [23] { value: 31 } + } + } + } +} +Test case: SELECT_NESTED_2 +Source: true || msg.oneof_type.payload.oneof_type.payload.oneof_type.payload.oneof_type.payload.single_bool || msg.oneof_type.payload.oneof_type.payload.oneof_type.child.child.payload.single_bool +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + SELECT [3] { + SELECT [4] { + SELECT [5] { + SELECT [6] { + SELECT [7] { + IDENT [8] { + name: msg + }.oneof_type + }.payload + }.oneof_type + }.payload + }.oneof_type + } + } + } + CALL [9] { + function: _||_ + args: { + CALL [10] { + function: _||_ + args: { + CONSTANT [11] { value: true } + SELECT [12] { + SELECT [13] { + SELECT [14] { + SELECT [15] { + IDENT [16] { + name: @index0 + }.payload + }.oneof_type + }.payload + }.single_bool + } + } + } + SELECT [17] { + SELECT [18] { + SELECT [19] { + SELECT [20] { + IDENT [21] { + name: @index0 + }.child + }.child + }.payload + }.single_bool + } + } + } + } +} +Test case: SELECT_NESTED_MESSAGE_MAP_INDEX_1 +Source: msg.oneof_type.payload.map_int32_int64[1] + msg.oneof_type.payload.map_int32_int64[1] + msg.oneof_type.payload.map_int32_int64[1] == 15 +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + CALL [3] { + function: _[_] + args: { + SELECT [4] { + SELECT [5] { + SELECT [6] { + IDENT [7] { + name: msg + }.oneof_type + }.payload + }.map_int32_int64 + } + CONSTANT [8] { value: 1 } + } + } + } + } + CALL [9] { + function: _==_ + args: { + CALL [10] { + function: _+_ + args: { + CALL [11] { + function: _+_ + args: { + IDENT [12] { + name: @index0 + } + IDENT [13] { + name: @index0 + } + } + } + IDENT [14] { + name: @index0 + } + } + } + CONSTANT [15] { value: 15 } + } + } + } +} +Test case: SELECT_NESTED_MESSAGE_MAP_INDEX_2 +Source: msg.oneof_type.payload.map_int32_int64[0] + msg.oneof_type.payload.map_int32_int64[1] + msg.oneof_type.payload.map_int32_int64[2] == 8 +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + SELECT [3] { + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.map_int32_int64 + } + } + } + CALL [7] { + function: _==_ + args: { + CALL [8] { + function: _+_ + args: { + CALL [9] { + function: _+_ + args: { + CALL [10] { + function: _[_] + args: { + IDENT [11] { + name: @index0 + } + CONSTANT [12] { value: 0 } + } + } + CALL [13] { + function: _[_] + args: { + IDENT [14] { + name: @index0 + } + CONSTANT [15] { value: 1 } + } + } + } + } + CALL [16] { + function: _[_] + args: { + IDENT [17] { + name: @index0 + } + CONSTANT [18] { value: 2 } + } + } + } + } + CONSTANT [19] { value: 8 } + } + } + } +} +Test case: SELECT_NESTED_NO_COMMON_SUBEXPR +Source: msg.oneof_type.payload.oneof_type.payload.oneof_type.payload.oneof_type.payload.single_int64 +=====> +SELECT [10] { + SELECT [9] { + SELECT [8] { + SELECT [7] { + SELECT [6] { + SELECT [5] { + SELECT [4] { + SELECT [3] { + SELECT [2] { + IDENT [1] { + name: msg + }.oneof_type + }.payload + }.oneof_type + }.payload + }.oneof_type + }.payload + }.oneof_type + }.payload + }.single_int64 +} +Test case: TERNARY +Source: (msg.single_int64 > 0 ? msg.single_int64 : 0) == 3 +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + SELECT [3] { + IDENT [4] { + name: msg + }.single_int64 + } + } + } + CALL [5] { + function: _==_ + args: { + CALL [6] { + function: _?_:_ + args: { + CALL [7] { + function: _>_ + args: { + IDENT [8] { + name: @index0 + } + CONSTANT [9] { value: 0 } + } + } + IDENT [10] { + name: @index0 + } + CONSTANT [11] { value: 0 } + } + } + CONSTANT [12] { value: 3 } + } + } + } +} +Test case: TERNARY_BIND_RHS_ONLY +Source: false ? false : (msg.single_int64) + ((msg.single_int64 + 1) * 2) == 11 +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + SELECT [3] { + IDENT [4] { + name: msg + }.single_int64 + } + } + } + CALL [5] { + function: _?_:_ + args: { + CONSTANT [6] { value: false } + CONSTANT [7] { value: false } + CALL [8] { + function: _==_ + args: { + CALL [9] { + function: _+_ + args: { + IDENT [10] { + name: @index0 + } + CALL [11] { + function: _*_ + args: { + CALL [12] { + function: _+_ + args: { + IDENT [13] { + name: @index0 + } + CONSTANT [14] { value: 1 } + } + } + CONSTANT [15] { value: 2 } + } + } + } + } + CONSTANT [16] { value: 11 } + } + } + } + } + } +} +Test case: NESTED_TERNARY +Source: (msg.single_int64 > 0 ? (msg.single_int32 > 0 ? msg.single_int64 + msg.single_int32 : 0) : 0) == 8 +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + SELECT [3] { + IDENT [4] { + name: msg + }.single_int64 + } + SELECT [5] { + IDENT [6] { + name: msg + }.single_int32 + } + } + } + CALL [7] { + function: _==_ + args: { + CALL [8] { + function: _?_:_ + args: { + CALL [9] { + function: _>_ + args: { + IDENT [10] { + name: @index0 + } + CONSTANT [11] { value: 0 } + } + } + CALL [12] { + function: _?_:_ + args: { + CALL [13] { + function: _>_ + args: { + IDENT [14] { + name: @index1 + } + CONSTANT [15] { value: 0 } + } + } + CALL [16] { + function: _+_ + args: { + IDENT [17] { + name: @index0 + } + IDENT [18] { + name: @index1 + } + } + } + CONSTANT [19] { value: 0 } + } + } + CONSTANT [20] { value: 0 } + } + } + CONSTANT [21] { value: 8 } + } + } + } +} +Test case: MULTIPLE_MACROS_1 +Source: size([[1].exists(i, i > 0)]) + size([[1].exists(j, j > 0)]) + size([[2].exists(k, k > 1)]) + size([[2].exists(l, l > 1)]) == 4 +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + COMPREHENSION [3] { + iter_var: @it:0:0 + iter_range: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [6] { value: false } + } + loop_condition: { + CALL [7] { + function: @not_strictly_false + args: { + CALL [8] { + function: !_ + args: { + IDENT [9] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [10] { + function: _||_ + args: { + IDENT [11] { + name: @ac:0:0 + } + CALL [12] { + function: _>_ + args: { + IDENT [13] { + name: @it:0:0 + } + CONSTANT [14] { value: 0 } + } + } + } + } + } + result: { + IDENT [15] { + name: @ac:0:0 + } + } + } + CALL [16] { + function: size + args: { + LIST [17] { + elements: { + IDENT [18] { + name: @index0 + } + } + } + } + } + COMPREHENSION [19] { + iter_var: @it:0:0 + iter_range: { + LIST [20] { + elements: { + CONSTANT [21] { value: 2 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [22] { value: false } + } + loop_condition: { + CALL [23] { + function: @not_strictly_false + args: { + CALL [24] { + function: !_ + args: { + IDENT [25] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [26] { + function: _||_ + args: { + IDENT [27] { + name: @ac:0:0 + } + CALL [28] { + function: _>_ + args: { + IDENT [29] { + name: @it:0:0 + } + CONSTANT [30] { value: 1 } + } + } + } + } + } + result: { + IDENT [31] { + name: @ac:0:0 + } + } + } + CALL [32] { + function: size + args: { + LIST [33] { + elements: { + IDENT [34] { + name: @index2 + } + } + } + } + } + } + } + CALL [35] { + function: _==_ + args: { + CALL [36] { + function: _+_ + args: { + CALL [37] { + function: _+_ + args: { + CALL [38] { + function: _+_ + args: { + IDENT [39] { + name: @index1 + } + IDENT [40] { + name: @index1 + } + } + } + IDENT [41] { + name: @index3 + } + } + } + IDENT [42] { + name: @index3 + } + } + } + CONSTANT [43] { value: 4 } + } + } + } +} +Test case: MULTIPLE_MACROS_2 +Source: [[1].exists(i, i > 0)] + [[1].exists(j, j > 0)] + [['a'].exists(k, k == 'a')] + [['a'].exists(l, l == 'a')] == [true, true, true, true] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + COMPREHENSION [3] { + iter_var: @it:0:0 + iter_range: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [6] { value: false } + } + loop_condition: { + CALL [7] { + function: @not_strictly_false + args: { + CALL [8] { + function: !_ + args: { + IDENT [9] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [10] { + function: _||_ + args: { + IDENT [11] { + name: @ac:0:0 + } + CALL [12] { + function: _>_ + args: { + IDENT [13] { + name: @it:0:0 + } + CONSTANT [14] { value: 0 } + } + } + } + } + } + result: { + IDENT [15] { + name: @ac:0:0 + } + } + } + LIST [16] { + elements: { + IDENT [17] { + name: @index0 + } + } + } + COMPREHENSION [18] { + iter_var: @it:0:1 + iter_range: { + LIST [19] { + elements: { + CONSTANT [20] { value: "a" } + } + } + } + accu_var: @ac:0:1 + accu_init: { + CONSTANT [21] { value: false } + } + loop_condition: { + CALL [22] { + function: @not_strictly_false + args: { + CALL [23] { + function: !_ + args: { + IDENT [24] { + name: @ac:0:1 + } + } + } + } + } + } + loop_step: { + CALL [25] { + function: _||_ + args: { + IDENT [26] { + name: @ac:0:1 + } + CALL [27] { + function: _==_ + args: { + IDENT [28] { + name: @it:0:1 + } + CONSTANT [29] { value: "a" } + } + } + } + } + } + result: { + IDENT [30] { + name: @ac:0:1 + } + } + } + LIST [31] { + elements: { + IDENT [32] { + name: @index2 + } + } + } + } + } + CALL [33] { + function: _==_ + args: { + CALL [34] { + function: _+_ + args: { + CALL [35] { + function: _+_ + args: { + CALL [36] { + function: _+_ + args: { + IDENT [37] { + name: @index1 + } + IDENT [38] { + name: @index1 + } + } + } + IDENT [39] { + name: @index3 + } + } + } + IDENT [40] { + name: @index3 + } + } + } + LIST [41] { + elements: { + CONSTANT [42] { value: true } + CONSTANT [43] { value: true } + CONSTANT [44] { value: true } + CONSTANT [45] { value: true } + } + } + } + } + } +} +Test case: MULTIPLE_MACROS_3 +Source: [1].exists(i, i > 0) && [1].exists(j, j > 0) && [1].exists(k, k > 1) && [2].exists(l, l > 1) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + COMPREHENSION [3] { + iter_var: @it:0:0 + iter_range: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [6] { value: false } + } + loop_condition: { + CALL [7] { + function: @not_strictly_false + args: { + CALL [8] { + function: !_ + args: { + IDENT [9] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [10] { + function: _||_ + args: { + IDENT [11] { + name: @ac:0:0 + } + CALL [12] { + function: _>_ + args: { + IDENT [13] { + name: @it:0:0 + } + CONSTANT [14] { value: 0 } + } + } + } + } + } + result: { + IDENT [15] { + name: @ac:0:0 + } + } + } + } + } + CALL [16] { + function: _&&_ + args: { + CALL [17] { + function: _&&_ + args: { + IDENT [18] { + name: @index0 + } + IDENT [19] { + name: @index0 + } + } + } + CALL [20] { + function: _&&_ + args: { + COMPREHENSION [21] { + iter_var: @it:0:0 + iter_range: { + LIST [22] { + elements: { + CONSTANT [23] { value: 1 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [24] { value: false } + } + loop_condition: { + CALL [25] { + function: @not_strictly_false + args: { + CALL [26] { + function: !_ + args: { + IDENT [27] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [28] { + function: _||_ + args: { + IDENT [29] { + name: @ac:0:0 + } + CALL [30] { + function: _>_ + args: { + IDENT [31] { + name: @it:0:0 + } + CONSTANT [32] { value: 1 } + } + } + } + } + } + result: { + IDENT [33] { + name: @ac:0:0 + } + } + } + COMPREHENSION [34] { + iter_var: @it:0:0 + iter_range: { + LIST [35] { + elements: { + CONSTANT [36] { value: 2 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [37] { value: false } + } + loop_condition: { + CALL [38] { + function: @not_strictly_false + args: { + CALL [39] { + function: !_ + args: { + IDENT [40] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [41] { + function: _||_ + args: { + IDENT [42] { + name: @ac:0:0 + } + CALL [43] { + function: _>_ + args: { + IDENT [44] { + name: @it:0:0 + } + CONSTANT [45] { value: 1 } + } + } + } + } + } + result: { + IDENT [46] { + name: @ac:0:0 + } + } + } + } + } + } + } + } +} +Test case: NESTED_MACROS +Source: [1,2,3].map(i, [1, 2, 3].map(i, i + 1)) == [[2, 3, 4], [2, 3, 4], [2, 3, 4]] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + CONSTANT [5] { value: 2 } + CONSTANT [6] { value: 3 } + } + } + LIST [7] { + elements: { + CONSTANT [8] { value: 2 } + CONSTANT [9] { value: 3 } + CONSTANT [10] { value: 4 } + } + } + } + } + CALL [11] { + function: _==_ + args: { + COMPREHENSION [12] { + iter_var: @it:0:0 + iter_range: { + IDENT [13] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [14] { + elements: { + } + } + } + loop_condition: { + CONSTANT [15] { value: true } + } + loop_step: { + CALL [16] { + function: _+_ + args: { + IDENT [17] { + name: @ac:0:0 + } + LIST [18] { + elements: { + COMPREHENSION [19] { + iter_var: @it:1:0 + iter_range: { + IDENT [20] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [21] { + elements: { + } + } + } + loop_condition: { + CONSTANT [22] { value: true } + } + loop_step: { + CALL [23] { + function: _+_ + args: { + IDENT [24] { + name: @ac:1:0 + } + LIST [25] { + elements: { + CALL [26] { + function: _+_ + args: { + IDENT [27] { + name: @it:1:0 + } + CONSTANT [28] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [29] { + name: @ac:1:0 + } + } + } + } + } + } + } + } + result: { + IDENT [30] { + name: @ac:0:0 + } + } + } + LIST [31] { + elements: { + IDENT [32] { + name: @index1 + } + IDENT [33] { + name: @index1 + } + IDENT [34] { + name: @index1 + } + } + } + } + } + } +} +Test case: NESTED_MACROS_2 +Source: [1, 2].map(y, [1, 2, 3].filter(x, x == y)) == [[1], [2]] +=====> +CALL [31] { + function: _==_ + args: { + COMPREHENSION [30] { + iter_var: @it:0:0 + iter_range: { + LIST [1] { + elements: { + CONSTANT [2] { value: 1 } + CONSTANT [3] { value: 2 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [24] { + elements: { + } + } + } + loop_condition: { + CONSTANT [25] { value: true } + } + loop_step: { + CALL [28] { + function: _+_ + args: { + IDENT [26] { + name: @ac:0:0 + } + LIST [27] { + elements: { + COMPREHENSION [23] { + iter_var: @it:1:0 + iter_range: { + LIST [6] { + elements: { + CONSTANT [7] { value: 1 } + CONSTANT [8] { value: 2 } + CONSTANT [9] { value: 3 } + } + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [15] { + elements: { + } + } + } + loop_condition: { + CONSTANT [16] { value: true } + } + loop_step: { + CALL [21] { + function: _?_:_ + args: { + CALL [13] { + function: _==_ + args: { + IDENT [12] { + name: @it:1:0 + } + IDENT [14] { + name: @it:0:0 + } + } + } + CALL [19] { + function: _+_ + args: { + IDENT [17] { + name: @ac:1:0 + } + LIST [18] { + elements: { + IDENT [11] { + name: @it:1:0 + } + } + } + } + } + IDENT [20] { + name: @ac:1:0 + } + } + } + } + result: { + IDENT [22] { + name: @ac:1:0 + } + } + } + } + } + } + } + } + result: { + IDENT [29] { + name: @ac:0:0 + } + } + } + LIST [32] { + elements: { + LIST [33] { + elements: { + CONSTANT [34] { value: 1 } + } + } + LIST [35] { + elements: { + CONSTANT [36] { value: 2 } + } + } + } + } + } +} +Test case: ADJACENT_NESTED_MACROS +Source: [1,2,3].map(i, [1, 2, 3].map(i, i + 1)) == [1,2,3].map(j, [1, 2, 3].map(j, j + 1)) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + CONSTANT [5] { value: 2 } + CONSTANT [6] { value: 3 } + } + } + COMPREHENSION [7] { + iter_var: @it:0:0 + iter_range: { + IDENT [8] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [9] { + elements: { + } + } + } + loop_condition: { + CONSTANT [10] { value: true } + } + loop_step: { + CALL [11] { + function: _+_ + args: { + IDENT [12] { + name: @ac:0:0 + } + LIST [13] { + elements: { + COMPREHENSION [14] { + iter_var: @it:1:0 + iter_range: { + IDENT [15] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [16] { + elements: { + } + } + } + loop_condition: { + CONSTANT [17] { value: true } + } + loop_step: { + CALL [18] { + function: _+_ + args: { + IDENT [19] { + name: @ac:1:0 + } + LIST [20] { + elements: { + CALL [21] { + function: _+_ + args: { + IDENT [22] { + name: @it:1:0 + } + CONSTANT [23] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [24] { + name: @ac:1:0 + } + } + } + } + } + } + } + } + result: { + IDENT [25] { + name: @ac:0:0 + } + } + } + } + } + CALL [26] { + function: _==_ + args: { + IDENT [27] { + name: @index1 + } + IDENT [28] { + name: @index1 + } + } + } + } +} +Test case: INCLUSION_LIST +Source: 1 in [1,2,3] && 2 in [1,2,3] && 3 in [3, [1,2,3]] && 1 in [1,2,3] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + CONSTANT [5] { value: 2 } + CONSTANT [6] { value: 3 } + } + } + CALL [7] { + function: @in + args: { + CONSTANT [8] { value: 1 } + IDENT [9] { + name: @index0 + } + } + } + } + } + CALL [10] { + function: _&&_ + args: { + CALL [11] { + function: _&&_ + args: { + IDENT [12] { + name: @index1 + } + CALL [13] { + function: @in + args: { + CONSTANT [14] { value: 2 } + IDENT [15] { + name: @index0 + } + } + } + } + } + CALL [16] { + function: _&&_ + args: { + CALL [17] { + function: @in + args: { + CONSTANT [18] { value: 3 } + LIST [19] { + elements: { + CONSTANT [20] { value: 3 } + IDENT [21] { + name: @index0 + } + } + } + } + } + IDENT [22] { + name: @index1 + } + } + } + } + } + } +} +Test case: INCLUSION_MAP +Source: 2 in {'a': 1, 2: {true: false}, 3: {true: false}} +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + MAP [3] { + MAP_ENTRY [4] { + key: { + CONSTANT [5] { value: true } + } + value: { + CONSTANT [6] { value: false } + } + } + } + } + } + CALL [7] { + function: @in + args: { + CONSTANT [8] { value: 2 } + MAP [9] { + MAP_ENTRY [10] { + key: { + CONSTANT [11] { value: "a" } + } + value: { + CONSTANT [12] { value: 1 } + } + } + MAP_ENTRY [13] { + key: { + CONSTANT [14] { value: 2 } + } + value: { + IDENT [15] { + name: @index0 + } + } + } + MAP_ENTRY [16] { + key: { + CONSTANT [17] { value: 3 } + } + value: { + IDENT [18] { + name: @index0 + } + } + } + } + } + } + } +} +Test case: MACRO_ITER_VAR_NOT_REFERENCED +Source: [1,2].map(i, [1, 2].map(i, [3,4])) == [[[3, 4], [3, 4]], [[3, 4], [3, 4]]] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + CONSTANT [5] { value: 2 } + } + } + LIST [6] { + elements: { + CONSTANT [7] { value: 3 } + CONSTANT [8] { value: 4 } + } + } + LIST [9] { + elements: { + IDENT [10] { + name: @index1 + } + IDENT [11] { + name: @index1 + } + } + } + } + } + CALL [12] { + function: _==_ + args: { + COMPREHENSION [13] { + iter_var: @it:0:0 + iter_range: { + IDENT [14] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [15] { + elements: { + } + } + } + loop_condition: { + CONSTANT [16] { value: true } + } + loop_step: { + CALL [17] { + function: _+_ + args: { + IDENT [18] { + name: @ac:0:0 + } + LIST [19] { + elements: { + COMPREHENSION [20] { + iter_var: @it:1:0 + iter_range: { + IDENT [21] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [22] { + elements: { + } + } + } + loop_condition: { + CONSTANT [23] { value: true } + } + loop_step: { + CALL [24] { + function: _+_ + args: { + IDENT [25] { + name: @ac:1:0 + } + LIST [26] { + elements: { + IDENT [27] { + name: @index1 + } + } + } + } + } + } + result: { + IDENT [28] { + name: @ac:1:0 + } + } + } + } + } + } + } + } + result: { + IDENT [29] { + name: @ac:0:0 + } + } + } + LIST [30] { + elements: { + IDENT [31] { + name: @index2 + } + IDENT [32] { + name: @index2 + } + } + } + } + } + } +} +Test case: MACRO_SHADOWED_VARIABLE +Source: [x - 1 > 3 ? x - 1 : 5].exists(x, x - 1 > 3) || x - 1 > 3 +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + CALL [3] { + function: _-_ + args: { + IDENT [4] { + name: x + } + CONSTANT [5] { value: 1 } + } + } + CALL [6] { + function: _>_ + args: { + IDENT [7] { + name: @index0 + } + CONSTANT [8] { value: 3 } + } + } + } + } + CALL [9] { + function: _||_ + args: { + COMPREHENSION [10] { + iter_var: @it:0:0 + iter_range: { + LIST [11] { + elements: { + CALL [12] { + function: _?_:_ + args: { + IDENT [13] { + name: @index1 + } + IDENT [14] { + name: @index0 + } + CONSTANT [15] { value: 5 } + } + } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [16] { value: false } + } + loop_condition: { + CALL [17] { + function: @not_strictly_false + args: { + CALL [18] { + function: !_ + args: { + IDENT [19] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [20] { + function: _||_ + args: { + IDENT [21] { + name: @ac:0:0 + } + CALL [22] { + function: _>_ + args: { + CALL [23] { + function: _-_ + args: { + IDENT [24] { + name: @it:0:0 + } + CONSTANT [25] { value: 1 } + } + } + CONSTANT [26] { value: 3 } + } + } + } + } + } + result: { + IDENT [27] { + name: @ac:0:0 + } + } + } + IDENT [28] { + name: @index1 + } + } + } + } +} +Test case: MACRO_SHADOWED_VARIABLE_2 +Source: ["foo", "bar"].map(x, [x + x, x + x]).map(x, [x + x, x + x]) +=====> +COMPREHENSION [35] { + iter_var: @it:0:0 + iter_range: { + COMPREHENSION [19] { + iter_var: @it:1:0 + iter_range: { + LIST [1] { + elements: { + CONSTANT [2] { value: "foo" } + CONSTANT [3] { value: "bar" } + } + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [13] { + elements: { + } + } + } + loop_condition: { + CONSTANT [14] { value: true } + } + loop_step: { + CALL [17] { + function: _+_ + args: { + IDENT [15] { + name: @ac:1:0 + } + LIST [16] { + elements: { + LIST [6] { + elements: { + CALL [8] { + function: _+_ + args: { + IDENT [7] { + name: @it:1:0 + } + IDENT [9] { + name: @it:1:0 + } + } + } + CALL [11] { + function: _+_ + args: { + IDENT [10] { + name: @it:1:0 + } + IDENT [12] { + name: @it:1:0 + } + } + } + } + } + } + } + } + } + } + result: { + IDENT [18] { + name: @ac:1:0 + } + } + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [29] { + elements: { + } + } + } + loop_condition: { + CONSTANT [30] { value: true } + } + loop_step: { + CALL [33] { + function: _+_ + args: { + IDENT [31] { + name: @ac:0:0 + } + LIST [32] { + elements: { + LIST [22] { + elements: { + CALL [24] { + function: _+_ + args: { + IDENT [23] { + name: @it:0:0 + } + IDENT [25] { + name: @it:0:0 + } + } + } + CALL [27] { + function: _+_ + args: { + IDENT [26] { + name: @it:0:0 + } + IDENT [28] { + name: @it:0:0 + } + } + } + } + } + } + } + } + } + } + result: { + IDENT [34] { + name: @ac:0:0 + } + } +} +Test case: PRESENCE_TEST +Source: has({'a': true}.a) && {'a':true}['a'] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + MAP [3] { + MAP_ENTRY [4] { + key: { + CONSTANT [5] { value: "a" } + } + value: { + CONSTANT [6] { value: true } + } + } + } + } + } + CALL [7] { + function: _&&_ + args: { + SELECT [8] { + IDENT [9] { + name: @index0 + }.a~presence_test + } + CALL [10] { + function: _[_] + args: { + IDENT [11] { + name: @index0 + } + CONSTANT [12] { value: "a" } + } + } + } + } + } +} +Test case: PRESENCE_TEST_2 +Source: has({'a': true}.a) && has({'a': true}.a) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + SELECT [3] { + MAP [4] { + MAP_ENTRY [5] { + key: { + CONSTANT [6] { value: "a" } + } + value: { + CONSTANT [7] { value: true } + } + } + }.a~presence_test + } + } + } + CALL [8] { + function: _&&_ + args: { + IDENT [9] { + name: @index0 + } + IDENT [10] { + name: @index0 + } + } + } + } +} +Test case: PRESENCE_TEST_WITH_TERNARY +Source: (has(msg.oneof_type.payload) ? msg.oneof_type.payload.single_int64 : 0) == 10 +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + SELECT [3] { + IDENT [4] { + name: msg + }.oneof_type + } + } + } + CALL [5] { + function: _==_ + args: { + CALL [6] { + function: _?_:_ + args: { + SELECT [7] { + IDENT [8] { + name: @index0 + }.payload~presence_test + } + SELECT [9] { + SELECT [10] { + IDENT [11] { + name: @index0 + }.payload + }.single_int64 + } + CONSTANT [12] { value: 0 } + } + } + CONSTANT [13] { value: 10 } + } + } + } +} +Test case: PRESENCE_TEST_WITH_TERNARY_2 +Source: (has(msg.oneof_type.payload) ? msg.oneof_type.payload.single_int64 : msg.oneof_type.payload.single_int64 * 0) == 10 +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + SELECT [3] { + IDENT [4] { + name: msg + }.oneof_type + } + SELECT [5] { + SELECT [6] { + IDENT [7] { + name: @index0 + }.payload + }.single_int64 + } + } + } + CALL [8] { + function: _==_ + args: { + CALL [9] { + function: _?_:_ + args: { + SELECT [10] { + IDENT [11] { + name: @index0 + }.payload~presence_test + } + IDENT [12] { + name: @index1 + } + CALL [13] { + function: _*_ + args: { + IDENT [14] { + name: @index1 + } + CONSTANT [15] { value: 0 } + } + } + } + } + CONSTANT [16] { value: 10 } + } + } + } +} +Test case: PRESENCE_TEST_WITH_TERNARY_3 +Source: (has(msg.oneof_type.payload.single_int64) ? msg.oneof_type.payload.single_int64 : msg.oneof_type.payload.single_int64 * 0) == 10 +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + SELECT [3] { + SELECT [4] { + IDENT [5] { + name: msg + }.oneof_type + }.payload + } + SELECT [6] { + IDENT [7] { + name: @index0 + }.single_int64 + } + } + } + CALL [8] { + function: _==_ + args: { + CALL [9] { + function: _?_:_ + args: { + SELECT [10] { + IDENT [11] { + name: @index0 + }.single_int64~presence_test + } + IDENT [12] { + name: @index1 + } + CALL [13] { + function: _*_ + args: { + IDENT [14] { + name: @index1 + } + CONSTANT [15] { value: 0 } + } + } + } + } + CONSTANT [16] { value: 10 } + } + } + } +} +Test case: PRESENCE_TEST_WITH_TERNARY_NESTED +Source: (has(msg.oneof_type) && has(msg.oneof_type.payload) && has(msg.oneof_type.payload.single_int64)) ? ((has(msg.oneof_type.payload.map_string_string) && has(msg.oneof_type.payload.map_string_string.key)) ? msg.oneof_type.payload.map_string_string.key == 'A' : false) : false +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + SELECT [3] { + IDENT [4] { + name: msg + }.oneof_type + } + SELECT [5] { + IDENT [6] { + name: @index0 + }.payload + } + SELECT [7] { + IDENT [8] { + name: @index1 + }.map_string_string + } + } + } + CALL [9] { + function: _?_:_ + args: { + CALL [10] { + function: _&&_ + args: { + CALL [11] { + function: _&&_ + args: { + SELECT [12] { + IDENT [13] { + name: msg + }.oneof_type~presence_test + } + SELECT [14] { + IDENT [15] { + name: @index0 + }.payload~presence_test + } + } + } + SELECT [16] { + IDENT [17] { + name: @index1 + }.single_int64~presence_test + } + } + } + CALL [18] { + function: _?_:_ + args: { + CALL [19] { + function: _&&_ + args: { + SELECT [20] { + IDENT [21] { + name: @index1 + }.map_string_string~presence_test + } + SELECT [22] { + IDENT [23] { + name: @index2 + }.key~presence_test + } + } + } + CALL [24] { + function: _==_ + args: { + SELECT [25] { + IDENT [26] { + name: @index2 + }.key + } + CONSTANT [27] { value: "A" } + } + } + CONSTANT [28] { value: false } + } + } + CONSTANT [29] { value: false } + } + } + } +} +Test case: OPTIONAL_LIST +Source: [10, ?optional.none(), [?optional.none(), ?opt_x], [?optional.none(), ?opt_x]] == [10, [5], [5]] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + CALL [3] { + function: optional.none + args: { + } + } + LIST [4] { + elements: { + IDENT [5] { + name: @index0 + } + IDENT [6] { + name: opt_x + } + } + optional_indices: [0, 1] + } + LIST [7] { + elements: { + CONSTANT [8] { value: 5 } + } + } + } + } + CALL [9] { + function: _==_ + args: { + LIST [10] { + elements: { + CONSTANT [11] { value: 10 } + IDENT [12] { + name: @index0 + } + IDENT [13] { + name: @index1 + } + IDENT [14] { + name: @index1 + } + } + optional_indices: [0] + } + LIST [15] { + elements: { + CONSTANT [16] { value: 10 } + IDENT [17] { + name: @index2 + } + IDENT [18] { + name: @index2 + } + } + } + } + } + } +} +Test case: OPTIONAL_MAP +Source: {?'hello': optional.of('hello')}['hello'] + {?'hello': optional.of('hello')}['hello'] == 'hellohello' +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + CALL [3] { + function: _[_] + args: { + MAP [4] { + MAP_ENTRY [5] { + key: { + CONSTANT [6] { value: "hello" } + } + optional_entry: true + value: { + CALL [7] { + function: optional.of + args: { + CONSTANT [8] { value: "hello" } + } + } + } + } + } + CONSTANT [9] { value: "hello" } + } + } + } + } + CALL [10] { + function: _==_ + args: { + CALL [11] { + function: _+_ + args: { + IDENT [12] { + name: @index0 + } + IDENT [13] { + name: @index0 + } + } + } + CONSTANT [14] { value: "hellohello" } + } + } + } +} +Test case: OPTIONAL_MAP_CHAINED +Source: {?'key': optional.of('test')}[?'bogus'].or({'key': 'test'}[?'bogus']).orValue({'key': 'test'}['key']) == 'test' +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + MAP [3] { + MAP_ENTRY [4] { + key: { + CONSTANT [5] { value: "key" } + } + value: { + CONSTANT [6] { value: "test" } + } + } + } + } + } + CALL [7] { + function: _==_ + args: { + CALL [8] { + function: orValue + target: { + CALL [9] { + function: or + target: { + CALL [10] { + function: _[?_] + args: { + MAP [11] { + MAP_ENTRY [12] { + key: { + CONSTANT [13] { value: "key" } + } + optional_entry: true + value: { + CALL [14] { + function: optional.of + args: { + CONSTANT [15] { value: "test" } + } + } + } + } + } + CONSTANT [16] { value: "bogus" } + } + } + } + args: { + CALL [17] { + function: _[?_] + args: { + IDENT [18] { + name: @index0 + } + CONSTANT [19] { value: "bogus" } + } + } + } + } + } + args: { + CALL [20] { + function: _[_] + args: { + IDENT [21] { + name: @index0 + } + CONSTANT [22] { value: "key" } + } + } + } + } + CONSTANT [23] { value: "test" } + } + } + } +} +Test case: OPTIONAL_MESSAGE +Source: TestAllTypes{?single_int64: optional.ofNonZeroValue(1), ?single_int32: optional.of(4)}.single_int32 + TestAllTypes{?single_int64: optional.ofNonZeroValue(1), ?single_int32: optional.of(4)}.single_int64 == 5 +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + STRUCT [3] { + name: TestAllTypes + entries: { + ENTRY [4] { + field_key: single_int64 + optional_entry: true + value: { + CALL [5] { + function: optional.ofNonZeroValue + args: { + CONSTANT [6] { value: 1 } + } + } + } + } + ENTRY [7] { + field_key: single_int32 + optional_entry: true + value: { + CALL [8] { + function: optional.of + args: { + CONSTANT [9] { value: 4 } + } + } + } + } + } + } + } + } + CALL [10] { + function: _==_ + args: { + CALL [11] { + function: _+_ + args: { + SELECT [12] { + IDENT [13] { + name: @index0 + }.single_int32 + } + SELECT [14] { + IDENT [15] { + name: @index0 + }.single_int64 + } + } + } + CONSTANT [16] { value: 5 } + } + } + } +} +Test case: CALL +Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('h' + 'e' + 'l' + 'l' + 'o') +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + CALL [3] { + function: _+_ + args: { + CALL [4] { + function: _+_ + args: { + CALL [5] { + function: _+_ + args: { + CALL [6] { + function: _+_ + args: { + CONSTANT [7] { value: "h" } + CONSTANT [8] { value: "e" } + } + } + CONSTANT [9] { value: "l" } + } + } + CONSTANT [10] { value: "l" } + } + } + CONSTANT [11] { value: "o" } + } + } + } + } + CALL [12] { + function: matches + target: { + CALL [13] { + function: _+_ + args: { + IDENT [14] { + name: @index0 + } + CONSTANT [15] { value: " world" } + } + } + } + args: { + IDENT [16] { + name: @index0 + } + } + } + } +} +Test case: CALL_ARGUMENT_NESTED_NO_COMMON_SUBEXPR +Source: 'hello world'.matches('h' + 'e' + 'l' + 'l' + 'o') +=====> +CALL [2] { + function: matches + target: { + CONSTANT [1] { value: "hello world" } + } + args: { + CALL [10] { + function: _+_ + args: { + CALL [8] { + function: _+_ + args: { + CALL [6] { + function: _+_ + args: { + CALL [4] { + function: _+_ + args: { + CONSTANT [3] { value: "h" } + CONSTANT [5] { value: "e" } + } + } + CONSTANT [7] { value: "l" } + } + } + CONSTANT [9] { value: "l" } + } + } + CONSTANT [11] { value: "o" } + } + } + } +} +Test case: CALL_TARGET_NESTED_NO_COMMON_SUBEXPR +Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('hello') +=====> +CALL [12] { + function: matches + target: { + CALL [10] { + function: _+_ + args: { + CALL [8] { + function: _+_ + args: { + CALL [6] { + function: _+_ + args: { + CALL [4] { + function: _+_ + args: { + CALL [2] { + function: _+_ + args: { + CONSTANT [1] { value: "h" } + CONSTANT [3] { value: "e" } + } + } + CONSTANT [5] { value: "l" } + } + } + CONSTANT [7] { value: "l" } + } + } + CONSTANT [9] { value: "o" } + } + } + CONSTANT [11] { value: " world" } + } + } + } + args: { + CONSTANT [13] { value: "hello" } + } +} +Test case: CALL_BOTH_ARGUMENT_TARGET_NESTED_NO_COMMON_SUBEXPR +Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('w' + 'o' + 'r' + 'l' + 'd') +=====> +CALL [12] { + function: matches + target: { + CALL [10] { + function: _+_ + args: { + CALL [8] { + function: _+_ + args: { + CALL [6] { + function: _+_ + args: { + CALL [4] { + function: _+_ + args: { + CALL [2] { + function: _+_ + args: { + CONSTANT [1] { value: "h" } + CONSTANT [3] { value: "e" } + } + } + CONSTANT [5] { value: "l" } + } + } + CONSTANT [7] { value: "l" } + } + } + CONSTANT [9] { value: "o" } + } + } + CONSTANT [11] { value: " world" } + } + } + } + args: { + CALL [20] { + function: _+_ + args: { + CALL [18] { + function: _+_ + args: { + CALL [16] { + function: _+_ + args: { + CALL [14] { + function: _+_ + args: { + CONSTANT [13] { value: "w" } + CONSTANT [15] { value: "o" } + } + } + CONSTANT [17] { value: "r" } + } + } + CONSTANT [19] { value: "l" } + } + } + CONSTANT [21] { value: "d" } + } + } + } +} +Test case: CUSTOM_FUNCTION_INELIMINABLE +Source: non_pure_custom_func(msg.oneof_type.payload.single_int64) + non_pure_custom_func(msg.oneof_type.payload.single_int32) + non_pure_custom_func(msg.oneof_type.payload.single_int64) + non_pure_custom_func(msg.single_int64) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + SELECT [3] { + SELECT [4] { + IDENT [5] { + name: msg + }.oneof_type + }.payload + } + SELECT [6] { + IDENT [7] { + name: @index0 + }.single_int64 + } + } + } + CALL [8] { + function: _+_ + args: { + CALL [9] { + function: _+_ + args: { + CALL [10] { + function: _+_ + args: { + CALL [11] { + function: non_pure_custom_func + args: { + IDENT [12] { + name: @index1 + } + } + } + CALL [13] { + function: non_pure_custom_func + args: { + SELECT [14] { + IDENT [15] { + name: @index0 + }.single_int32 + } + } + } + } + } + CALL [16] { + function: non_pure_custom_func + args: { + IDENT [17] { + name: @index1 + } + } + } + } + } + CALL [18] { + function: non_pure_custom_func + args: { + SELECT [19] { + IDENT [20] { + name: msg + }.single_int64 + } + } + } + } + } + } +} +Test case: CUSTOM_FUNCTION_ELIMINABLE +Source: pure_custom_func(msg.oneof_type.payload.single_int64) + pure_custom_func(msg.oneof_type.payload.single_int32) + pure_custom_func(msg.oneof_type.payload.single_int64) + pure_custom_func(msg.single_int64) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + SELECT [3] { + SELECT [4] { + IDENT [5] { + name: msg + }.oneof_type + }.payload + } + CALL [6] { + function: pure_custom_func + args: { + SELECT [7] { + IDENT [8] { + name: @index0 + }.single_int64 + } + } + } + } + } + CALL [9] { + function: _+_ + args: { + CALL [10] { + function: _+_ + args: { + CALL [11] { + function: _+_ + args: { + IDENT [12] { + name: @index1 + } + CALL [13] { + function: pure_custom_func + args: { + SELECT [14] { + IDENT [15] { + name: @index0 + }.single_int32 + } + } + } + } + } + IDENT [16] { + name: @index1 + } + } + } + CALL [17] { + function: pure_custom_func + args: { + SELECT [18] { + IDENT [19] { + name: msg + }.single_int64 + } + } + } + } + } + } +} \ No newline at end of file diff --git a/optimizer/src/test/resources/subexpression_ast_block_common_subexpr_only.baseline b/optimizer/src/test/resources/subexpression_ast_block_common_subexpr_only.baseline index 960821d8a..9782b5dc2 100644 --- a/optimizer/src/test/resources/subexpression_ast_block_common_subexpr_only.baseline +++ b/optimizer/src/test/resources/subexpression_ast_block_common_subexpr_only.baseline @@ -4,12 +4,12 @@ Source: size([1,2]) + size([1,2]) + 1 == 5 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { function: size args: { - CREATE_LIST [4] { + LIST [4] { elements: { CONSTANT [5] { value: 1 } CONSTANT [6] { value: 2 } @@ -50,12 +50,12 @@ Source: 2 + size([1,2]) + size([1,2]) + 1 == 7 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { function: size args: { - CREATE_LIST [4] { + LIST [4] { elements: { CONSTANT [5] { value: 1 } CONSTANT [6] { value: 2 } @@ -102,12 +102,12 @@ Source: size([0]) + size([0]) + size([1,2]) + size([1,2]) == 6 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { function: size args: { - CREATE_LIST [4] { + LIST [4] { elements: { CONSTANT [5] { value: 0 } } @@ -117,7 +117,7 @@ CALL [1] { CALL [6] { function: size args: { - CREATE_LIST [7] { + LIST [7] { elements: { CONSTANT [8] { value: 1 } CONSTANT [9] { value: 2 } @@ -168,12 +168,12 @@ Source: 5 + size([0]) + size([0]) + size([1,2]) + size([1,2]) + size([1,2,3]) + CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { function: size args: { - CREATE_LIST [4] { + LIST [4] { elements: { CONSTANT [5] { value: 0 } } @@ -183,7 +183,7 @@ CALL [1] { CALL [6] { function: size args: { - CREATE_LIST [7] { + LIST [7] { elements: { CONSTANT [8] { value: 1 } CONSTANT [9] { value: 2 } @@ -194,7 +194,7 @@ CALL [1] { CALL [10] { function: size args: { - CREATE_LIST [11] { + LIST [11] { elements: { CONSTANT [12] { value: 1 } CONSTANT [13] { value: 2 } @@ -268,7 +268,7 @@ Source: timestamp(int(timestamp(1000000000))).getFullYear() + timestamp(int(time CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { function: getFullYear @@ -459,12 +459,12 @@ Source: {"a": 2}["a"] + {"a": 2}["a"] * {"a": 2}["a"] == 6 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { function: _[_] args: { - CREATE_MAP [4] { + MAP [4] { MAP_ENTRY [5] { key: { CONSTANT [6] { value: "a" } @@ -512,9 +512,9 @@ Source: {'a': {'b': 1}, 'c': {'b': 1}, 'd': {'e': {'b': 1}}, 'e': {'e': {'b': 1} CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_MAP [3] { + MAP [3] { MAP_ENTRY [4] { key: { CONSTANT [5] { value: "b" } @@ -524,7 +524,7 @@ CALL [1] { } } } - CREATE_MAP [7] { + MAP [7] { MAP_ENTRY [8] { key: { CONSTANT [9] { value: "e" } @@ -538,7 +538,7 @@ CALL [1] { } } } - CREATE_MAP [11] { + MAP [11] { MAP_ENTRY [12] { key: { CONSTANT [13] { value: "a" } @@ -588,9 +588,9 @@ Source: [1, [1,2,3,4], 2, [1,2,3,4], 5, [1,2,3,4], 7, [[1,2], [1,2,3,4]], [1,2]] CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { + LIST [3] { elements: { CONSTANT [4] { value: 1 } CONSTANT [5] { value: 2 } @@ -598,7 +598,7 @@ CALL [1] { CONSTANT [7] { value: 4 } } } - CREATE_LIST [8] { + LIST [8] { elements: { CONSTANT [9] { value: 1 } CONSTANT [10] { value: 2 } @@ -606,7 +606,7 @@ CALL [1] { } } } - CREATE_LIST [11] { + LIST [11] { elements: { CONSTANT [12] { value: 1 } IDENT [13] { @@ -621,7 +621,7 @@ CALL [1] { name: @index0 } CONSTANT [18] { value: 7 } - CREATE_LIST [19] { + LIST [19] { elements: { IDENT [20] { name: @index1 @@ -644,7 +644,7 @@ Source: msg.single_int64 + msg.single_int64 == 6 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { @@ -678,7 +678,7 @@ Source: msg.oneof_type.payload.single_int64 + msg.oneof_type.payload.single_int3 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { SELECT [4] { @@ -753,7 +753,7 @@ Source: true || msg.oneof_type.payload.oneof_type.payload.oneof_type.payload.one CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { SELECT [4] { @@ -811,7 +811,7 @@ Source: msg.oneof_type.payload.map_int32_int64[1] + msg.oneof_type.payload.map_i CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { function: _[_] @@ -863,7 +863,7 @@ Source: msg.oneof_type.payload.map_int32_int64[0] + msg.oneof_type.payload.map_i CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { SELECT [4] { @@ -951,7 +951,7 @@ Source: (msg.single_int64 > 0 ? msg.single_int64 : 0) == 3 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { @@ -992,7 +992,7 @@ Source: false ? false : (msg.single_int64) + ((msg.single_int64 + 1) * 2) == 11 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { @@ -1045,7 +1045,7 @@ Source: (msg.single_int64 > 0 ? (msg.single_int32 > 0 ? msg.single_int64 + msg.s CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { @@ -1114,165 +1114,171 @@ Source: size([[1].exists(i, i > 0)]) + size([[1].exists(j, j > 0)]) + size([[2]. CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CALL [3] { - function: size - args: { - CREATE_LIST [4] { + COMPREHENSION [3] { + iter_var: @it:0:0 + iter_range: { + LIST [4] { elements: { - COMPREHENSION [5] { - iter_var: @c0:0 - iter_range: { - CREATE_LIST [6] { - elements: { - CONSTANT [7] { value: 1 } - } - } - } - accu_var: @x0:0 - accu_init: { - CONSTANT [8] { value: false } - } - loop_condition: { - CALL [9] { - function: @not_strictly_false - args: { - CALL [10] { - function: !_ - args: { - IDENT [11] { - name: @x0:0 - } - } - } - } - } - } - loop_step: { - CALL [12] { - function: _||_ - args: { - IDENT [13] { - name: @x0:0 - } - CALL [14] { - function: _>_ - args: { - IDENT [15] { - name: @c0:0 - } - CONSTANT [16] { value: 0 } - } - } - } + CONSTANT [5] { value: 1 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [6] { value: false } + } + loop_condition: { + CALL [7] { + function: @not_strictly_false + args: { + CALL [8] { + function: !_ + args: { + IDENT [9] { + name: @ac:0:0 } } - result: { - IDENT [17] { - name: @x0:0 + } + } + } + } + loop_step: { + CALL [10] { + function: _||_ + args: { + IDENT [11] { + name: @ac:0:0 + } + CALL [12] { + function: _>_ + args: { + IDENT [13] { + name: @it:0:0 } + CONSTANT [14] { value: 0 } } } } } } + result: { + IDENT [15] { + name: @ac:0:0 + } + } } - CALL [18] { + CALL [16] { function: size args: { - CREATE_LIST [19] { + LIST [17] { elements: { - COMPREHENSION [20] { - iter_var: @c0:0 - iter_range: { - CREATE_LIST [21] { - elements: { - CONSTANT [22] { value: 2 } - } - } - } - accu_var: @x0:0 - accu_init: { - CONSTANT [23] { value: false } - } - loop_condition: { - CALL [24] { - function: @not_strictly_false - args: { - CALL [25] { - function: !_ - args: { - IDENT [26] { - name: @x0:0 - } - } - } - } - } - } - loop_step: { - CALL [27] { - function: _||_ - args: { - IDENT [28] { - name: @x0:0 - } - CALL [29] { - function: _>_ - args: { - IDENT [30] { - name: @c0:0 - } - CONSTANT [31] { value: 1 } - } - } - } + IDENT [18] { + name: @index0 + } + } + } + } + } + COMPREHENSION [19] { + iter_var: @it:0:0 + iter_range: { + LIST [20] { + elements: { + CONSTANT [21] { value: 2 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [22] { value: false } + } + loop_condition: { + CALL [23] { + function: @not_strictly_false + args: { + CALL [24] { + function: !_ + args: { + IDENT [25] { + name: @ac:0:0 } } - result: { - IDENT [32] { - name: @x0:0 + } + } + } + } + loop_step: { + CALL [26] { + function: _||_ + args: { + IDENT [27] { + name: @ac:0:0 + } + CALL [28] { + function: _>_ + args: { + IDENT [29] { + name: @it:0:0 } + CONSTANT [30] { value: 1 } } } } } } + result: { + IDENT [31] { + name: @ac:0:0 + } + } + } + CALL [32] { + function: size + args: { + LIST [33] { + elements: { + IDENT [34] { + name: @index2 + } + } + } + } } } } - CALL [33] { + CALL [35] { function: _==_ args: { - CALL [34] { + CALL [36] { function: _+_ args: { - CALL [35] { + CALL [37] { function: _+_ args: { - CALL [36] { + CALL [38] { function: _+_ args: { - IDENT [37] { - name: @index0 + IDENT [39] { + name: @index1 } - IDENT [38] { - name: @index0 + IDENT [40] { + name: @index1 } } } - IDENT [39] { - name: @index1 + IDENT [41] { + name: @index3 } } } - IDENT [40] { - name: @index1 + IDENT [42] { + name: @index3 } } } - CONSTANT [41] { value: 4 } + CONSTANT [43] { value: 4 } } } } @@ -1283,252 +1289,1527 @@ Source: [[1].exists(i, i > 0)] + [[1].exists(j, j > 0)] + [['a'].exists(k, k == CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - COMPREHENSION [4] { - iter_var: @c0:0 - iter_range: { - CREATE_LIST [5] { - elements: { - CONSTANT [6] { value: 1 } - } - } - } - accu_var: @x0:0 - accu_init: { - CONSTANT [7] { value: false } + COMPREHENSION [3] { + iter_var: @it:0:0 + iter_range: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } } - loop_condition: { + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [6] { value: false } + } + loop_condition: { + CALL [7] { + function: @not_strictly_false + args: { CALL [8] { - function: @not_strictly_false + function: !_ args: { - CALL [9] { - function: !_ - args: { - IDENT [10] { - name: @x0:0 - } - } + IDENT [9] { + name: @ac:0:0 } } } } - loop_step: { - CALL [11] { - function: _||_ + } + } + loop_step: { + CALL [10] { + function: _||_ + args: { + IDENT [11] { + name: @ac:0:0 + } + CALL [12] { + function: _>_ args: { - IDENT [12] { - name: @x0:0 - } - CALL [13] { - function: _>_ - args: { - IDENT [14] { - name: @c0:0 - } - CONSTANT [15] { value: 0 } - } + IDENT [13] { + name: @it:0:0 } + CONSTANT [14] { value: 0 } } } } - result: { - IDENT [16] { - name: @x0:0 - } - } + } + } + result: { + IDENT [15] { + name: @ac:0:0 } } } - CREATE_LIST [17] { + LIST [16] { elements: { - COMPREHENSION [18] { - iter_var: @c0:1 - iter_range: { - CREATE_LIST [19] { - elements: { - CONSTANT [20] { value: "a" } - } - } - } - accu_var: @x0:1 - accu_init: { - CONSTANT [21] { value: false } + IDENT [17] { + name: @index0 + } + } + } + COMPREHENSION [18] { + iter_var: @it:0:1 + iter_range: { + LIST [19] { + elements: { + CONSTANT [20] { value: "a" } } - loop_condition: { - CALL [22] { - function: @not_strictly_false + } + } + accu_var: @ac:0:1 + accu_init: { + CONSTANT [21] { value: false } + } + loop_condition: { + CALL [22] { + function: @not_strictly_false + args: { + CALL [23] { + function: !_ args: { - CALL [23] { - function: !_ - args: { - IDENT [24] { - name: @x0:1 - } - } + IDENT [24] { + name: @ac:0:1 } } } } - loop_step: { - CALL [25] { - function: _||_ + } + } + loop_step: { + CALL [25] { + function: _||_ + args: { + IDENT [26] { + name: @ac:0:1 + } + CALL [27] { + function: _==_ args: { - IDENT [26] { - name: @x0:1 - } - CALL [27] { - function: _==_ - args: { - IDENT [28] { - name: @c0:1 - } - CONSTANT [29] { value: "a" } - } + IDENT [28] { + name: @it:0:1 } + CONSTANT [29] { value: "a" } } } } - result: { - IDENT [30] { - name: @x0:1 - } - } + } + } + result: { + IDENT [30] { + name: @ac:0:1 + } + } + } + LIST [31] { + elements: { + IDENT [32] { + name: @index2 } } } } } - CALL [31] { + CALL [33] { function: _==_ args: { - CALL [32] { + CALL [34] { function: _+_ args: { - CALL [33] { + CALL [35] { function: _+_ args: { - CALL [34] { + CALL [36] { function: _+_ args: { - IDENT [35] { - name: @index0 + IDENT [37] { + name: @index1 } - IDENT [36] { - name: @index0 + IDENT [38] { + name: @index1 } } } - IDENT [37] { - name: @index1 + IDENT [39] { + name: @index3 } } } - IDENT [38] { - name: @index1 + IDENT [40] { + name: @index3 } } } - CREATE_LIST [39] { + LIST [41] { elements: { - CONSTANT [40] { value: true } - CONSTANT [41] { value: true } CONSTANT [42] { value: true } CONSTANT [43] { value: true } + CONSTANT [44] { value: true } + CONSTANT [45] { value: true } } } } } } } -Test case: NESTED_MACROS -Source: [1,2,3].map(i, [1, 2, 3].map(i, i + 1)) == [[2, 3, 4], [2, 3, 4], [2, 3, 4]] +Test case: MULTIPLE_MACROS_3 +Source: [1].exists(i, i > 0) && [1].exists(j, j > 0) && [1].exists(k, k > 1) && [2].exists(l, l > 1) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + COMPREHENSION [3] { + iter_var: @it:0:0 + iter_range: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [6] { value: false } + } + loop_condition: { + CALL [7] { + function: @not_strictly_false + args: { + CALL [8] { + function: !_ + args: { + IDENT [9] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [10] { + function: _||_ + args: { + IDENT [11] { + name: @ac:0:0 + } + CALL [12] { + function: _>_ + args: { + IDENT [13] { + name: @it:0:0 + } + CONSTANT [14] { value: 0 } + } + } + } + } + } + result: { + IDENT [15] { + name: @ac:0:0 + } + } + } + } + } + CALL [16] { + function: _&&_ + args: { + CALL [17] { + function: _&&_ + args: { + IDENT [18] { + name: @index0 + } + IDENT [19] { + name: @index0 + } + } + } + CALL [20] { + function: _&&_ + args: { + COMPREHENSION [21] { + iter_var: @it:0:0 + iter_range: { + LIST [22] { + elements: { + CONSTANT [23] { value: 1 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [24] { value: false } + } + loop_condition: { + CALL [25] { + function: @not_strictly_false + args: { + CALL [26] { + function: !_ + args: { + IDENT [27] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [28] { + function: _||_ + args: { + IDENT [29] { + name: @ac:0:0 + } + CALL [30] { + function: _>_ + args: { + IDENT [31] { + name: @it:0:0 + } + CONSTANT [32] { value: 1 } + } + } + } + } + } + result: { + IDENT [33] { + name: @ac:0:0 + } + } + } + COMPREHENSION [34] { + iter_var: @it:0:0 + iter_range: { + LIST [35] { + elements: { + CONSTANT [36] { value: 2 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [37] { value: false } + } + loop_condition: { + CALL [38] { + function: @not_strictly_false + args: { + CALL [39] { + function: !_ + args: { + IDENT [40] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [41] { + function: _||_ + args: { + IDENT [42] { + name: @ac:0:0 + } + CALL [43] { + function: _>_ + args: { + IDENT [44] { + name: @it:0:0 + } + CONSTANT [45] { value: 1 } + } + } + } + } + } + result: { + IDENT [46] { + name: @ac:0:0 + } + } + } + } + } + } + } + } +} +Test case: MULTIPLE_MACROS_COMP_V2_1 +Source: size([[1].exists(i,x,i > 0 && x >= 0)]) + size([[1].exists(j, y, j > 0 && y >= 0)]) + size([[2].exists(k,z,k > 1 && z >= 0)]) + size([[2].exists(l, w, l > 1 && w >= 0)]) == 4 +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + COMPREHENSION [3] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [6] { value: false } + } + loop_condition: { + CALL [7] { + function: @not_strictly_false + args: { + CALL [8] { + function: !_ + args: { + IDENT [9] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [10] { + function: _||_ + args: { + IDENT [11] { + name: @ac:0:0 + } + CALL [12] { + function: _&&_ + args: { + CALL [13] { + function: _>_ + args: { + IDENT [14] { + name: @it:0:0 + } + CONSTANT [15] { value: 0 } + } + } + CALL [16] { + function: _>=_ + args: { + IDENT [17] { + name: @it2:0:0 + } + CONSTANT [18] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [19] { + name: @ac:0:0 + } + } + } + CALL [20] { + function: size + args: { + LIST [21] { + elements: { + IDENT [22] { + name: @index0 + } + } + } + } + } + COMPREHENSION [23] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + LIST [24] { + elements: { + CONSTANT [25] { value: 2 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [26] { value: false } + } + loop_condition: { + CALL [27] { + function: @not_strictly_false + args: { + CALL [28] { + function: !_ + args: { + IDENT [29] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [30] { + function: _||_ + args: { + IDENT [31] { + name: @ac:0:0 + } + CALL [32] { + function: _&&_ + args: { + CALL [33] { + function: _>_ + args: { + IDENT [34] { + name: @it:0:0 + } + CONSTANT [35] { value: 1 } + } + } + CALL [36] { + function: _>=_ + args: { + IDENT [37] { + name: @it2:0:0 + } + CONSTANT [38] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [39] { + name: @ac:0:0 + } + } + } + CALL [40] { + function: size + args: { + LIST [41] { + elements: { + IDENT [42] { + name: @index2 + } + } + } + } + } + } + } + CALL [43] { + function: _==_ + args: { + CALL [44] { + function: _+_ + args: { + CALL [45] { + function: _+_ + args: { + CALL [46] { + function: _+_ + args: { + IDENT [47] { + name: @index1 + } + IDENT [48] { + name: @index1 + } + } + } + IDENT [49] { + name: @index3 + } + } + } + IDENT [50] { + name: @index3 + } + } + } + CONSTANT [51] { value: 4 } + } + } + } +} +Test case: MULTIPLE_MACROS_COMP_V2_2 +Source: [1].exists(i, x, i > 0 && x > 0) && [1].exists(j, y, j > 0 && y > 0) && [1].exists(k, z, k > 1 && z > 0) && [2].exists(l, w, l > 1 && w > 0) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + COMPREHENSION [3] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [6] { value: false } + } + loop_condition: { + CALL [7] { + function: @not_strictly_false + args: { + CALL [8] { + function: !_ + args: { + IDENT [9] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [10] { + function: _||_ + args: { + IDENT [11] { + name: @ac:0:0 + } + CALL [12] { + function: _&&_ + args: { + CALL [13] { + function: _>_ + args: { + IDENT [14] { + name: @it:0:0 + } + CONSTANT [15] { value: 0 } + } + } + CALL [16] { + function: _>_ + args: { + IDENT [17] { + name: @it2:0:0 + } + CONSTANT [18] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [19] { + name: @ac:0:0 + } + } + } + } + } + CALL [20] { + function: _&&_ + args: { + CALL [21] { + function: _&&_ + args: { + IDENT [22] { + name: @index0 + } + IDENT [23] { + name: @index0 + } + } + } + CALL [24] { + function: _&&_ + args: { + COMPREHENSION [25] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + LIST [26] { + elements: { + CONSTANT [27] { value: 1 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [28] { value: false } + } + loop_condition: { + CALL [29] { + function: @not_strictly_false + args: { + CALL [30] { + function: !_ + args: { + IDENT [31] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [32] { + function: _||_ + args: { + IDENT [33] { + name: @ac:0:0 + } + CALL [34] { + function: _&&_ + args: { + CALL [35] { + function: _>_ + args: { + IDENT [36] { + name: @it:0:0 + } + CONSTANT [37] { value: 1 } + } + } + CALL [38] { + function: _>_ + args: { + IDENT [39] { + name: @it2:0:0 + } + CONSTANT [40] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [41] { + name: @ac:0:0 + } + } + } + COMPREHENSION [42] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + LIST [43] { + elements: { + CONSTANT [44] { value: 2 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [45] { value: false } + } + loop_condition: { + CALL [46] { + function: @not_strictly_false + args: { + CALL [47] { + function: !_ + args: { + IDENT [48] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [49] { + function: _||_ + args: { + IDENT [50] { + name: @ac:0:0 + } + CALL [51] { + function: _&&_ + args: { + CALL [52] { + function: _>_ + args: { + IDENT [53] { + name: @it:0:0 + } + CONSTANT [54] { value: 1 } + } + } + CALL [55] { + function: _>_ + args: { + IDENT [56] { + name: @it2:0:0 + } + CONSTANT [57] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [58] { + name: @ac:0:0 + } + } + } + } + } + } + } + } +} +Test case: NESTED_MACROS +Source: [1,2,3].map(i, [1, 2, 3].map(i, i + 1)) == [[2, 3, 4], [2, 3, 4], [2, 3, 4]] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + CONSTANT [5] { value: 2 } + CONSTANT [6] { value: 3 } + } + } + LIST [7] { + elements: { + CONSTANT [8] { value: 2 } + CONSTANT [9] { value: 3 } + CONSTANT [10] { value: 4 } + } + } + } + } + CALL [11] { + function: _==_ + args: { + COMPREHENSION [12] { + iter_var: @it:1:0 + iter_range: { + IDENT [13] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [14] { + elements: { + } + } + } + loop_condition: { + CONSTANT [15] { value: true } + } + loop_step: { + CALL [16] { + function: _+_ + args: { + IDENT [17] { + name: @ac:1:0 + } + LIST [18] { + elements: { + COMPREHENSION [19] { + iter_var: @it:0:0 + iter_range: { + IDENT [20] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [21] { + elements: { + } + } + } + loop_condition: { + CONSTANT [22] { value: true } + } + loop_step: { + CALL [23] { + function: _+_ + args: { + IDENT [24] { + name: @ac:0:0 + } + LIST [25] { + elements: { + CALL [26] { + function: _+_ + args: { + IDENT [27] { + name: @it:0:0 + } + CONSTANT [28] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [29] { + name: @ac:0:0 + } + } + } + } + } + } + } + } + result: { + IDENT [30] { + name: @ac:1:0 + } + } + } + LIST [31] { + elements: { + IDENT [32] { + name: @index1 + } + IDENT [33] { + name: @index1 + } + IDENT [34] { + name: @index1 + } + } + } + } + } + } +} +Test case: NESTED_MACROS_2 +Source: [1, 2].map(y, [1, 2, 3].filter(x, x == y)) == [[1], [2]] +=====> +CALL [31] { + function: _==_ + args: { + COMPREHENSION [30] { + iter_var: y + iter_range: { + LIST [1] { + elements: { + CONSTANT [2] { value: 1 } + CONSTANT [3] { value: 2 } + } + } + } + accu_var: @result + accu_init: { + LIST [24] { + elements: { + } + } + } + loop_condition: { + CONSTANT [25] { value: true } + } + loop_step: { + CALL [28] { + function: _+_ + args: { + IDENT [26] { + name: @result + } + LIST [27] { + elements: { + COMPREHENSION [23] { + iter_var: x + iter_range: { + LIST [6] { + elements: { + CONSTANT [7] { value: 1 } + CONSTANT [8] { value: 2 } + CONSTANT [9] { value: 3 } + } + } + } + accu_var: @result + accu_init: { + LIST [15] { + elements: { + } + } + } + loop_condition: { + CONSTANT [16] { value: true } + } + loop_step: { + CALL [21] { + function: _?_:_ + args: { + CALL [13] { + function: _==_ + args: { + IDENT [12] { + name: x + } + IDENT [14] { + name: y + } + } + } + CALL [19] { + function: _+_ + args: { + IDENT [17] { + name: @result + } + LIST [18] { + elements: { + IDENT [11] { + name: x + } + } + } + } + } + IDENT [20] { + name: @result + } + } + } + } + result: { + IDENT [22] { + name: @result + } + } + } + } + } + } + } + } + result: { + IDENT [29] { + name: @result + } + } + } + LIST [32] { + elements: { + LIST [33] { + elements: { + CONSTANT [34] { value: 1 } + } + } + LIST [35] { + elements: { + CONSTANT [36] { value: 2 } + } + } + } + } + } +} +Test case: NESTED_MACROS_3 +Source: [1,2,3].map(i, i * 2) + [1,2,3].map(i, i * 2).map(i, i * 2) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + COMPREHENSION [3] { + iter_var: @it:0:0 + iter_range: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + CONSTANT [6] { value: 2 } + CONSTANT [7] { value: 3 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [8] { + elements: { + } + } + } + loop_condition: { + CONSTANT [9] { value: true } + } + loop_step: { + CALL [10] { + function: _+_ + args: { + IDENT [11] { + name: @ac:0:0 + } + LIST [12] { + elements: { + CALL [13] { + function: _*_ + args: { + IDENT [14] { + name: @it:0:0 + } + CONSTANT [15] { value: 2 } + } + } + } + } + } + } + } + result: { + IDENT [16] { + name: @ac:0:0 + } + } + } + } + } + CALL [17] { + function: _+_ + args: { + IDENT [18] { + name: @index0 + } + COMPREHENSION [19] { + iter_var: @it:1:0 + iter_range: { + IDENT [20] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [21] { + elements: { + } + } + } + loop_condition: { + CONSTANT [22] { value: true } + } + loop_step: { + CALL [23] { + function: _+_ + args: { + IDENT [24] { + name: @ac:1:0 + } + LIST [25] { + elements: { + CALL [26] { + function: _*_ + args: { + IDENT [27] { + name: @it:1:0 + } + CONSTANT [28] { value: 2 } + } + } + } + } + } + } + } + result: { + IDENT [29] { + name: @ac:1:0 + } + } + } + } + } + } +} +Test case: NESTED_MACROS_COMP_V2_1 +Source: [1,2,3].transformList(i, v, [1, 2, 3].transformList(i, v, i + v + 1)) == [[2, 4, 6], [2, 4, 6], [2, 4, 6]] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + CONSTANT [5] { value: 2 } + CONSTANT [6] { value: 3 } + } + } + LIST [7] { + elements: { + CONSTANT [8] { value: 2 } + CONSTANT [9] { value: 4 } + CONSTANT [10] { value: 6 } + } + } + } + } + CALL [11] { + function: _==_ + args: { + COMPREHENSION [12] { + iter_var: @it:1:0 + iter_var2: @it2:1:0 + iter_range: { + IDENT [13] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [14] { + elements: { + } + } + } + loop_condition: { + CONSTANT [15] { value: true } + } + loop_step: { + CALL [16] { + function: _+_ + args: { + IDENT [17] { + name: @ac:1:0 + } + LIST [18] { + elements: { + COMPREHENSION [19] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + IDENT [20] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [21] { + elements: { + } + } + } + loop_condition: { + CONSTANT [22] { value: true } + } + loop_step: { + CALL [23] { + function: _+_ + args: { + IDENT [24] { + name: @ac:0:0 + } + LIST [25] { + elements: { + CALL [26] { + function: _+_ + args: { + CALL [27] { + function: _+_ + args: { + IDENT [28] { + name: @it:0:0 + } + IDENT [29] { + name: @it2:0:0 + } + } + } + CONSTANT [30] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [31] { + name: @ac:0:0 + } + } + } + } + } + } + } + } + result: { + IDENT [32] { + name: @ac:1:0 + } + } + } + LIST [33] { + elements: { + IDENT [34] { + name: @index1 + } + IDENT [35] { + name: @index1 + } + IDENT [36] { + name: @index1 + } + } + } + } + } + } +} +Test case: NESTED_MACROS_COMP_V2_2 +Source: [1, 2].transformList(i, y, [1, 2, 3].filter(x, x == y && i < y)) == [[1], [2]] +=====> +CALL [36] { + function: _==_ + args: { + COMPREHENSION [35] { + iter_var: i + iter_var2: y + iter_range: { + LIST [1] { + elements: { + CONSTANT [2] { value: 1 } + CONSTANT [3] { value: 2 } + } + } + } + accu_var: @result + accu_init: { + LIST [29] { + elements: { + } + } + } + loop_condition: { + CONSTANT [30] { value: true } + } + loop_step: { + CALL [33] { + function: _+_ + args: { + IDENT [31] { + name: @result + } + LIST [32] { + elements: { + COMPREHENSION [28] { + iter_var: x + iter_range: { + LIST [7] { + elements: { + CONSTANT [8] { value: 1 } + CONSTANT [9] { value: 2 } + CONSTANT [10] { value: 3 } + } + } + } + accu_var: @result + accu_init: { + LIST [20] { + elements: { + } + } + } + loop_condition: { + CONSTANT [21] { value: true } + } + loop_step: { + CALL [26] { + function: _?_:_ + args: { + CALL [16] { + function: _&&_ + args: { + CALL [14] { + function: _==_ + args: { + IDENT [13] { + name: x + } + IDENT [15] { + name: y + } + } + } + CALL [18] { + function: _<_ + args: { + IDENT [17] { + name: i + } + IDENT [19] { + name: y + } + } + } + } + } + CALL [24] { + function: _+_ + args: { + IDENT [22] { + name: @result + } + LIST [23] { + elements: { + IDENT [12] { + name: x + } + } + } + } + } + IDENT [25] { + name: @result + } + } + } + } + result: { + IDENT [27] { + name: @result + } + } + } + } + } + } + } + } + result: { + IDENT [34] { + name: @result + } + } + } + LIST [37] { + elements: { + LIST [38] { + elements: { + CONSTANT [39] { value: 1 } + } + } + LIST [40] { + elements: { + CONSTANT [41] { value: 2 } + } + } + } + } + } +} +Test case: ADJACENT_NESTED_MACROS +Source: [1,2,3].map(i, [1, 2, 3].map(i, i + 1)) == [1,2,3].map(j, [1, 2, 3].map(j, j + 1)) =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { + LIST [3] { elements: { CONSTANT [4] { value: 1 } CONSTANT [5] { value: 2 } CONSTANT [6] { value: 3 } } } - CREATE_LIST [7] { - elements: { - CONSTANT [8] { value: 2 } - CONSTANT [9] { value: 3 } - CONSTANT [10] { value: 4 } - } - } - } - } - CALL [11] { - function: _==_ - args: { - COMPREHENSION [12] { - iter_var: @c0:0 + COMPREHENSION [7] { + iter_var: @it:1:0 iter_range: { - IDENT [13] { + IDENT [8] { name: @index0 } } - accu_var: @x0:0 + accu_var: @ac:1:0 accu_init: { - CREATE_LIST [14] { + LIST [9] { elements: { } } } loop_condition: { - CONSTANT [15] { value: true } + CONSTANT [10] { value: true } } loop_step: { - CALL [16] { + CALL [11] { function: _+_ args: { - IDENT [17] { - name: @x0:0 + IDENT [12] { + name: @ac:1:0 } - CREATE_LIST [18] { + LIST [13] { elements: { - COMPREHENSION [19] { - iter_var: @c1:0 + COMPREHENSION [14] { + iter_var: @it:0:0 iter_range: { - IDENT [20] { + IDENT [15] { name: @index0 } } - accu_var: @x1:0 + accu_var: @ac:0:0 accu_init: { - CREATE_LIST [21] { + LIST [16] { elements: { } } } loop_condition: { - CONSTANT [22] { value: true } + CONSTANT [17] { value: true } } loop_step: { - CALL [23] { + CALL [18] { function: _+_ args: { - IDENT [24] { - name: @x1:0 + IDENT [19] { + name: @ac:0:0 } - CREATE_LIST [25] { + LIST [20] { elements: { - CALL [26] { + CALL [21] { function: _+_ args: { - IDENT [27] { - name: @c1:0 + IDENT [22] { + name: @it:0:0 } - CONSTANT [28] { value: 1 } + CONSTANT [23] { value: 1 } } } } @@ -1537,8 +2818,8 @@ CALL [1] { } } result: { - IDENT [29] { - name: @x1:0 + IDENT [24] { + name: @ac:0:0 } } } @@ -1548,148 +2829,140 @@ CALL [1] { } } result: { - IDENT [30] { - name: @x0:0 + IDENT [25] { + name: @ac:1:0 } } } - CREATE_LIST [31] { - elements: { - IDENT [32] { - name: @index1 - } - IDENT [33] { - name: @index1 - } - IDENT [34] { - name: @index1 - } - } + } + } + CALL [26] { + function: _==_ + args: { + IDENT [27] { + name: @index1 + } + IDENT [28] { + name: @index1 } } } } } -Test case: NESTED_MACROS_2 -Source: [1, 2].map(y, [1, 2, 3].filter(x, x == y)) == [[1], [2]] +Test case: ADJACENT_NESTED_MACROS_COMP_V2 +Source: [1,2,3].transformMap(i, x, [1, 2, 3].transformMap(i, x, i + x + 1)) == [1,2,3].transformMap(j, y, [1, 2, 3].transformMap(j, y, j + y + 1)) =====> -CALL [31] { - function: _==_ +CALL [1] { + function: cel.@block args: { - COMPREHENSION [30] { - iter_var: @c0:0 - iter_range: { - CREATE_LIST [1] { + LIST [2] { + elements: { + LIST [3] { elements: { - CONSTANT [2] { value: 1 } - CONSTANT [3] { value: 2 } + CONSTANT [4] { value: 1 } + CONSTANT [5] { value: 2 } + CONSTANT [6] { value: 3 } } } - } - accu_var: @x0:0 - accu_init: { - CREATE_LIST [24] { - elements: { + COMPREHENSION [7] { + iter_var: @it:1:0 + iter_var2: @it2:1:0 + iter_range: { + IDENT [8] { + name: @index0 + } } - } - } - loop_condition: { - CONSTANT [25] { value: true } - } - loop_step: { - CALL [28] { - function: _+_ - args: { - IDENT [26] { - name: @x0:0 + accu_var: @ac:1:0 + accu_init: { + MAP [9] { + } - CREATE_LIST [27] { - elements: { - COMPREHENSION [23] { - iter_var: @c1:0 + } + loop_condition: { + CONSTANT [10] { value: true } + } + loop_step: { + CALL [11] { + function: cel.@mapInsert + args: { + IDENT [12] { + name: @ac:1:0 + } + IDENT [13] { + name: @it:1:0 + } + COMPREHENSION [14] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 iter_range: { - CREATE_LIST [6] { - elements: { - CONSTANT [7] { value: 1 } - CONSTANT [8] { value: 2 } - CONSTANT [9] { value: 3 } - } + IDENT [15] { + name: @index0 } } - accu_var: @x1:0 + accu_var: @ac:0:0 accu_init: { - CREATE_LIST [15] { - elements: { - } + MAP [16] { + } } loop_condition: { - CONSTANT [16] { value: true } + CONSTANT [17] { value: true } } loop_step: { - CALL [21] { - function: _?_:_ + CALL [18] { + function: cel.@mapInsert args: { - CALL [13] { - function: _==_ - args: { - IDENT [12] { - name: @c1:0 - } - IDENT [14] { - name: @c0:0 - } - } + IDENT [19] { + name: @ac:0:0 } - CALL [19] { + IDENT [20] { + name: @it:0:0 + } + CALL [21] { function: _+_ args: { - IDENT [17] { - name: @x1:0 - } - CREATE_LIST [18] { - elements: { - IDENT [11] { - name: @c1:0 + CALL [22] { + function: _+_ + args: { + IDENT [23] { + name: @it:0:0 + } + IDENT [24] { + name: @it2:0:0 } } } + CONSTANT [25] { value: 1 } } } - IDENT [20] { - name: @x1:0 - } } } } result: { - IDENT [22] { - name: @x1:0 + IDENT [26] { + name: @ac:0:0 } } } } } } - } - } - result: { - IDENT [29] { - name: @x0:0 + result: { + IDENT [27] { + name: @ac:1:0 + } + } } } } - CREATE_LIST [32] { - elements: { - CREATE_LIST [33] { - elements: { - CONSTANT [34] { value: 1 } - } + CALL [28] { + function: _==_ + args: { + IDENT [29] { + name: @index1 } - CREATE_LIST [35] { - elements: { - CONSTANT [36] { value: 2 } - } + IDENT [30] { + name: @index1 } } } @@ -1701,9 +2974,9 @@ Source: 1 in [1,2,3] && 2 in [1,2,3] && 3 in [3, [1,2,3]] && 1 in [1,2,3] CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { + LIST [3] { elements: { CONSTANT [4] { value: 1 } CONSTANT [5] { value: 2 } @@ -1748,7 +3021,7 @@ CALL [1] { function: @in args: { CONSTANT [18] { value: 3 } - CREATE_LIST [19] { + LIST [19] { elements: { CONSTANT [20] { value: 3 } IDENT [21] { @@ -1773,9 +3046,9 @@ Source: 2 in {'a': 1, 2: {true: false}, 3: {true: false}} CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_MAP [3] { + MAP [3] { MAP_ENTRY [4] { key: { CONSTANT [5] { value: true } @@ -1791,7 +3064,7 @@ CALL [1] { function: @in args: { CONSTANT [8] { value: 2 } - CREATE_MAP [9] { + MAP [9] { MAP_ENTRY [10] { key: { CONSTANT [11] { value: "a" } @@ -1814,10 +3087,136 @@ CALL [1] { key: { CONSTANT [17] { value: 3 } } - value: { - IDENT [18] { - name: @index0 - } + value: { + IDENT [18] { + name: @index0 + } + } + } + } + } + } + } +} +Test case: MACRO_ITER_VAR_NOT_REFERENCED +Source: [1,2].map(i, [1, 2].map(i, [3,4])) == [[[3, 4], [3, 4]], [[3, 4], [3, 4]]] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + CONSTANT [5] { value: 2 } + } + } + LIST [6] { + elements: { + CONSTANT [7] { value: 3 } + CONSTANT [8] { value: 4 } + } + } + LIST [9] { + elements: { + IDENT [10] { + name: @index1 + } + IDENT [11] { + name: @index1 + } + } + } + } + } + CALL [12] { + function: _==_ + args: { + COMPREHENSION [13] { + iter_var: @it:1:0 + iter_range: { + IDENT [14] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [15] { + elements: { + } + } + } + loop_condition: { + CONSTANT [16] { value: true } + } + loop_step: { + CALL [17] { + function: _+_ + args: { + IDENT [18] { + name: @ac:1:0 + } + LIST [19] { + elements: { + COMPREHENSION [20] { + iter_var: @it:0:0 + iter_range: { + IDENT [21] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [22] { + elements: { + } + } + } + loop_condition: { + CONSTANT [23] { value: true } + } + loop_step: { + CALL [24] { + function: _+_ + args: { + IDENT [25] { + name: @ac:0:0 + } + LIST [26] { + elements: { + IDENT [27] { + name: @index1 + } + } + } + } + } + } + result: { + IDENT [28] { + name: @ac:0:0 + } + } + } + } + } + } + } + } + result: { + IDENT [29] { + name: @ac:1:0 + } + } + } + LIST [30] { + elements: { + IDENT [31] { + name: @index2 + } + IDENT [32] { + name: @index2 } } } @@ -1825,27 +3224,27 @@ CALL [1] { } } } -Test case: MACRO_ITER_VAR_NOT_REFERENCED -Source: [1,2].map(i, [1, 2].map(i, [3,4])) == [[[3, 4], [3, 4]], [[3, 4], [3, 4]]] +Test case: MACRO_ITER_VAR2_NOT_REFERENCED +Source: [1,2].transformList(i, v, [1, 2].transformList(i, v, [3,4])) == [[[3, 4], [3, 4]], [[3, 4], [3, 4]]] =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { + LIST [3] { elements: { CONSTANT [4] { value: 1 } CONSTANT [5] { value: 2 } } } - CREATE_LIST [6] { + LIST [6] { elements: { CONSTANT [7] { value: 3 } CONSTANT [8] { value: 4 } } } - CREATE_LIST [9] { + LIST [9] { elements: { IDENT [10] { name: @index1 @@ -1861,15 +3260,16 @@ CALL [1] { function: _==_ args: { COMPREHENSION [13] { - iter_var: @c0:0 + iter_var: @it:1:0 + iter_var2: @it2:1:0 iter_range: { IDENT [14] { name: @index0 } } - accu_var: @x0:0 + accu_var: @ac:1:0 accu_init: { - CREATE_LIST [15] { + LIST [15] { elements: { } } @@ -1882,20 +3282,21 @@ CALL [1] { function: _+_ args: { IDENT [18] { - name: @x0:0 + name: @ac:1:0 } - CREATE_LIST [19] { + LIST [19] { elements: { COMPREHENSION [20] { - iter_var: @c1:0 + iter_var: @it:0:0 + iter_var2: @it2:0:0 iter_range: { IDENT [21] { name: @index0 } } - accu_var: @x1:0 + accu_var: @ac:0:0 accu_init: { - CREATE_LIST [22] { + LIST [22] { elements: { } } @@ -1908,9 +3309,9 @@ CALL [1] { function: _+_ args: { IDENT [25] { - name: @x1:0 + name: @ac:0:0 } - CREATE_LIST [26] { + LIST [26] { elements: { IDENT [27] { name: @index1 @@ -1922,7 +3323,7 @@ CALL [1] { } result: { IDENT [28] { - name: @x1:0 + name: @ac:0:0 } } } @@ -1933,11 +3334,11 @@ CALL [1] { } result: { IDENT [29] { - name: @x0:0 + name: @ac:1:0 } } } - CREATE_LIST [30] { + LIST [30] { elements: { IDENT [31] { name: @index2 @@ -1957,7 +3358,7 @@ Source: [x - 1 > 3 ? x - 1 : 5].exists(x, x - 1 > 3) || x - 1 > 3 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { function: _-_ @@ -1983,9 +3384,9 @@ CALL [1] { function: _||_ args: { COMPREHENSION [10] { - iter_var: @c0:0 + iter_var: @it:0:0 iter_range: { - CREATE_LIST [11] { + LIST [11] { elements: { CALL [12] { function: _?_:_ @@ -2002,7 +3403,7 @@ CALL [1] { } } } - accu_var: @x0:0 + accu_var: @ac:0:0 accu_init: { CONSTANT [16] { value: false } } @@ -2014,7 +3415,7 @@ CALL [1] { function: !_ args: { IDENT [19] { - name: @x0:0 + name: @ac:0:0 } } } @@ -2026,7 +3427,7 @@ CALL [1] { function: _||_ args: { IDENT [21] { - name: @x0:0 + name: @ac:0:0 } CALL [22] { function: _>_ @@ -2035,7 +3436,7 @@ CALL [1] { function: _-_ args: { IDENT [24] { - name: @c0:0 + name: @it:0:0 } CONSTANT [25] { value: 1 } } @@ -2048,7 +3449,7 @@ CALL [1] { } result: { IDENT [27] { - name: @x0:0 + name: @ac:0:0 } } } @@ -2062,115 +3463,315 @@ CALL [1] { Test case: MACRO_SHADOWED_VARIABLE_2 Source: ["foo", "bar"].map(x, [x + x, x + x]).map(x, [x + x, x + x]) =====> +COMPREHENSION [35] { + iter_var: x + iter_range: { + COMPREHENSION [19] { + iter_var: x + iter_range: { + LIST [1] { + elements: { + CONSTANT [2] { value: "foo" } + CONSTANT [3] { value: "bar" } + } + } + } + accu_var: @result + accu_init: { + LIST [13] { + elements: { + } + } + } + loop_condition: { + CONSTANT [14] { value: true } + } + loop_step: { + CALL [17] { + function: _+_ + args: { + IDENT [15] { + name: @result + } + LIST [16] { + elements: { + LIST [6] { + elements: { + CALL [8] { + function: _+_ + args: { + IDENT [7] { + name: x + } + IDENT [9] { + name: x + } + } + } + CALL [11] { + function: _+_ + args: { + IDENT [10] { + name: x + } + IDENT [12] { + name: x + } + } + } + } + } + } + } + } + } + } + result: { + IDENT [18] { + name: @result + } + } + } + } + accu_var: @result + accu_init: { + LIST [29] { + elements: { + } + } + } + loop_condition: { + CONSTANT [30] { value: true } + } + loop_step: { + CALL [33] { + function: _+_ + args: { + IDENT [31] { + name: @result + } + LIST [32] { + elements: { + LIST [22] { + elements: { + CALL [24] { + function: _+_ + args: { + IDENT [23] { + name: x + } + IDENT [25] { + name: x + } + } + } + CALL [27] { + function: _+_ + args: { + IDENT [26] { + name: x + } + IDENT [28] { + name: x + } + } + } + } + } + } + } + } + } + } + result: { + IDENT [34] { + name: @result + } + } +} +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 +=====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { - function: _+_ + function: _-_ args: { - IDENT [4] { - name: @c1:0 - } - IDENT [5] { - name: @c1:0 + CALL [4] { + function: _-_ + args: { + IDENT [5] { + name: x + } + IDENT [6] { + name: y + } + } } + CONSTANT [7] { value: 1 } } } - CALL [6] { - function: _+_ + CALL [8] { + function: _>_ args: { - IDENT [7] { - name: @c0:0 - } - IDENT [8] { - name: @c0:0 + IDENT [9] { + name: @index0 } + CONSTANT [10] { value: 3 } } } } } - COMPREHENSION [9] { - iter_var: @c0:0 - iter_range: { - COMPREHENSION [10] { - iter_var: @c1:0 + CALL [11] { + function: _||_ + args: { + COMPREHENSION [12] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 iter_range: { - CREATE_LIST [11] { + LIST [13] { elements: { - CONSTANT [12] { value: "foo" } - CONSTANT [13] { value: "bar" } + CALL [14] { + function: _?_:_ + args: { + IDENT [15] { + name: @index1 + } + IDENT [16] { + name: @index0 + } + CONSTANT [17] { value: 5 } + } + } } } } - accu_var: @x1:0 + accu_var: @ac:0:0 accu_init: { - CREATE_LIST [14] { - elements: { - } - } + CONSTANT [18] { value: false } } loop_condition: { - CONSTANT [15] { value: true } + CALL [19] { + function: @not_strictly_false + args: { + CALL [20] { + function: !_ + args: { + IDENT [21] { + name: @ac:0:0 + } + } + } + } + } } loop_step: { - CALL [16] { - function: _+_ + CALL [22] { + function: _||_ args: { - IDENT [17] { - name: @x1:0 + IDENT [23] { + name: @ac:0:0 } - CREATE_LIST [18] { - elements: { - CREATE_LIST [19] { - elements: { - IDENT [20] { - name: @index0 - } - IDENT [21] { - name: @index0 + CALL [24] { + function: _>_ + args: { + CALL [25] { + function: _-_ + args: { + CALL [26] { + function: _-_ + args: { + IDENT [27] { + name: @it:0:0 + } + IDENT [28] { + name: @it2:0:0 + } + } } + CONSTANT [29] { value: 1 } } } + CONSTANT [30] { value: 3 } } } } } } result: { - IDENT [22] { - name: @x1:0 + IDENT [31] { + name: @ac:0:0 } } } + IDENT [32] { + name: @index1 + } } - accu_var: @x0:0 - accu_init: { - CREATE_LIST [23] { + } + } +} +Test case: MACRO_SHADOWED_VARIABLE_COMP_V2_2 +Source: ["foo", "bar"].transformMap(x, y, [x + x, y + y]).transformMap(x, y, [x + x, y + y]) +=====> +COMPREHENSION [35] { + iter_var: x + iter_var2: y + iter_range: { + COMPREHENSION [19] { + iter_var: x + iter_var2: y + iter_range: { + LIST [1] { elements: { + CONSTANT [2] { value: "foo" } + CONSTANT [3] { value: "bar" } } } } + accu_var: @result + accu_init: { + MAP [14] { + + } + } loop_condition: { - CONSTANT [24] { value: true } + CONSTANT [15] { value: true } } loop_step: { - CALL [25] { - function: _+_ + CALL [17] { + function: cel.@mapInsert args: { - IDENT [26] { - name: @x0:0 + IDENT [16] { + name: @result + } + IDENT [5] { + name: x } - CREATE_LIST [27] { + LIST [7] { elements: { - CREATE_LIST [28] { - elements: { - IDENT [29] { - name: @index1 + CALL [9] { + function: _+_ + args: { + IDENT [8] { + name: x } - IDENT [30] { - name: @index1 + IDENT [10] { + name: x + } + } + } + CALL [12] { + function: _+_ + args: { + IDENT [11] { + name: y + } + IDENT [13] { + name: y } } } @@ -2180,12 +3781,65 @@ CALL [1] { } } result: { - IDENT [31] { - name: @x0:0 + IDENT [18] { + name: @result + } + } + } + } + accu_var: @result + accu_init: { + MAP [30] { + + } + } + loop_condition: { + CONSTANT [31] { value: true } + } + loop_step: { + CALL [33] { + function: cel.@mapInsert + args: { + IDENT [32] { + name: @result + } + IDENT [21] { + name: x + } + LIST [23] { + elements: { + CALL [25] { + function: _+_ + args: { + IDENT [24] { + name: x + } + IDENT [26] { + name: x + } + } + } + CALL [28] { + function: _+_ + args: { + IDENT [27] { + name: y + } + IDENT [29] { + name: y + } + } + } + } } } } } + result: { + IDENT [34] { + name: @result + } + } } Test case: PRESENCE_TEST Source: has({'a': true}.a) && {'a':true}['a'] @@ -2193,9 +3847,9 @@ Source: has({'a': true}.a) && {'a':true}['a'] CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_MAP [3] { + MAP [3] { MAP_ENTRY [4] { key: { CONSTANT [5] { value: "a" } @@ -2228,13 +3882,48 @@ CALL [1] { } } } +Test case: PRESENCE_TEST_2 +Source: has({'a': true}.a) && has({'a': true}.a) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + SELECT [3] { + MAP [4] { + MAP_ENTRY [5] { + key: { + CONSTANT [6] { value: "a" } + } + value: { + CONSTANT [7] { value: true } + } + } + }.a~presence_test + } + } + } + CALL [8] { + function: _&&_ + args: { + IDENT [9] { + name: @index0 + } + IDENT [10] { + name: @index0 + } + } + } + } +} Test case: PRESENCE_TEST_WITH_TERNARY Source: (has(msg.oneof_type.payload) ? msg.oneof_type.payload.single_int64 : 0) == 10 =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { @@ -2275,7 +3964,7 @@ Source: (has(msg.oneof_type.payload) ? msg.oneof_type.payload.single_int64 : msg CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { @@ -2327,7 +4016,7 @@ Source: (has(msg.oneof_type.payload.single_int64) ? msg.oneof_type.payload.singl CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { SELECT [4] { @@ -2379,7 +4068,7 @@ Source: (has(msg.oneof_type) && has(msg.oneof_type.payload) && has(msg.oneof_typ CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { @@ -2469,14 +4158,14 @@ Source: [10, ?optional.none(), [?optional.none(), ?opt_x], [?optional.none(), ?o CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { function: optional.none args: { } } - CREATE_LIST [4] { + LIST [4] { elements: { IDENT [5] { name: @index0 @@ -2487,7 +4176,7 @@ CALL [1] { } optional_indices: [0, 1] } - CREATE_LIST [7] { + LIST [7] { elements: { CONSTANT [8] { value: 5 } } @@ -2497,7 +4186,7 @@ CALL [1] { CALL [9] { function: _==_ args: { - CREATE_LIST [10] { + LIST [10] { elements: { CONSTANT [11] { value: 10 } IDENT [12] { @@ -2512,7 +4201,7 @@ CALL [1] { } optional_indices: [0] } - CREATE_LIST [15] { + LIST [15] { elements: { CONSTANT [16] { value: 10 } IDENT [17] { @@ -2533,12 +4222,12 @@ Source: {?'hello': optional.of('hello')}['hello'] + {?'hello': optional.of('hell CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { function: _[_] args: { - CREATE_MAP [4] { + MAP [4] { MAP_ENTRY [5] { key: { CONSTANT [6] { value: "hello" } @@ -2584,9 +4273,9 @@ Source: {?'key': optional.of('test')}[?'bogus'].or({'key': 'test'}[?'bogus']).or CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_MAP [3] { + MAP [3] { MAP_ENTRY [4] { key: { CONSTANT [5] { value: "key" } @@ -2610,7 +4299,7 @@ CALL [1] { CALL [10] { function: _[?_] args: { - CREATE_MAP [11] { + MAP [11] { MAP_ENTRY [12] { key: { CONSTANT [13] { value: "key" } @@ -2666,10 +4355,10 @@ Source: TestAllTypes{?single_int64: optional.ofNonZeroValue(1), ?single_int32: o CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_STRUCT [3] { - name: TestAllTypes + STRUCT [3] { + name: cel.expr.conformance.proto3.TestAllTypes entries: { ENTRY [4] { field_key: single_int64 @@ -2728,7 +4417,7 @@ Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('h' + 'e' + 'l' + 'l' + CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { function: _+_ @@ -2928,7 +4617,7 @@ Source: non_pure_custom_func(msg.oneof_type.payload.single_int64) + non_pure_cus CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { SELECT [4] { @@ -3003,7 +4692,7 @@ Source: pure_custom_func(msg.oneof_type.payload.single_int64) + pure_custom_func CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { SELECT [4] { @@ -3066,4 +4755,4 @@ CALL [1] { } } } -} +} \ No newline at end of file diff --git a/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_1.baseline b/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_1.baseline index a8b6c243c..1b3146069 100644 --- a/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_1.baseline +++ b/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_1.baseline @@ -4,9 +4,9 @@ Source: size([1,2]) + size([1,2]) + 1 == 5 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { + LIST [3] { elements: { CONSTANT [4] { value: 1 } CONSTANT [5] { value: 2 } @@ -59,9 +59,9 @@ Source: 2 + size([1,2]) + size([1,2]) + 1 == 7 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { + LIST [3] { elements: { CONSTANT [4] { value: 1 } CONSTANT [5] { value: 2 } @@ -123,9 +123,9 @@ Source: size([0]) + size([0]) + size([1,2]) + size([1,2]) == 6 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { + LIST [3] { elements: { CONSTANT [4] { value: 0 } } @@ -138,7 +138,7 @@ CALL [1] { } } } - CREATE_LIST [7] { + LIST [7] { elements: { CONSTANT [8] { value: 1 } CONSTANT [9] { value: 2 } @@ -204,9 +204,9 @@ Source: 5 + size([0]) + size([0]) + size([1,2]) + size([1,2]) + size([1,2,3]) + CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { + LIST [3] { elements: { CONSTANT [4] { value: 0 } } @@ -219,7 +219,7 @@ CALL [1] { } } } - CREATE_LIST [7] { + LIST [7] { elements: { CONSTANT [8] { value: 1 } CONSTANT [9] { value: 2 } @@ -233,7 +233,7 @@ CALL [1] { } } } - CREATE_LIST [12] { + LIST [12] { elements: { CONSTANT [13] { value: 1 } CONSTANT [14] { value: 2 } @@ -331,7 +331,7 @@ Source: timestamp(int(timestamp(1000000000))).getFullYear() + timestamp(int(time CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { function: timestamp @@ -442,7 +442,7 @@ CALL [1] { } } CALL [31] { - function: getMinutes + function: getFullYear target: { IDENT [32] { name: @index13 @@ -452,109 +452,109 @@ CALL [1] { } } CALL [33] { - function: getSeconds - target: { + function: _+_ + args: { IDENT [34] { - name: @index6 + name: @index3 } - } - args: { - } - } - CALL [35] { - function: getFullYear - target: { - IDENT [36] { - name: @index6 + IDENT [35] { + name: @index14 } } - args: { - } } - CALL [37] { + CALL [36] { function: getFullYear target: { - IDENT [38] { - name: @index13 + IDENT [37] { + name: @index6 } } args: { } } - CALL [39] { + CALL [38] { function: _+_ args: { - IDENT [40] { - name: @index3 + IDENT [39] { + name: @index15 } - IDENT [41] { - name: @index17 + IDENT [40] { + name: @index16 } } } - CALL [42] { + CALL [41] { function: _+_ args: { + IDENT [42] { + name: @index17 + } IDENT [43] { - name: @index18 + name: @index3 } - IDENT [44] { - name: @index16 + } + } + CALL [44] { + function: getSeconds + target: { + IDENT [45] { + name: @index6 } } + args: { + } } - CALL [45] { + CALL [46] { function: _+_ args: { - IDENT [46] { - name: @index19 - } IDENT [47] { - name: @index3 + name: @index18 + } + IDENT [48] { + name: @index19 } } } - CALL [48] { + CALL [49] { function: _+_ args: { - IDENT [49] { + IDENT [50] { name: @index20 } - IDENT [50] { - name: @index15 + IDENT [51] { + name: @index10 } } } - CALL [51] { + CALL [52] { function: _+_ args: { - IDENT [52] { + IDENT [53] { name: @index21 } - IDENT [53] { + IDENT [54] { name: @index10 } } } - CALL [54] { - function: _+_ - args: { - IDENT [55] { - name: @index22 - } + CALL [55] { + function: getMinutes + target: { IDENT [56] { - name: @index10 + name: @index13 } } + args: { + } } CALL [57] { function: _+_ args: { IDENT [58] { - name: @index23 + name: @index22 } IDENT [59] { - name: @index14 + name: @index23 } } } @@ -588,9 +588,9 @@ Source: {"a": 2}["a"] + {"a": 2}["a"] * {"a": 2}["a"] == 6 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_MAP [3] { + MAP [3] { MAP_ENTRY [4] { key: { CONSTANT [5] { value: "a" } @@ -650,9 +650,9 @@ Source: {'a': {'b': 1}, 'c': {'b': 1}, 'd': {'e': {'b': 1}}, 'e': {'e': {'b': 1} CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_MAP [3] { + MAP [3] { MAP_ENTRY [4] { key: { CONSTANT [5] { value: "b" } @@ -662,7 +662,7 @@ CALL [1] { } } } - CREATE_MAP [7] { + MAP [7] { MAP_ENTRY [8] { key: { CONSTANT [9] { value: "e" } @@ -676,7 +676,7 @@ CALL [1] { } } } - CREATE_MAP [11] { + MAP [11] { MAP_ENTRY [12] { key: { CONSTANT [13] { value: "a" } @@ -726,9 +726,9 @@ Source: [1, [1,2,3,4], 2, [1,2,3,4], 5, [1,2,3,4], 7, [[1,2], [1,2,3,4]], [1,2]] CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { + LIST [3] { elements: { CONSTANT [4] { value: 1 } CONSTANT [5] { value: 2 } @@ -736,13 +736,13 @@ CALL [1] { CONSTANT [7] { value: 4 } } } - CREATE_LIST [8] { + LIST [8] { elements: { CONSTANT [9] { value: 1 } CONSTANT [10] { value: 2 } } } - CREATE_LIST [11] { + LIST [11] { elements: { IDENT [12] { name: @index1 @@ -754,7 +754,7 @@ CALL [1] { } } } - CREATE_LIST [14] { + LIST [14] { elements: { CONSTANT [15] { value: 1 } IDENT [16] { @@ -785,7 +785,7 @@ Source: msg.single_int64 + msg.single_int64 == 6 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { @@ -822,7 +822,7 @@ Source: msg.oneof_type.payload.single_int64 + msg.oneof_type.payload.single_int3 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { @@ -842,69 +842,69 @@ CALL [1] { SELECT [9] { IDENT [10] { name: @index1 - }.oneof_type - } - SELECT [11] { - IDENT [12] { - name: @index3 - }.payload - } - SELECT [13] { - IDENT [14] { - name: @index4 - }.single_int64 - } - SELECT [15] { - IDENT [16] { - name: msg - }.single_int64 - } - SELECT [17] { - IDENT [18] { - name: @index1 }.single_int32 } - CALL [19] { + CALL [11] { function: _+_ args: { - IDENT [20] { + IDENT [12] { name: @index2 } - IDENT [21] { - name: @index7 + IDENT [13] { + name: @index3 } } } - CALL [22] { + CALL [14] { function: _+_ args: { - IDENT [23] { - name: @index8 + IDENT [15] { + name: @index4 } - IDENT [24] { + IDENT [16] { name: @index2 } } } - CALL [25] { + SELECT [17] { + IDENT [18] { + name: msg + }.single_int64 + } + CALL [19] { function: _+_ args: { - IDENT [26] { - name: @index9 + IDENT [20] { + name: @index5 } - IDENT [27] { + IDENT [21] { name: @index6 } } } + SELECT [22] { + IDENT [23] { + name: @index1 + }.oneof_type + } + SELECT [24] { + IDENT [25] { + name: @index8 + }.payload + } + SELECT [26] { + IDENT [27] { + name: @index9 + }.single_int64 + } CALL [28] { function: _+_ args: { IDENT [29] { - name: @index10 + name: @index7 } IDENT [30] { - name: @index5 + name: @index10 } } } @@ -927,7 +927,7 @@ Source: true || msg.oneof_type.payload.oneof_type.payload.oneof_type.payload.one CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { @@ -957,12 +957,12 @@ CALL [1] { SELECT [13] { IDENT [14] { name: @index4 - }.child + }.payload } SELECT [15] { IDENT [16] { name: @index5 - }.child + }.oneof_type } SELECT [17] { IDENT [18] { @@ -974,34 +974,34 @@ CALL [1] { name: @index7 }.single_bool } - SELECT [21] { - IDENT [22] { - name: @index4 - }.payload + CALL [21] { + function: _||_ + args: { + CONSTANT [22] { value: true } + IDENT [23] { + name: @index8 + } + } } - SELECT [23] { - IDENT [24] { - name: @index9 - }.oneof_type + SELECT [24] { + IDENT [25] { + name: @index4 + }.child } - SELECT [25] { - IDENT [26] { + SELECT [26] { + IDENT [27] { name: @index10 - }.payload + }.child } - SELECT [27] { - IDENT [28] { + SELECT [28] { + IDENT [29] { name: @index11 - }.single_bool + }.payload } - CALL [29] { - function: _||_ - args: { - CONSTANT [30] { value: true } - IDENT [31] { - name: @index12 - } - } + SELECT [30] { + IDENT [31] { + name: @index12 + }.single_bool } } } @@ -1009,10 +1009,10 @@ CALL [1] { function: _||_ args: { IDENT [33] { - name: @index13 + name: @index9 } IDENT [34] { - name: @index8 + name: @index13 } } } @@ -1024,7 +1024,7 @@ Source: msg.oneof_type.payload.map_int32_int64[1] + msg.oneof_type.payload.map_i CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { @@ -1091,7 +1091,7 @@ Source: msg.oneof_type.payload.map_int32_int64[0] + msg.oneof_type.payload.map_i CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { @@ -1114,7 +1114,7 @@ CALL [1] { IDENT [10] { name: @index2 } - CONSTANT [11] { value: 2 } + CONSTANT [11] { value: 0 } } } CALL [12] { @@ -1127,33 +1127,33 @@ CALL [1] { } } CALL [15] { - function: _[_] + function: _+_ args: { IDENT [16] { - name: @index2 + name: @index3 + } + IDENT [17] { + name: @index4 } - CONSTANT [17] { value: 0 } } } CALL [18] { - function: _+_ + function: _[_] args: { IDENT [19] { - name: @index5 - } - IDENT [20] { - name: @index4 + name: @index2 } + CONSTANT [20] { value: 2 } } } CALL [21] { function: _+_ args: { IDENT [22] { - name: @index6 + name: @index5 } IDENT [23] { - name: @index3 + name: @index6 } } } @@ -1176,7 +1176,7 @@ Source: msg.oneof_type.payload.oneof_type.payload.oneof_type.payload.oneof_type. CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { @@ -1233,7 +1233,7 @@ Source: (msg.single_int64 > 0 ? msg.single_int64 : 0) == 3 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { @@ -1280,7 +1280,7 @@ Source: false ? false : (msg.single_int64) + ((msg.single_int64 + 1) * 2) == 11 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { @@ -1345,7 +1345,7 @@ Source: (msg.single_int64 > 0 ? (msg.single_int32 > 0 ? msg.single_int64 + msg.s CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { @@ -1358,14 +1358,12 @@ CALL [1] { }.single_int32 } CALL [7] { - function: _+_ + function: _>_ args: { IDENT [8] { name: @index0 } - IDENT [9] { - name: @index1 - } + CONSTANT [9] { value: 0 } } } CALL [10] { @@ -1378,22 +1376,24 @@ CALL [1] { } } CALL [13] { - function: _?_:_ + function: _+_ args: { IDENT [14] { - name: @index3 + name: @index0 } IDENT [15] { - name: @index2 + name: @index1 } - CONSTANT [16] { value: 0 } } } - CALL [17] { - function: _>_ + CALL [16] { + function: _?_:_ args: { + IDENT [17] { + name: @index3 + } IDENT [18] { - name: @index0 + name: @index4 } CONSTANT [19] { value: 0 } } @@ -1402,10 +1402,10 @@ CALL [1] { function: _?_:_ args: { IDENT [21] { - name: @index5 + name: @index2 } IDENT [22] { - name: @index4 + name: @index5 } CONSTANT [23] { value: 0 } } @@ -1429,97 +1429,57 @@ Source: size([[1].exists(i, i > 0)]) + size([[1].exists(j, j > 0)]) + size([[2]. CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { + LIST [3] { elements: { CONSTANT [4] { value: 1 } } } - CALL [5] { - function: _>_ - args: { - IDENT [6] { - name: @c0:0 - } - CONSTANT [7] { value: 0 } - } - } - CALL [8] { - function: _||_ - args: { - IDENT [9] { - name: @x0:0 - } - IDENT [10] { - name: @index1 - } - } - } - CREATE_LIST [11] { + LIST [5] { elements: { - CONSTANT [12] { value: 2 } - } - } - CALL [13] { - function: _>_ - args: { - IDENT [14] { - name: @c0:0 - } - CONSTANT [15] { value: 1 } - } - } - CALL [16] { - function: _||_ - args: { - IDENT [17] { - name: @x0:0 - } - IDENT [18] { - name: @index4 - } + CONSTANT [6] { value: 2 } } } } } - CALL [19] { + CALL [7] { function: _==_ args: { - CALL [20] { + CALL [8] { function: _+_ args: { - CALL [21] { + CALL [9] { function: _+_ args: { - CALL [22] { + CALL [10] { function: _+_ args: { - CALL [23] { + CALL [11] { function: size args: { - CREATE_LIST [24] { + LIST [12] { elements: { - COMPREHENSION [25] { - iter_var: @c0:0 + COMPREHENSION [13] { + iter_var: @it:0:0 iter_range: { - IDENT [26] { + IDENT [14] { name: @index0 } } - accu_var: @x0:0 + accu_var: @ac:0:0 accu_init: { - CONSTANT [27] { value: false } + CONSTANT [15] { value: false } } loop_condition: { - CALL [28] { + CALL [16] { function: @not_strictly_false args: { - CALL [29] { + CALL [17] { function: !_ args: { - IDENT [30] { - name: @x0:0 + IDENT [18] { + name: @ac:0:0 } } } @@ -1527,13 +1487,27 @@ CALL [1] { } } loop_step: { - IDENT [31] { - name: @index2 - } - } - result: { - IDENT [32] { - name: @x0:0 + CALL [19] { + function: _||_ + args: { + IDENT [20] { + name: @ac:0:0 + } + CALL [21] { + function: _>_ + args: { + IDENT [22] { + name: @it:0:0 + } + CONSTANT [23] { value: 0 } + } + } + } + } + } + result: { + IDENT [24] { + name: @ac:0:0 } } } @@ -1541,31 +1515,31 @@ CALL [1] { } } } - CALL [33] { + CALL [25] { function: size args: { - CREATE_LIST [34] { + LIST [26] { elements: { - COMPREHENSION [35] { - iter_var: @c0:0 + COMPREHENSION [27] { + iter_var: @it:0:0 iter_range: { - IDENT [36] { + IDENT [28] { name: @index0 } } - accu_var: @x0:0 + accu_var: @ac:0:0 accu_init: { - CONSTANT [37] { value: false } + CONSTANT [29] { value: false } } loop_condition: { - CALL [38] { + CALL [30] { function: @not_strictly_false args: { - CALL [39] { + CALL [31] { function: !_ args: { - IDENT [40] { - name: @x0:0 + IDENT [32] { + name: @ac:0:0 } } } @@ -1573,13 +1547,27 @@ CALL [1] { } } loop_step: { - IDENT [41] { - name: @index2 + CALL [33] { + function: _||_ + args: { + IDENT [34] { + name: @ac:0:0 + } + CALL [35] { + function: _>_ + args: { + IDENT [36] { + name: @it:0:0 + } + CONSTANT [37] { value: 0 } + } + } + } } } result: { - IDENT [42] { - name: @x0:0 + IDENT [38] { + name: @ac:0:0 } } } @@ -1589,31 +1577,31 @@ CALL [1] { } } } - CALL [43] { + CALL [39] { function: size args: { - CREATE_LIST [44] { + LIST [40] { elements: { - COMPREHENSION [45] { - iter_var: @c0:0 + COMPREHENSION [41] { + iter_var: @it:0:0 iter_range: { - IDENT [46] { - name: @index3 + IDENT [42] { + name: @index1 } } - accu_var: @x0:0 + accu_var: @ac:0:0 accu_init: { - CONSTANT [47] { value: false } + CONSTANT [43] { value: false } } loop_condition: { - CALL [48] { + CALL [44] { function: @not_strictly_false args: { - CALL [49] { + CALL [45] { function: !_ args: { - IDENT [50] { - name: @x0:0 + IDENT [46] { + name: @ac:0:0 } } } @@ -1621,13 +1609,27 @@ CALL [1] { } } loop_step: { - IDENT [51] { - name: @index5 + CALL [47] { + function: _||_ + args: { + IDENT [48] { + name: @ac:0:0 + } + CALL [49] { + function: _>_ + args: { + IDENT [50] { + name: @it:0:0 + } + CONSTANT [51] { value: 1 } + } + } + } } } result: { IDENT [52] { - name: @x0:0 + name: @ac:0:0 } } } @@ -1640,16 +1642,16 @@ CALL [1] { CALL [53] { function: size args: { - CREATE_LIST [54] { + LIST [54] { elements: { COMPREHENSION [55] { - iter_var: @c0:0 + iter_var: @it:0:0 iter_range: { IDENT [56] { - name: @index3 + name: @index1 } } - accu_var: @x0:0 + accu_var: @ac:0:0 accu_init: { CONSTANT [57] { value: false } } @@ -1661,7 +1663,7 @@ CALL [1] { function: !_ args: { IDENT [60] { - name: @x0:0 + name: @ac:0:0 } } } @@ -1669,13 +1671,27 @@ CALL [1] { } } loop_step: { - IDENT [61] { - name: @index5 + CALL [61] { + function: _||_ + args: { + IDENT [62] { + name: @ac:0:0 + } + CALL [63] { + function: _>_ + args: { + IDENT [64] { + name: @it:0:0 + } + CONSTANT [65] { value: 1 } + } + } + } } } result: { - IDENT [62] { - name: @x0:0 + IDENT [66] { + name: @ac:0:0 } } } @@ -1685,7 +1701,7 @@ CALL [1] { } } } - CONSTANT [63] { value: 4 } + CONSTANT [67] { value: 4 } } } } @@ -1696,102 +1712,62 @@ Source: [[1].exists(i, i > 0)] + [[1].exists(j, j > 0)] + [['a'].exists(k, k == CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { + LIST [3] { elements: { CONSTANT [4] { value: 1 } } } - CALL [5] { - function: _>_ - args: { - IDENT [6] { - name: @c0:0 - } - CONSTANT [7] { value: 0 } - } - } - CALL [8] { - function: _||_ - args: { - IDENT [9] { - name: @x0:0 - } - IDENT [10] { - name: @index1 - } - } - } - CREATE_LIST [11] { + LIST [5] { elements: { - CONSTANT [12] { value: "a" } - } - } - CALL [13] { - function: _==_ - args: { - IDENT [14] { - name: @c0:1 - } - CONSTANT [15] { value: "a" } - } - } - CALL [16] { - function: _||_ - args: { - IDENT [17] { - name: @x0:1 - } - IDENT [18] { - name: @index4 - } + CONSTANT [6] { value: "a" } } } - CREATE_LIST [19] { + LIST [7] { elements: { - CONSTANT [20] { value: true } - CONSTANT [21] { value: true } - CONSTANT [22] { value: true } - CONSTANT [23] { value: true } + CONSTANT [8] { value: true } + CONSTANT [9] { value: true } + CONSTANT [10] { value: true } + CONSTANT [11] { value: true } } } } } - CALL [24] { + CALL [12] { function: _==_ args: { - CALL [25] { + CALL [13] { function: _+_ args: { - CALL [26] { + CALL [14] { function: _+_ args: { - CALL [27] { + CALL [15] { function: _+_ args: { - CREATE_LIST [28] { + LIST [16] { elements: { - COMPREHENSION [29] { - iter_var: @c0:0 + COMPREHENSION [17] { + iter_var: @it:0:0 iter_range: { - IDENT [30] { + IDENT [18] { name: @index0 } } - accu_var: @x0:0 + accu_var: @ac:0:0 accu_init: { - CONSTANT [31] { value: false } + CONSTANT [19] { value: false } } loop_condition: { - CALL [32] { + CALL [20] { function: @not_strictly_false args: { - CALL [33] { + CALL [21] { function: !_ args: { - IDENT [34] { - name: @x0:0 + IDENT [22] { + name: @ac:0:0 } } } @@ -1799,40 +1775,54 @@ CALL [1] { } } loop_step: { - IDENT [35] { - name: @index2 + CALL [23] { + function: _||_ + args: { + IDENT [24] { + name: @ac:0:0 + } + CALL [25] { + function: _>_ + args: { + IDENT [26] { + name: @it:0:0 + } + CONSTANT [27] { value: 0 } + } + } + } } } result: { - IDENT [36] { - name: @x0:0 + IDENT [28] { + name: @ac:0:0 } } } } } - CREATE_LIST [37] { + LIST [29] { elements: { - COMPREHENSION [38] { - iter_var: @c0:0 + COMPREHENSION [30] { + iter_var: @it:0:0 iter_range: { - IDENT [39] { + IDENT [31] { name: @index0 } } - accu_var: @x0:0 + accu_var: @ac:0:0 accu_init: { - CONSTANT [40] { value: false } + CONSTANT [32] { value: false } } loop_condition: { - CALL [41] { + CALL [33] { function: @not_strictly_false args: { - CALL [42] { + CALL [34] { function: !_ args: { - IDENT [43] { - name: @x0:0 + IDENT [35] { + name: @ac:0:0 } } } @@ -1840,13 +1830,27 @@ CALL [1] { } } loop_step: { - IDENT [44] { - name: @index2 + CALL [36] { + function: _||_ + args: { + IDENT [37] { + name: @ac:0:0 + } + CALL [38] { + function: _>_ + args: { + IDENT [39] { + name: @it:0:0 + } + CONSTANT [40] { value: 0 } + } + } + } } } result: { - IDENT [45] { - name: @x0:0 + IDENT [41] { + name: @ac:0:0 } } } @@ -1854,28 +1858,28 @@ CALL [1] { } } } - CREATE_LIST [46] { + LIST [42] { elements: { - COMPREHENSION [47] { - iter_var: @c0:1 + COMPREHENSION [43] { + iter_var: @it:0:1 iter_range: { - IDENT [48] { - name: @index3 + IDENT [44] { + name: @index1 } } - accu_var: @x0:1 + accu_var: @ac:0:1 accu_init: { - CONSTANT [49] { value: false } + CONSTANT [45] { value: false } } loop_condition: { - CALL [50] { + CALL [46] { function: @not_strictly_false args: { - CALL [51] { + CALL [47] { function: !_ args: { - IDENT [52] { - name: @x0:1 + IDENT [48] { + name: @ac:0:1 } } } @@ -1883,13 +1887,27 @@ CALL [1] { } } loop_step: { - IDENT [53] { - name: @index5 + CALL [49] { + function: _||_ + args: { + IDENT [50] { + name: @ac:0:1 + } + CALL [51] { + function: _==_ + args: { + IDENT [52] { + name: @it:0:1 + } + CONSTANT [53] { value: "a" } + } + } + } } } result: { IDENT [54] { - name: @x0:1 + name: @ac:0:1 } } } @@ -1897,16 +1915,16 @@ CALL [1] { } } } - CREATE_LIST [55] { + LIST [55] { elements: { COMPREHENSION [56] { - iter_var: @c0:1 + iter_var: @it:0:1 iter_range: { IDENT [57] { - name: @index3 + name: @index1 } } - accu_var: @x0:1 + accu_var: @ac:0:1 accu_init: { CONSTANT [58] { value: false } } @@ -1918,7 +1936,7 @@ CALL [1] { function: !_ args: { IDENT [61] { - name: @x0:1 + name: @ac:0:1 } } } @@ -1926,13 +1944,27 @@ CALL [1] { } } loop_step: { - IDENT [62] { - name: @index5 + CALL [62] { + function: _||_ + args: { + IDENT [63] { + name: @ac:0:1 + } + CALL [64] { + function: _==_ + args: { + IDENT [65] { + name: @it:0:1 + } + CONSTANT [66] { value: "a" } + } + } + } } } result: { - IDENT [63] { - name: @x0:1 + IDENT [67] { + name: @ac:0:1 } } } @@ -1940,309 +1972,2030 @@ CALL [1] { } } } - IDENT [64] { - name: @index6 + IDENT [68] { + name: @index2 } } } } } -Test case: NESTED_MACROS -Source: [1,2,3].map(i, [1, 2, 3].map(i, i + 1)) == [[2, 3, 4], [2, 3, 4], [2, 3, 4]] +Test case: MULTIPLE_MACROS_3 +Source: [1].exists(i, i > 0) && [1].exists(j, j > 0) && [1].exists(k, k > 1) && [2].exists(l, l > 1) =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { + LIST [3] { elements: { CONSTANT [4] { value: 1 } - CONSTANT [5] { value: 2 } - CONSTANT [6] { value: 3 } - } - } - CREATE_LIST [7] { - elements: { - CONSTANT [8] { value: 2 } - CONSTANT [9] { value: 3 } - CONSTANT [10] { value: 4 } - } - } - CREATE_LIST [11] { - elements: { - IDENT [12] { - name: @index1 - } - IDENT [13] { - name: @index1 - } - IDENT [14] { - name: @index1 - } - } - } - CALL [15] { - function: _+_ - args: { - IDENT [16] { - name: @c1:0 - } - CONSTANT [17] { value: 1 } } } - CREATE_LIST [18] { + LIST [5] { elements: { - IDENT [19] { - name: @index3 - } - } - } - CALL [20] { - function: _+_ - args: { - IDENT [21] { - name: @x1:0 - } - IDENT [22] { - name: @index4 - } + CONSTANT [6] { value: 2 } } } } } - CALL [23] { - function: _==_ + CALL [7] { + function: _&&_ args: { - COMPREHENSION [24] { - iter_var: @c0:0 - iter_range: { - IDENT [25] { - name: @index0 - } - } - accu_var: @x0:0 - accu_init: { - CREATE_LIST [26] { - elements: { - } - } - } - loop_condition: { - CONSTANT [27] { value: true } - } - loop_step: { - CALL [28] { - function: _+_ - args: { - IDENT [29] { - name: @x0:0 + CALL [8] { + function: _&&_ + args: { + COMPREHENSION [9] { + iter_var: @it:0:0 + iter_range: { + IDENT [10] { + name: @index0 } - CREATE_LIST [30] { - elements: { - COMPREHENSION [31] { - iter_var: @c1:0 - iter_range: { - IDENT [32] { - name: @index0 + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [11] { value: false } + } + loop_condition: { + CALL [12] { + function: @not_strictly_false + args: { + CALL [13] { + function: !_ + args: { + IDENT [14] { + name: @ac:0:0 } } - accu_var: @x1:0 - accu_init: { - CREATE_LIST [33] { - elements: { - } + } + } + } + } + loop_step: { + CALL [15] { + function: _||_ + args: { + IDENT [16] { + name: @ac:0:0 + } + CALL [17] { + function: _>_ + args: { + IDENT [18] { + name: @it:0:0 } + CONSTANT [19] { value: 0 } } - loop_condition: { - CONSTANT [34] { value: true } - } - loop_step: { - IDENT [35] { - name: @index5 + } + } + } + } + result: { + IDENT [20] { + name: @ac:0:0 + } + } + } + COMPREHENSION [21] { + iter_var: @it:0:0 + iter_range: { + IDENT [22] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [23] { value: false } + } + loop_condition: { + CALL [24] { + function: @not_strictly_false + args: { + CALL [25] { + function: !_ + args: { + IDENT [26] { + name: @ac:0:0 } } - result: { - IDENT [36] { - name: @x1:0 + } + } + } + } + loop_step: { + CALL [27] { + function: _||_ + args: { + IDENT [28] { + name: @ac:0:0 + } + CALL [29] { + function: _>_ + args: { + IDENT [30] { + name: @it:0:0 } + CONSTANT [31] { value: 0 } } } } } } + result: { + IDENT [32] { + name: @ac:0:0 + } + } } } - result: { - IDENT [37] { - name: @x0:0 - } - } - } - IDENT [38] { - name: @index2 - } - } - } - } -} -Test case: NESTED_MACROS_2 -Source: [1, 2].map(y, [1, 2, 3].filter(x, x == y)) == [[1], [2]] -=====> -CALL [1] { - function: cel.@block - args: { - CREATE_LIST [2] { - elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 2 } - } } - CREATE_LIST [5] { - elements: { - CONSTANT [6] { value: 1 } + CALL [33] { + function: _&&_ + args: { + COMPREHENSION [34] { + iter_var: @it:0:0 + iter_range: { + IDENT [35] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [36] { value: false } + } + loop_condition: { + CALL [37] { + function: @not_strictly_false + args: { + CALL [38] { + function: !_ + args: { + IDENT [39] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [40] { + function: _||_ + args: { + IDENT [41] { + name: @ac:0:0 + } + CALL [42] { + function: _>_ + args: { + IDENT [43] { + name: @it:0:0 + } + CONSTANT [44] { value: 1 } + } + } + } + } + } + result: { + IDENT [45] { + name: @ac:0:0 + } + } + } + COMPREHENSION [46] { + iter_var: @it:0:0 + iter_range: { + IDENT [47] { + name: @index1 + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [48] { value: false } + } + loop_condition: { + CALL [49] { + function: @not_strictly_false + args: { + CALL [50] { + function: !_ + args: { + IDENT [51] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [52] { + function: _||_ + args: { + IDENT [53] { + name: @ac:0:0 + } + CALL [54] { + function: _>_ + args: { + IDENT [55] { + name: @it:0:0 + } + CONSTANT [56] { value: 1 } + } + } + } + } + } + result: { + IDENT [57] { + name: @ac:0:0 + } + } + } + } + } + } + } + } +} +Test case: MULTIPLE_MACROS_COMP_V2_1 +Source: size([[1].exists(i,x,i > 0 && x >= 0)]) + size([[1].exists(j, y, j > 0 && y >= 0)]) + size([[2].exists(k,z,k > 1 && z >= 0)]) + size([[2].exists(l, w, l > 1 && w >= 0)]) == 4 +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + } + } + LIST [5] { + elements: { + CONSTANT [6] { value: 2 } + } + } + } + } + CALL [7] { + function: _==_ + args: { + CALL [8] { + function: _+_ + args: { + CALL [9] { + function: _+_ + args: { + CALL [10] { + function: _+_ + args: { + CALL [11] { + function: size + args: { + LIST [12] { + elements: { + COMPREHENSION [13] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + IDENT [14] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [15] { value: false } + } + loop_condition: { + CALL [16] { + function: @not_strictly_false + args: { + CALL [17] { + function: !_ + args: { + IDENT [18] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [19] { + function: _||_ + args: { + IDENT [20] { + name: @ac:0:0 + } + CALL [21] { + function: _&&_ + args: { + CALL [22] { + function: _>_ + args: { + IDENT [23] { + name: @it:0:0 + } + CONSTANT [24] { value: 0 } + } + } + CALL [25] { + function: _>=_ + args: { + IDENT [26] { + name: @it2:0:0 + } + CONSTANT [27] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [28] { + name: @ac:0:0 + } + } + } + } + } + } + } + CALL [29] { + function: size + args: { + LIST [30] { + elements: { + COMPREHENSION [31] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + IDENT [32] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [33] { value: false } + } + loop_condition: { + CALL [34] { + function: @not_strictly_false + args: { + CALL [35] { + function: !_ + args: { + IDENT [36] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [37] { + function: _||_ + args: { + IDENT [38] { + name: @ac:0:0 + } + CALL [39] { + function: _&&_ + args: { + CALL [40] { + function: _>_ + args: { + IDENT [41] { + name: @it:0:0 + } + CONSTANT [42] { value: 0 } + } + } + CALL [43] { + function: _>=_ + args: { + IDENT [44] { + name: @it2:0:0 + } + CONSTANT [45] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [46] { + name: @ac:0:0 + } + } + } + } + } + } + } + } + } + CALL [47] { + function: size + args: { + LIST [48] { + elements: { + COMPREHENSION [49] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + IDENT [50] { + name: @index1 + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [51] { value: false } + } + loop_condition: { + CALL [52] { + function: @not_strictly_false + args: { + CALL [53] { + function: !_ + args: { + IDENT [54] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [55] { + function: _||_ + args: { + IDENT [56] { + name: @ac:0:0 + } + CALL [57] { + function: _&&_ + args: { + CALL [58] { + function: _>_ + args: { + IDENT [59] { + name: @it:0:0 + } + CONSTANT [60] { value: 1 } + } + } + CALL [61] { + function: _>=_ + args: { + IDENT [62] { + name: @it2:0:0 + } + CONSTANT [63] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [64] { + name: @ac:0:0 + } + } + } + } + } + } + } + } + } + CALL [65] { + function: size + args: { + LIST [66] { + elements: { + COMPREHENSION [67] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + IDENT [68] { + name: @index1 + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [69] { value: false } + } + loop_condition: { + CALL [70] { + function: @not_strictly_false + args: { + CALL [71] { + function: !_ + args: { + IDENT [72] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [73] { + function: _||_ + args: { + IDENT [74] { + name: @ac:0:0 + } + CALL [75] { + function: _&&_ + args: { + CALL [76] { + function: _>_ + args: { + IDENT [77] { + name: @it:0:0 + } + CONSTANT [78] { value: 1 } + } + } + CALL [79] { + function: _>=_ + args: { + IDENT [80] { + name: @it2:0:0 + } + CONSTANT [81] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [82] { + name: @ac:0:0 + } + } + } + } + } + } + } + } + } + CONSTANT [83] { value: 4 } + } + } + } +} +Test case: MULTIPLE_MACROS_COMP_V2_2 +Source: [1].exists(i, x, i > 0 && x > 0) && [1].exists(j, y, j > 0 && y > 0) && [1].exists(k, z, k > 1 && z > 0) && [2].exists(l, w, l > 1 && w > 0) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + } + } + LIST [5] { + elements: { + CONSTANT [6] { value: 2 } + } + } + } + } + CALL [7] { + function: _&&_ + args: { + CALL [8] { + function: _&&_ + args: { + COMPREHENSION [9] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + IDENT [10] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [11] { value: false } + } + loop_condition: { + CALL [12] { + function: @not_strictly_false + args: { + CALL [13] { + function: !_ + args: { + IDENT [14] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [15] { + function: _||_ + args: { + IDENT [16] { + name: @ac:0:0 + } + CALL [17] { + function: _&&_ + args: { + CALL [18] { + function: _>_ + args: { + IDENT [19] { + name: @it:0:0 + } + CONSTANT [20] { value: 0 } + } + } + CALL [21] { + function: _>_ + args: { + IDENT [22] { + name: @it2:0:0 + } + CONSTANT [23] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [24] { + name: @ac:0:0 + } + } + } + COMPREHENSION [25] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + IDENT [26] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [27] { value: false } + } + loop_condition: { + CALL [28] { + function: @not_strictly_false + args: { + CALL [29] { + function: !_ + args: { + IDENT [30] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [31] { + function: _||_ + args: { + IDENT [32] { + name: @ac:0:0 + } + CALL [33] { + function: _&&_ + args: { + CALL [34] { + function: _>_ + args: { + IDENT [35] { + name: @it:0:0 + } + CONSTANT [36] { value: 0 } + } + } + CALL [37] { + function: _>_ + args: { + IDENT [38] { + name: @it2:0:0 + } + CONSTANT [39] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [40] { + name: @ac:0:0 + } + } + } + } + } + CALL [41] { + function: _&&_ + args: { + COMPREHENSION [42] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + IDENT [43] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [44] { value: false } + } + loop_condition: { + CALL [45] { + function: @not_strictly_false + args: { + CALL [46] { + function: !_ + args: { + IDENT [47] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [48] { + function: _||_ + args: { + IDENT [49] { + name: @ac:0:0 + } + CALL [50] { + function: _&&_ + args: { + CALL [51] { + function: _>_ + args: { + IDENT [52] { + name: @it:0:0 + } + CONSTANT [53] { value: 1 } + } + } + CALL [54] { + function: _>_ + args: { + IDENT [55] { + name: @it2:0:0 + } + CONSTANT [56] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [57] { + name: @ac:0:0 + } + } + } + COMPREHENSION [58] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + IDENT [59] { + name: @index1 + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [60] { value: false } + } + loop_condition: { + CALL [61] { + function: @not_strictly_false + args: { + CALL [62] { + function: !_ + args: { + IDENT [63] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [64] { + function: _||_ + args: { + IDENT [65] { + name: @ac:0:0 + } + CALL [66] { + function: _&&_ + args: { + CALL [67] { + function: _>_ + args: { + IDENT [68] { + name: @it:0:0 + } + CONSTANT [69] { value: 1 } + } + } + CALL [70] { + function: _>_ + args: { + IDENT [71] { + name: @it2:0:0 + } + CONSTANT [72] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [73] { + name: @ac:0:0 + } + } + } + } + } + } + } + } +} +Test case: NESTED_MACROS +Source: [1,2,3].map(i, [1, 2, 3].map(i, i + 1)) == [[2, 3, 4], [2, 3, 4], [2, 3, 4]] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + CONSTANT [5] { value: 2 } + CONSTANT [6] { value: 3 } + } + } + LIST [7] { + elements: { + CONSTANT [8] { value: 2 } + CONSTANT [9] { value: 3 } + CONSTANT [10] { value: 4 } + } + } + LIST [11] { + elements: { + IDENT [12] { + name: @index1 + } + IDENT [13] { + name: @index1 + } + IDENT [14] { + name: @index1 + } + } + } + } + } + CALL [15] { + function: _==_ + args: { + COMPREHENSION [16] { + iter_var: @it:1:0 + iter_range: { + IDENT [17] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [18] { + elements: { + } + } + } + loop_condition: { + CONSTANT [19] { value: true } + } + loop_step: { + CALL [20] { + function: _+_ + args: { + IDENT [21] { + name: @ac:1:0 + } + LIST [22] { + elements: { + COMPREHENSION [23] { + iter_var: @it:0:0 + iter_range: { + IDENT [24] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [25] { + elements: { + } + } + } + loop_condition: { + CONSTANT [26] { value: true } + } + loop_step: { + CALL [27] { + function: _+_ + args: { + IDENT [28] { + name: @ac:0:0 + } + LIST [29] { + elements: { + CALL [30] { + function: _+_ + args: { + IDENT [31] { + name: @it:0:0 + } + CONSTANT [32] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [33] { + name: @ac:0:0 + } + } + } + } + } + } + } + } + result: { + IDENT [34] { + name: @ac:1:0 + } + } + } + IDENT [35] { + name: @index2 + } + } + } + } +} +Test case: NESTED_MACROS_2 +Source: [1, 2].map(y, [1, 2, 3].filter(x, x == y)) == [[1], [2]] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + CONSTANT [5] { value: 2 } + } + } + LIST [6] { + elements: { + CONSTANT [7] { value: 1 } + CONSTANT [8] { value: 2 } + CONSTANT [9] { value: 3 } + } + } + LIST [10] { + elements: { + CONSTANT [11] { value: 1 } + } + } + LIST [12] { + elements: { + CONSTANT [13] { value: 2 } + } + } + LIST [14] { + elements: { + IDENT [15] { + name: @index2 + } + IDENT [16] { + name: @index3 + } + } + } + } + } + CALL [17] { + function: _==_ + args: { + COMPREHENSION [18] { + iter_var: @it:1:0 + iter_range: { + IDENT [19] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [20] { + elements: { + } + } + } + loop_condition: { + CONSTANT [21] { value: true } + } + loop_step: { + CALL [22] { + function: _+_ + args: { + IDENT [23] { + name: @ac:1:0 + } + LIST [24] { + elements: { + COMPREHENSION [25] { + iter_var: @it:0:0 + iter_range: { + IDENT [26] { + name: @index1 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [27] { + elements: { + } + } + } + loop_condition: { + CONSTANT [28] { value: true } + } + loop_step: { + CALL [29] { + function: _?_:_ + args: { + CALL [30] { + function: _==_ + args: { + IDENT [31] { + name: @it:0:0 + } + IDENT [32] { + name: @it:1:0 + } + } + } + CALL [33] { + function: _+_ + args: { + IDENT [34] { + name: @ac:0:0 + } + LIST [35] { + elements: { + IDENT [36] { + name: @it:0:0 + } + } + } + } + } + IDENT [37] { + name: @ac:0:0 + } + } + } + } + result: { + IDENT [38] { + name: @ac:0:0 + } + } + } + } + } + } + } + } + result: { + IDENT [39] { + name: @ac:1:0 + } + } + } + IDENT [40] { + name: @index4 + } + } + } + } +} +Test case: NESTED_MACROS_3 +Source: [1,2,3].map(i, i * 2) + [1,2,3].map(i, i * 2).map(i, i * 2) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + CONSTANT [5] { value: 2 } + CONSTANT [6] { value: 3 } + } + } + } + } + CALL [7] { + function: _+_ + args: { + COMPREHENSION [8] { + iter_var: @it:0:0 + iter_range: { + IDENT [9] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [10] { + elements: { + } + } + } + loop_condition: { + CONSTANT [11] { value: true } + } + loop_step: { + CALL [12] { + function: _+_ + args: { + IDENT [13] { + name: @ac:0:0 + } + LIST [14] { + elements: { + CALL [15] { + function: _*_ + args: { + IDENT [16] { + name: @it:0:0 + } + CONSTANT [17] { value: 2 } + } + } + } + } + } + } + } + result: { + IDENT [18] { + name: @ac:0:0 + } + } + } + COMPREHENSION [19] { + iter_var: @it:1:0 + iter_range: { + COMPREHENSION [20] { + iter_var: @it:0:0 + iter_range: { + IDENT [21] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [22] { + elements: { + } + } + } + loop_condition: { + CONSTANT [23] { value: true } + } + loop_step: { + CALL [24] { + function: _+_ + args: { + IDENT [25] { + name: @ac:0:0 + } + LIST [26] { + elements: { + CALL [27] { + function: _*_ + args: { + IDENT [28] { + name: @it:0:0 + } + CONSTANT [29] { value: 2 } + } + } + } + } + } + } + } + result: { + IDENT [30] { + name: @ac:0:0 + } + } + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [31] { + elements: { + } + } + } + loop_condition: { + CONSTANT [32] { value: true } + } + loop_step: { + CALL [33] { + function: _+_ + args: { + IDENT [34] { + name: @ac:1:0 + } + LIST [35] { + elements: { + CALL [36] { + function: _*_ + args: { + IDENT [37] { + name: @it:1:0 + } + CONSTANT [38] { value: 2 } + } + } + } + } + } + } + } + result: { + IDENT [39] { + name: @ac:1:0 + } + } + } + } + } + } +} +Test case: NESTED_MACROS_COMP_V2_1 +Source: [1,2,3].transformList(i, v, [1, 2, 3].transformList(i, v, i + v + 1)) == [[2, 4, 6], [2, 4, 6], [2, 4, 6]] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + CONSTANT [5] { value: 2 } + CONSTANT [6] { value: 3 } + } + } + LIST [7] { + elements: { + CONSTANT [8] { value: 2 } + CONSTANT [9] { value: 4 } + CONSTANT [10] { value: 6 } + } + } + LIST [11] { + elements: { + IDENT [12] { + name: @index1 + } + IDENT [13] { + name: @index1 + } + IDENT [14] { + name: @index1 + } + } + } + } + } + CALL [15] { + function: _==_ + args: { + COMPREHENSION [16] { + iter_var: @it:1:0 + iter_var2: @it2:1:0 + iter_range: { + IDENT [17] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [18] { + elements: { + } + } + } + loop_condition: { + CONSTANT [19] { value: true } + } + loop_step: { + CALL [20] { + function: _+_ + args: { + IDENT [21] { + name: @ac:1:0 + } + LIST [22] { + elements: { + COMPREHENSION [23] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + IDENT [24] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [25] { + elements: { + } + } + } + loop_condition: { + CONSTANT [26] { value: true } + } + loop_step: { + CALL [27] { + function: _+_ + args: { + IDENT [28] { + name: @ac:0:0 + } + LIST [29] { + elements: { + CALL [30] { + function: _+_ + args: { + CALL [31] { + function: _+_ + args: { + IDENT [32] { + name: @it:0:0 + } + IDENT [33] { + name: @it2:0:0 + } + } + } + CONSTANT [34] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [35] { + name: @ac:0:0 + } + } + } + } + } + } + } + } + result: { + IDENT [36] { + name: @ac:1:0 + } } } - CREATE_LIST [7] { + IDENT [37] { + name: @index2 + } + } + } + } +} +Test case: NESTED_MACROS_COMP_V2_2 +Source: [1, 2].transformList(i, y, [1, 2, 3].filter(x, x == y && i < y)) == [[1], [2]] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { elements: { - IDENT [8] { - name: @index1 + CONSTANT [4] { value: 1 } + CONSTANT [5] { value: 2 } + } + } + LIST [6] { + elements: { + CONSTANT [7] { value: 1 } + CONSTANT [8] { value: 2 } + CONSTANT [9] { value: 3 } + } + } + LIST [10] { + elements: { + CONSTANT [11] { value: 1 } + } + } + LIST [12] { + elements: { + CONSTANT [13] { value: 2 } + } + } + LIST [14] { + elements: { + IDENT [15] { + name: @index2 } - IDENT [9] { + IDENT [16] { + name: @index3 + } + } + } + } + } + CALL [17] { + function: _==_ + args: { + COMPREHENSION [18] { + iter_var: @it:1:0 + iter_var2: @it2:1:0 + iter_range: { + IDENT [19] { name: @index0 } } + accu_var: @ac:1:0 + accu_init: { + LIST [20] { + elements: { + } + } + } + loop_condition: { + CONSTANT [21] { value: true } + } + loop_step: { + CALL [22] { + function: _+_ + args: { + IDENT [23] { + name: @ac:1:0 + } + LIST [24] { + elements: { + COMPREHENSION [25] { + iter_var: @it:0:0 + iter_range: { + IDENT [26] { + name: @index1 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [27] { + elements: { + } + } + } + loop_condition: { + CONSTANT [28] { value: true } + } + loop_step: { + CALL [29] { + function: _?_:_ + args: { + CALL [30] { + function: _&&_ + args: { + CALL [31] { + function: _==_ + args: { + IDENT [32] { + name: @it:0:0 + } + IDENT [33] { + name: @it2:1:0 + } + } + } + CALL [34] { + function: _<_ + args: { + IDENT [35] { + name: @it:1:0 + } + IDENT [36] { + name: @it2:1:0 + } + } + } + } + } + CALL [37] { + function: _+_ + args: { + IDENT [38] { + name: @ac:0:0 + } + LIST [39] { + elements: { + IDENT [40] { + name: @it:0:0 + } + } + } + } + } + IDENT [41] { + name: @ac:0:0 + } + } + } + } + result: { + IDENT [42] { + name: @ac:0:0 + } + } + } + } + } + } + } + } + result: { + IDENT [43] { + name: @ac:1:0 + } + } + } + IDENT [44] { + name: @index4 } - CREATE_LIST [10] { + } + } + } +} +Test case: ADJACENT_NESTED_MACROS +Source: [1,2,3].map(i, [1, 2, 3].map(i, i + 1)) == [1,2,3].map(j, [1, 2, 3].map(j, j + 1)) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { elements: { - IDENT [11] { - name: @c1:0 - } + CONSTANT [4] { value: 1 } + CONSTANT [5] { value: 2 } + CONSTANT [6] { value: 3 } } } - CALL [12] { - function: _+_ - args: { - IDENT [13] { - name: @x1:0 + } + } + CALL [7] { + function: _==_ + args: { + COMPREHENSION [8] { + iter_var: @it:1:0 + iter_range: { + IDENT [9] { + name: @index0 } - IDENT [14] { - name: @index3 + } + accu_var: @ac:1:0 + accu_init: { + LIST [10] { + elements: { + } } } - } - CALL [15] { - function: _==_ - args: { - IDENT [16] { - name: @c1:0 + loop_condition: { + CONSTANT [11] { value: true } + } + loop_step: { + CALL [12] { + function: _+_ + args: { + IDENT [13] { + name: @ac:1:0 + } + LIST [14] { + elements: { + COMPREHENSION [15] { + iter_var: @it:0:0 + iter_range: { + IDENT [16] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [17] { + elements: { + } + } + } + loop_condition: { + CONSTANT [18] { value: true } + } + loop_step: { + CALL [19] { + function: _+_ + args: { + IDENT [20] { + name: @ac:0:0 + } + LIST [21] { + elements: { + CALL [22] { + function: _+_ + args: { + IDENT [23] { + name: @it:0:0 + } + CONSTANT [24] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [25] { + name: @ac:0:0 + } + } + } + } + } + } } - IDENT [17] { - name: @c0:0 + } + result: { + IDENT [26] { + name: @ac:1:0 } } } - CALL [18] { - function: _?_:_ - args: { - IDENT [19] { - name: @index5 + COMPREHENSION [27] { + iter_var: @it:1:0 + iter_range: { + IDENT [28] { + name: @index0 } - IDENT [20] { - name: @index4 + } + accu_var: @ac:1:0 + accu_init: { + LIST [29] { + elements: { + } } - IDENT [21] { - name: @x1:0 + } + loop_condition: { + CONSTANT [30] { value: true } + } + loop_step: { + CALL [31] { + function: _+_ + args: { + IDENT [32] { + name: @ac:1:0 + } + LIST [33] { + elements: { + COMPREHENSION [34] { + iter_var: @it:0:0 + iter_range: { + IDENT [35] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [36] { + elements: { + } + } + } + loop_condition: { + CONSTANT [37] { value: true } + } + loop_step: { + CALL [38] { + function: _+_ + args: { + IDENT [39] { + name: @ac:0:0 + } + LIST [40] { + elements: { + CALL [41] { + function: _+_ + args: { + IDENT [42] { + name: @it:0:0 + } + CONSTANT [43] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [44] { + name: @ac:0:0 + } + } + } + } + } + } } } - } - CREATE_LIST [22] { - elements: { - CONSTANT [23] { value: 1 } - CONSTANT [24] { value: 2 } - CONSTANT [25] { value: 3 } + result: { + IDENT [45] { + name: @ac:1:0 + } } } - CREATE_LIST [26] { + } + } + } +} +Test case: ADJACENT_NESTED_MACROS_COMP_V2 +Source: [1,2,3].transformMap(i, x, [1, 2, 3].transformMap(i, x, i + x + 1)) == [1,2,3].transformMap(j, y, [1, 2, 3].transformMap(j, y, j + y + 1)) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { elements: { - CONSTANT [27] { value: 1 } - CONSTANT [28] { value: 2 } + CONSTANT [4] { value: 1 } + CONSTANT [5] { value: 2 } + CONSTANT [6] { value: 3 } } } } } - CALL [29] { + CALL [7] { function: _==_ args: { - COMPREHENSION [30] { - iter_var: @c0:0 + COMPREHENSION [8] { + iter_var: @it:1:0 + iter_var2: @it2:1:0 iter_range: { - IDENT [31] { - name: @index8 + IDENT [9] { + name: @index0 } } - accu_var: @x0:0 + accu_var: @ac:1:0 accu_init: { - CREATE_LIST [32] { - elements: { - } + MAP [10] { + } } loop_condition: { - CONSTANT [33] { value: true } + CONSTANT [11] { value: true } } loop_step: { - CALL [34] { - function: _+_ + CALL [12] { + function: cel.@mapInsert args: { - IDENT [35] { - name: @x0:0 + IDENT [13] { + name: @ac:1:0 } - CREATE_LIST [36] { - elements: { - COMPREHENSION [37] { - iter_var: @c1:0 - iter_range: { - IDENT [38] { - name: @index7 + IDENT [14] { + name: @it:1:0 + } + COMPREHENSION [15] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + IDENT [16] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + MAP [17] { + + } + } + loop_condition: { + CONSTANT [18] { value: true } + } + loop_step: { + CALL [19] { + function: cel.@mapInsert + args: { + IDENT [20] { + name: @ac:0:0 } - } - accu_var: @x1:0 - accu_init: { - CREATE_LIST [39] { - elements: { + IDENT [21] { + name: @it:0:0 + } + CALL [22] { + function: _+_ + args: { + CALL [23] { + function: _+_ + args: { + IDENT [24] { + name: @it:0:0 + } + IDENT [25] { + name: @it2:0:0 + } + } + } + CONSTANT [26] { value: 1 } } } } - loop_condition: { - CONSTANT [40] { value: true } - } - loop_step: { + } + } + result: { + IDENT [27] { + name: @ac:0:0 + } + } + } + } + } + } + result: { + IDENT [28] { + name: @ac:1:0 + } + } + } + COMPREHENSION [29] { + iter_var: @it:1:0 + iter_var2: @it2:1:0 + iter_range: { + IDENT [30] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + MAP [31] { + + } + } + loop_condition: { + CONSTANT [32] { value: true } + } + loop_step: { + CALL [33] { + function: cel.@mapInsert + args: { + IDENT [34] { + name: @ac:1:0 + } + IDENT [35] { + name: @it:1:0 + } + COMPREHENSION [36] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + IDENT [37] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + MAP [38] { + + } + } + loop_condition: { + CONSTANT [39] { value: true } + } + loop_step: { + CALL [40] { + function: cel.@mapInsert + args: { IDENT [41] { - name: @index6 + name: @ac:0:0 } - } - result: { IDENT [42] { - name: @x1:0 + name: @it:0:0 + } + CALL [43] { + function: _+_ + args: { + CALL [44] { + function: _+_ + args: { + IDENT [45] { + name: @it:0:0 + } + IDENT [46] { + name: @it2:0:0 + } + } + } + CONSTANT [47] { value: 1 } + } } } } } + result: { + IDENT [48] { + name: @ac:0:0 + } + } } } } } result: { - IDENT [43] { - name: @x0:0 + IDENT [49] { + name: @ac:1:0 } } } - IDENT [44] { - name: @index2 - } } } } @@ -2253,9 +4006,9 @@ Source: 1 in [1,2,3] && 2 in [1,2,3] && 3 in [3, [1,2,3]] && 1 in [1,2,3] CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { + LIST [3] { elements: { CONSTANT [4] { value: 1 } CONSTANT [5] { value: 2 } @@ -2271,40 +4024,40 @@ CALL [1] { } } } - CREATE_LIST [10] { - elements: { - CONSTANT [11] { value: 3 } + CALL [10] { + function: @in + args: { + CONSTANT [11] { value: 2 } IDENT [12] { name: @index0 } } } CALL [13] { - function: @in + function: _&&_ args: { - CONSTANT [14] { value: 3 } + IDENT [14] { + name: @index1 + } IDENT [15] { name: @index2 } } } - CALL [16] { - function: _&&_ - args: { - IDENT [17] { - name: @index3 - } + LIST [16] { + elements: { + CONSTANT [17] { value: 3 } IDENT [18] { - name: @index1 + name: @index0 } } } CALL [19] { function: @in args: { - CONSTANT [20] { value: 2 } + CONSTANT [20] { value: 3 } IDENT [21] { - name: @index0 + name: @index4 } } } @@ -2312,10 +4065,10 @@ CALL [1] { function: _&&_ args: { IDENT [23] { - name: @index1 + name: @index5 } IDENT [24] { - name: @index5 + name: @index1 } } } @@ -2325,10 +4078,10 @@ CALL [1] { function: _&&_ args: { IDENT [26] { - name: @index6 + name: @index3 } IDENT [27] { - name: @index4 + name: @index6 } } } @@ -2340,9 +4093,9 @@ Source: 2 in {'a': 1, 2: {true: false}, 3: {true: false}} CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_MAP [3] { + MAP [3] { MAP_ENTRY [4] { key: { CONSTANT [5] { value: true } @@ -2352,7 +4105,7 @@ CALL [1] { } } } - CREATE_MAP [7] { + MAP [7] { MAP_ENTRY [8] { key: { CONSTANT [9] { value: "a" } @@ -2371,51 +4124,183 @@ CALL [1] { } } } - MAP_ENTRY [14] { - key: { - CONSTANT [15] { value: 3 } - } - value: { - IDENT [16] { - name: @index0 - } + MAP_ENTRY [14] { + key: { + CONSTANT [15] { value: 3 } + } + value: { + IDENT [16] { + name: @index0 + } + } + } + } + } + } + CALL [17] { + function: @in + args: { + CONSTANT [18] { value: 2 } + IDENT [19] { + name: @index1 + } + } + } + } +} +Test case: MACRO_ITER_VAR_NOT_REFERENCED +Source: [1,2].map(i, [1, 2].map(i, [3,4])) == [[[3, 4], [3, 4]], [[3, 4], [3, 4]]] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + CONSTANT [5] { value: 2 } + } + } + LIST [6] { + elements: { + CONSTANT [7] { value: 3 } + CONSTANT [8] { value: 4 } + } + } + LIST [9] { + elements: { + IDENT [10] { + name: @index1 + } + IDENT [11] { + name: @index1 + } + } + } + LIST [12] { + elements: { + IDENT [13] { + name: @index1 + } + } + } + LIST [14] { + elements: { + IDENT [15] { + name: @index2 + } + IDENT [16] { + name: @index2 + } + } + } + } + } + CALL [17] { + function: _==_ + args: { + COMPREHENSION [18] { + iter_var: @it:1:0 + iter_range: { + IDENT [19] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [20] { + elements: { + } + } + } + loop_condition: { + CONSTANT [21] { value: true } + } + loop_step: { + CALL [22] { + function: _+_ + args: { + IDENT [23] { + name: @ac:1:0 + } + LIST [24] { + elements: { + COMPREHENSION [25] { + iter_var: @it:0:0 + iter_range: { + IDENT [26] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [27] { + elements: { + } + } + } + loop_condition: { + CONSTANT [28] { value: true } + } + loop_step: { + CALL [29] { + function: _+_ + args: { + IDENT [30] { + name: @ac:0:0 + } + IDENT [31] { + name: @index3 + } + } + } + } + result: { + IDENT [32] { + name: @ac:0:0 + } + } + } + } + } + } + } + } + result: { + IDENT [33] { + name: @ac:1:0 } } } - } - } - CALL [17] { - function: @in - args: { - CONSTANT [18] { value: 2 } - IDENT [19] { - name: @index1 + IDENT [34] { + name: @index4 } } } } } -Test case: MACRO_ITER_VAR_NOT_REFERENCED -Source: [1,2].map(i, [1, 2].map(i, [3,4])) == [[[3, 4], [3, 4]], [[3, 4], [3, 4]]] +Test case: MACRO_ITER_VAR2_NOT_REFERENCED +Source: [1,2].transformList(i, v, [1, 2].transformList(i, v, [3,4])) == [[[3, 4], [3, 4]], [[3, 4], [3, 4]]] =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { + LIST [3] { elements: { CONSTANT [4] { value: 1 } CONSTANT [5] { value: 2 } } } - CREATE_LIST [6] { + LIST [6] { elements: { CONSTANT [7] { value: 3 } CONSTANT [8] { value: 4 } } } - CREATE_LIST [9] { + LIST [9] { elements: { IDENT [10] { name: @index1 @@ -2425,90 +4310,89 @@ CALL [1] { } } } - CREATE_LIST [12] { + LIST [12] { elements: { IDENT [13] { - name: @index2 - } - IDENT [14] { - name: @index2 - } - } - } - CREATE_LIST [15] { - elements: { - IDENT [16] { name: @index1 } } } - CALL [17] { - function: _+_ - args: { - IDENT [18] { - name: @x1:0 + LIST [14] { + elements: { + IDENT [15] { + name: @index2 } - IDENT [19] { - name: @index4 + IDENT [16] { + name: @index2 } } } } } - CALL [20] { + CALL [17] { function: _==_ args: { - COMPREHENSION [21] { - iter_var: @c0:0 + COMPREHENSION [18] { + iter_var: @it:1:0 + iter_var2: @it2:1:0 iter_range: { - IDENT [22] { + IDENT [19] { name: @index0 } } - accu_var: @x0:0 + accu_var: @ac:1:0 accu_init: { - CREATE_LIST [23] { + LIST [20] { elements: { } } } loop_condition: { - CONSTANT [24] { value: true } + CONSTANT [21] { value: true } } loop_step: { - CALL [25] { + CALL [22] { function: _+_ args: { - IDENT [26] { - name: @x0:0 + IDENT [23] { + name: @ac:1:0 } - CREATE_LIST [27] { + LIST [24] { elements: { - COMPREHENSION [28] { - iter_var: @c1:0 + COMPREHENSION [25] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 iter_range: { - IDENT [29] { + IDENT [26] { name: @index0 } } - accu_var: @x1:0 + accu_var: @ac:0:0 accu_init: { - CREATE_LIST [30] { + LIST [27] { elements: { } } } loop_condition: { - CONSTANT [31] { value: true } + CONSTANT [28] { value: true } } loop_step: { - IDENT [32] { - name: @index5 + CALL [29] { + function: _+_ + args: { + IDENT [30] { + name: @ac:0:0 + } + IDENT [31] { + name: @index3 + } + } } } result: { - IDENT [33] { - name: @x1:0 + IDENT [32] { + name: @ac:0:0 } } } @@ -2518,13 +4402,13 @@ CALL [1] { } } result: { - IDENT [34] { - name: @x0:0 + IDENT [33] { + name: @ac:1:0 } } } - IDENT [35] { - name: @index3 + IDENT [34] { + name: @index4 } } } @@ -2536,7 +4420,7 @@ Source: [x - 1 > 3 ? x - 1 : 5].exists(x, x - 1 > 3) || x - 1 > 3 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { function: _-_ @@ -2557,78 +4441,49 @@ CALL [1] { } } CALL [9] { - function: _-_ - args: { - IDENT [10] { - name: @c0:0 - } - CONSTANT [11] { value: 1 } - } - } - CALL [12] { - function: _>_ - args: { - IDENT [13] { - name: @index2 - } - CONSTANT [14] { value: 3 } - } - } - CALL [15] { - function: _||_ - args: { - IDENT [16] { - name: @x0:0 - } - IDENT [17] { - name: @index3 - } - } - } - CALL [18] { function: _?_:_ args: { - IDENT [19] { + IDENT [10] { name: @index1 } - IDENT [20] { + IDENT [11] { name: @index0 } - CONSTANT [21] { value: 5 } + CONSTANT [12] { value: 5 } } } - CREATE_LIST [22] { + LIST [13] { elements: { - IDENT [23] { - name: @index5 + IDENT [14] { + name: @index2 } } } } } - CALL [24] { + CALL [15] { function: _||_ args: { - COMPREHENSION [25] { - iter_var: @c0:0 + COMPREHENSION [16] { + iter_var: @it:0:0 iter_range: { - IDENT [26] { - name: @index6 + IDENT [17] { + name: @index3 } } - accu_var: @x0:0 + accu_var: @ac:0:0 accu_init: { - CONSTANT [27] { value: false } + CONSTANT [18] { value: false } } loop_condition: { - CALL [28] { + CALL [19] { function: @not_strictly_false args: { - CALL [29] { + CALL [20] { function: !_ args: { - IDENT [30] { - name: @x0:0 + IDENT [21] { + name: @ac:0:0 } } } @@ -2636,17 +4491,37 @@ CALL [1] { } } loop_step: { - IDENT [31] { - name: @index4 + CALL [22] { + function: _||_ + args: { + IDENT [23] { + name: @ac:0:0 + } + CALL [24] { + function: _>_ + args: { + CALL [25] { + function: _-_ + args: { + IDENT [26] { + name: @it:0:0 + } + CONSTANT [27] { value: 1 } + } + } + CONSTANT [28] { value: 3 } + } + } + } } } result: { - IDENT [32] { - name: @x0:0 + IDENT [29] { + name: @ac:0:0 } } } - IDENT [33] { + IDENT [30] { name: @index1 } } @@ -2659,144 +4534,409 @@ Source: ["foo", "bar"].map(x, [x + x, x + x]).map(x, [x + x, x + x]) CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CALL [3] { + LIST [3] { + elements: { + CONSTANT [4] { value: "foo" } + CONSTANT [5] { value: "bar" } + } + } + } + } + COMPREHENSION [6] { + iter_var: @it:1:0 + iter_range: { + COMPREHENSION [7] { + iter_var: @it:0:0 + iter_range: { + IDENT [8] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [9] { + elements: { + } + } + } + loop_condition: { + CONSTANT [10] { value: true } + } + loop_step: { + CALL [11] { + function: _+_ + args: { + IDENT [12] { + name: @ac:0:0 + } + LIST [13] { + elements: { + LIST [14] { + elements: { + CALL [15] { + function: _+_ + args: { + IDENT [16] { + name: @it:0:0 + } + IDENT [17] { + name: @it:0:0 + } + } + } + CALL [18] { + function: _+_ + args: { + IDENT [19] { + name: @it:0:0 + } + IDENT [20] { + name: @it:0:0 + } + } + } + } + } + } + } + } + } + } + result: { + IDENT [21] { + name: @ac:0:0 + } + } + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [22] { + elements: { + } + } + } + loop_condition: { + CONSTANT [23] { value: true } + } + loop_step: { + CALL [24] { function: _+_ + args: { + IDENT [25] { + name: @ac:1:0 + } + LIST [26] { + elements: { + LIST [27] { + elements: { + CALL [28] { + function: _+_ + args: { + IDENT [29] { + name: @it:1:0 + } + IDENT [30] { + name: @it:1:0 + } + } + } + CALL [31] { + function: _+_ + args: { + IDENT [32] { + name: @it:1:0 + } + IDENT [33] { + name: @it:1:0 + } + } + } + } + } + } + } + } + } + } + result: { + IDENT [34] { + name: @ac:1:0 + } + } + } + } +} +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 +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + CALL [3] { + function: _-_ args: { IDENT [4] { - name: @c1:0 + name: x } IDENT [5] { - name: @c1:0 + name: y } } } CALL [6] { - function: _+_ + function: _-_ args: { IDENT [7] { - name: @c0:0 - } - IDENT [8] { - name: @c0:0 + name: @index0 } + CONSTANT [8] { value: 1 } } } - CREATE_LIST [9] { - elements: { + CALL [9] { + function: _>_ + args: { IDENT [10] { name: @index1 } - IDENT [11] { - name: @index1 - } + CONSTANT [11] { value: 3 } } } - CREATE_LIST [12] { - elements: { + CALL [12] { + function: _?_:_ + args: { IDENT [13] { name: @index2 } - } - } - CALL [14] { - function: _+_ - args: { - IDENT [15] { - name: @x0:0 - } - IDENT [16] { - name: @index3 + IDENT [14] { + name: @index1 } + CONSTANT [15] { value: 5 } } } - CREATE_LIST [17] { + LIST [16] { elements: { - IDENT [18] { - name: @index0 - } - IDENT [19] { - name: @index0 + IDENT [17] { + name: @index3 } } } - CREATE_LIST [20] { - elements: { - IDENT [21] { - name: @index5 + } + } + CALL [18] { + function: _||_ + args: { + COMPREHENSION [19] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + IDENT [20] { + name: @index4 } } - } - CALL [22] { - function: _+_ - args: { - IDENT [23] { - name: @x1:0 + accu_var: @ac:0:0 + accu_init: { + CONSTANT [21] { value: false } + } + loop_condition: { + CALL [22] { + function: @not_strictly_false + args: { + CALL [23] { + function: !_ + args: { + IDENT [24] { + name: @ac:0:0 + } + } + } + } } - IDENT [24] { - name: @index6 + } + loop_step: { + CALL [25] { + function: _||_ + args: { + IDENT [26] { + name: @ac:0:0 + } + CALL [27] { + function: _>_ + args: { + CALL [28] { + function: _-_ + args: { + CALL [29] { + function: _-_ + args: { + IDENT [30] { + name: @it:0:0 + } + IDENT [31] { + name: @it2:0:0 + } + } + } + CONSTANT [32] { value: 1 } + } + } + CONSTANT [33] { value: 3 } + } + } + } + } + } + result: { + IDENT [34] { + name: @ac:0:0 } } } - CREATE_LIST [25] { + IDENT [35] { + name: @index2 + } + } + } + } +} +Test case: MACRO_SHADOWED_VARIABLE_COMP_V2_2 +Source: ["foo", "bar"].transformMap(x, y, [x + x, y + y]).transformMap(x, y, [x + x, y + y]) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { elements: { - CONSTANT [26] { value: "foo" } - CONSTANT [27] { value: "bar" } + CONSTANT [4] { value: "foo" } + CONSTANT [5] { value: "bar" } } } } } - COMPREHENSION [28] { - iter_var: @c0:0 + COMPREHENSION [6] { + iter_var: @it:1:0 + iter_var2: @it2:1:0 iter_range: { - COMPREHENSION [29] { - iter_var: @c1:0 + COMPREHENSION [7] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 iter_range: { - IDENT [30] { - name: @index8 + IDENT [8] { + name: @index0 } } - accu_var: @x1:0 + accu_var: @ac:0:0 accu_init: { - CREATE_LIST [31] { - elements: { - } + MAP [9] { + } } loop_condition: { - CONSTANT [32] { value: true } + CONSTANT [10] { value: true } } loop_step: { - IDENT [33] { - name: @index7 + CALL [11] { + function: cel.@mapInsert + args: { + IDENT [12] { + name: @ac:0:0 + } + IDENT [13] { + name: @it:0:0 + } + LIST [14] { + elements: { + CALL [15] { + function: _+_ + args: { + IDENT [16] { + name: @it:0:0 + } + IDENT [17] { + name: @it:0:0 + } + } + } + CALL [18] { + function: _+_ + args: { + IDENT [19] { + name: @it2:0:0 + } + IDENT [20] { + name: @it2:0:0 + } + } + } + } + } + } } } result: { - IDENT [34] { - name: @x1:0 + IDENT [21] { + name: @ac:0:0 } } } } - accu_var: @x0:0 + accu_var: @ac:1:0 accu_init: { - CREATE_LIST [35] { - elements: { - } + MAP [22] { + } } loop_condition: { - CONSTANT [36] { value: true } + CONSTANT [23] { value: true } } loop_step: { - IDENT [37] { - name: @index4 + CALL [24] { + function: cel.@mapInsert + args: { + IDENT [25] { + name: @ac:1:0 + } + IDENT [26] { + name: @it:1:0 + } + LIST [27] { + elements: { + CALL [28] { + function: _+_ + args: { + IDENT [29] { + name: @it:1:0 + } + IDENT [30] { + name: @it:1:0 + } + } + } + CALL [31] { + function: _+_ + args: { + IDENT [32] { + name: @it2:1:0 + } + IDENT [33] { + name: @it2:1:0 + } + } + } + } + } + } } } result: { - IDENT [38] { - name: @x0:0 + IDENT [34] { + name: @ac:1:0 } } } @@ -2808,9 +4948,9 @@ Source: has({'a': true}.a) && {'a':true}['a'] CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_MAP [3] { + MAP [3] { MAP_ENTRY [4] { key: { CONSTANT [5] { value: "a" } @@ -2820,26 +4960,67 @@ CALL [1] { } } } - CALL [7] { + SELECT [7] { + IDENT [8] { + name: @index0 + }.a~presence_test + } + CALL [9] { function: _[_] args: { - IDENT [8] { + IDENT [10] { name: @index0 } - CONSTANT [9] { value: "a" } + CONSTANT [11] { value: "a" } } } } } - CALL [10] { + CALL [12] { function: _&&_ args: { - SELECT [11] { - IDENT [12] { + IDENT [13] { + name: @index1 + } + IDENT [14] { + name: @index2 + } + } + } + } +} +Test case: PRESENCE_TEST_2 +Source: has({'a': true}.a) && has({'a': true}.a) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + MAP [3] { + MAP_ENTRY [4] { + key: { + CONSTANT [5] { value: "a" } + } + value: { + CONSTANT [6] { value: true } + } + } + } + SELECT [7] { + IDENT [8] { name: @index0 }.a~presence_test } - IDENT [13] { + } + } + CALL [9] { + function: _&&_ + args: { + IDENT [10] { + name: @index1 + } + IDENT [11] { name: @index1 } } @@ -2852,7 +5033,7 @@ Source: (has(msg.oneof_type.payload) ? msg.oneof_type.payload.single_int64 : 0) CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { @@ -2862,33 +5043,39 @@ CALL [1] { SELECT [5] { IDENT [6] { name: @index0 - }.payload + }.payload~presence_test } SELECT [7] { IDENT [8] { - name: @index1 + name: @index0 + }.payload + } + SELECT [9] { + IDENT [10] { + name: @index2 }.single_int64 } - } - } - CALL [9] { - function: _==_ - args: { - CALL [10] { + CALL [11] { function: _?_:_ args: { - SELECT [11] { - IDENT [12] { - name: @index0 - }.payload~presence_test + IDENT [12] { + name: @index1 } IDENT [13] { - name: @index2 + name: @index3 } CONSTANT [14] { value: 0 } } } - CONSTANT [15] { value: 10 } + } + } + CALL [15] { + function: _==_ + args: { + IDENT [16] { + name: @index4 + } + CONSTANT [17] { value: 10 } } } } @@ -2899,7 +5086,7 @@ Source: (has(msg.oneof_type.payload) ? msg.oneof_type.payload.single_int64 : msg CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { @@ -2916,37 +5103,43 @@ CALL [1] { name: @index1 }.single_int64 } - CALL [9] { + SELECT [9] { + IDENT [10] { + name: @index0 + }.payload~presence_test + } + CALL [11] { function: _*_ args: { - IDENT [10] { + IDENT [12] { name: @index2 } - CONSTANT [11] { value: 0 } + CONSTANT [13] { value: 0 } } } - } - } - CALL [12] { - function: _==_ - args: { - CALL [13] { + CALL [14] { function: _?_:_ args: { - SELECT [14] { - IDENT [15] { - name: @index0 - }.payload~presence_test + IDENT [15] { + name: @index3 } IDENT [16] { name: @index2 } IDENT [17] { - name: @index3 + name: @index4 } } } - CONSTANT [18] { value: 10 } + } + } + CALL [18] { + function: _==_ + args: { + IDENT [19] { + name: @index5 + } + CONSTANT [20] { value: 10 } } } } @@ -2957,7 +5150,7 @@ Source: (has(msg.oneof_type.payload.single_int64) ? msg.oneof_type.payload.singl CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { @@ -2974,37 +5167,43 @@ CALL [1] { name: @index1 }.single_int64 } - CALL [9] { + SELECT [9] { + IDENT [10] { + name: @index1 + }.single_int64~presence_test + } + CALL [11] { function: _*_ args: { - IDENT [10] { + IDENT [12] { name: @index2 } - CONSTANT [11] { value: 0 } + CONSTANT [13] { value: 0 } } } - } - } - CALL [12] { - function: _==_ - args: { - CALL [13] { + CALL [14] { function: _?_:_ args: { - SELECT [14] { - IDENT [15] { - name: @index1 - }.single_int64~presence_test + IDENT [15] { + name: @index3 } IDENT [16] { name: @index2 } IDENT [17] { - name: @index3 + name: @index4 } } } - CONSTANT [18] { value: 10 } + } + } + CALL [18] { + function: _==_ + args: { + IDENT [19] { + name: @index5 + } + CONSTANT [20] { value: 10 } } } } @@ -3015,7 +5214,7 @@ Source: (has(msg.oneof_type) && has(msg.oneof_type.payload) && has(msg.oneof_typ CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { @@ -3034,73 +5233,100 @@ CALL [1] { } SELECT [9] { IDENT [10] { - name: @index2 - }.key + name: msg + }.oneof_type~presence_test } - CALL [11] { - function: _==_ + SELECT [11] { + IDENT [12] { + name: @index0 + }.payload~presence_test + } + CALL [13] { + function: _&&_ args: { - IDENT [12] { + IDENT [14] { name: @index3 } - CONSTANT [13] { value: "A" } + IDENT [15] { + name: @index4 + } } } - } - } - CALL [14] { - function: _?_:_ - args: { - CALL [15] { + SELECT [16] { + IDENT [17] { + name: @index1 + }.single_int64~presence_test + } + CALL [18] { function: _&&_ args: { - CALL [16] { - function: _&&_ - args: { - SELECT [17] { - IDENT [18] { - name: msg - }.oneof_type~presence_test - } - SELECT [19] { - IDENT [20] { - name: @index0 - }.payload~presence_test - } - } + IDENT [19] { + name: @index5 } - SELECT [21] { - IDENT [22] { - name: @index1 - }.single_int64~presence_test + IDENT [20] { + name: @index6 } } } - CALL [23] { + SELECT [21] { + IDENT [22] { + name: @index1 + }.map_string_string~presence_test + } + SELECT [23] { + IDENT [24] { + name: @index2 + }.key~presence_test + } + CALL [25] { + function: _&&_ + args: { + IDENT [26] { + name: @index8 + } + IDENT [27] { + name: @index9 + } + } + } + SELECT [28] { + IDENT [29] { + name: @index2 + }.key + } + CALL [30] { + function: _==_ + args: { + IDENT [31] { + name: @index11 + } + CONSTANT [32] { value: "A" } + } + } + CALL [33] { function: _?_:_ args: { - CALL [24] { - function: _&&_ - args: { - SELECT [25] { - IDENT [26] { - name: @index1 - }.map_string_string~presence_test - } - SELECT [27] { - IDENT [28] { - name: @index2 - }.key~presence_test - } - } + IDENT [34] { + name: @index10 } - IDENT [29] { - name: @index4 + IDENT [35] { + name: @index12 } - CONSTANT [30] { value: false } + CONSTANT [36] { value: false } } } - CONSTANT [31] { value: false } + } + } + CALL [37] { + function: _?_:_ + args: { + IDENT [38] { + name: @index7 + } + IDENT [39] { + name: @index13 + } + CONSTANT [40] { value: false } } } } @@ -3111,14 +5337,14 @@ Source: [10, ?optional.none(), [?optional.none(), ?opt_x], [?optional.none(), ?o CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { function: optional.none args: { } } - CREATE_LIST [4] { + LIST [4] { elements: { IDENT [5] { name: @index0 @@ -3129,36 +5355,36 @@ CALL [1] { } optional_indices: [0, 1] } - CREATE_LIST [7] { + LIST [7] { elements: { CONSTANT [8] { value: 5 } } } - CREATE_LIST [9] { + LIST [9] { elements: { CONSTANT [10] { value: 10 } IDENT [11] { - name: @index2 + name: @index0 } IDENT [12] { - name: @index2 + name: @index1 + } + IDENT [13] { + name: @index1 } } + optional_indices: [0] } - CREATE_LIST [13] { + LIST [14] { elements: { - CONSTANT [14] { value: 10 } - IDENT [15] { - name: @index0 - } + CONSTANT [15] { value: 10 } IDENT [16] { - name: @index1 + name: @index2 } IDENT [17] { - name: @index1 + name: @index2 } } - optional_indices: [0] } } } @@ -3166,10 +5392,10 @@ CALL [1] { function: _==_ args: { IDENT [19] { - name: @index4 + name: @index3 } IDENT [20] { - name: @index3 + name: @index4 } } } @@ -3181,7 +5407,7 @@ Source: {?'hello': optional.of('hello')}['hello'] + {?'hello': optional.of('hell CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { function: optional.of @@ -3189,7 +5415,7 @@ CALL [1] { CONSTANT [4] { value: "hello" } } } - CREATE_MAP [5] { + MAP [5] { MAP_ENTRY [6] { key: { CONSTANT [7] { value: "hello" } @@ -3241,9 +5467,9 @@ Source: {?'key': optional.of('test')}[?'bogus'].or({'key': 'test'}[?'bogus']).or CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_MAP [3] { + MAP [3] { MAP_ENTRY [4] { key: { CONSTANT [5] { value: "key" } @@ -3254,74 +5480,74 @@ CALL [1] { } } CALL [7] { - function: _[_] - args: { - IDENT [8] { - name: @index0 - } - CONSTANT [9] { value: "key" } - } - } - CALL [10] { - function: _[?_] - args: { - IDENT [11] { - name: @index0 - } - CONSTANT [12] { value: "bogus" } - } - } - CALL [13] { function: optional.of args: { - CONSTANT [14] { value: "test" } + CONSTANT [8] { value: "test" } } } - CREATE_MAP [15] { - MAP_ENTRY [16] { + MAP [9] { + MAP_ENTRY [10] { key: { - CONSTANT [17] { value: "key" } + CONSTANT [11] { value: "key" } } optional_entry: true value: { - IDENT [18] { - name: @index3 + IDENT [12] { + name: @index1 } } } } - CALL [19] { + CALL [13] { function: _[?_] args: { - IDENT [20] { - name: @index4 + IDENT [14] { + name: @index2 } - CONSTANT [21] { value: "bogus" } + CONSTANT [15] { value: "bogus" } } } - CALL [22] { + CALL [16] { + function: _[?_] + args: { + IDENT [17] { + name: @index0 + } + CONSTANT [18] { value: "bogus" } + } + } + CALL [19] { function: or target: { - IDENT [23] { - name: @index5 + IDENT [20] { + name: @index3 } } args: { - IDENT [24] { - name: @index2 + IDENT [21] { + name: @index4 + } + } + } + CALL [22] { + function: _[_] + args: { + IDENT [23] { + name: @index0 } + CONSTANT [24] { value: "key" } } } CALL [25] { function: orValue target: { IDENT [26] { - name: @index6 + name: @index5 } } args: { IDENT [27] { - name: @index1 + name: @index6 } } } @@ -3344,7 +5570,7 @@ Source: TestAllTypes{?single_int64: optional.ofNonZeroValue(1), ?single_int32: o CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { function: optional.ofNonZeroValue @@ -3358,8 +5584,8 @@ CALL [1] { CONSTANT [6] { value: 4 } } } - CREATE_STRUCT [7] { - name: TestAllTypes + STRUCT [7] { + name: cel.expr.conformance.proto3.TestAllTypes entries: { ENTRY [8] { field_key: single_int64 @@ -3384,21 +5610,21 @@ CALL [1] { SELECT [12] { IDENT [13] { name: @index2 - }.single_int64 + }.single_int32 } SELECT [14] { IDENT [15] { name: @index2 - }.single_int32 + }.single_int64 } CALL [16] { function: _+_ args: { IDENT [17] { - name: @index4 + name: @index3 } IDENT [18] { - name: @index3 + name: @index4 } } } @@ -3421,7 +5647,7 @@ Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('h' + 'e' + 'l' + 'l' + CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { function: _+_ @@ -3489,7 +5715,7 @@ Source: 'hello world'.matches('h' + 'e' + 'l' + 'l' + 'o') CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { function: _+_ @@ -3546,7 +5772,7 @@ Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('hello') CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { function: _+_ @@ -3612,13 +5838,13 @@ Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('w' + 'o' + 'r' + 'l' + CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { function: _+_ args: { - CONSTANT [4] { value: "w" } - CONSTANT [5] { value: "o" } + CONSTANT [4] { value: "h" } + CONSTANT [5] { value: "e" } } } CALL [6] { @@ -3627,7 +5853,7 @@ CALL [1] { IDENT [7] { name: @index0 } - CONSTANT [8] { value: "r" } + CONSTANT [8] { value: "l" } } } CALL [9] { @@ -3645,23 +5871,23 @@ CALL [1] { IDENT [13] { name: @index2 } - CONSTANT [14] { value: "d" } + CONSTANT [14] { value: "o" } } } CALL [15] { function: _+_ args: { - CONSTANT [16] { value: "h" } - CONSTANT [17] { value: "e" } + IDENT [16] { + name: @index3 + } + CONSTANT [17] { value: " world" } } } CALL [18] { function: _+_ args: { - IDENT [19] { - name: @index4 - } - CONSTANT [20] { value: "l" } + CONSTANT [19] { value: "w" } + CONSTANT [20] { value: "o" } } } CALL [21] { @@ -3670,7 +5896,7 @@ CALL [1] { IDENT [22] { name: @index5 } - CONSTANT [23] { value: "l" } + CONSTANT [23] { value: "r" } } } CALL [24] { @@ -3679,7 +5905,7 @@ CALL [1] { IDENT [25] { name: @index6 } - CONSTANT [26] { value: "o" } + CONSTANT [26] { value: "l" } } } CALL [27] { @@ -3688,7 +5914,7 @@ CALL [1] { IDENT [28] { name: @index7 } - CONSTANT [29] { value: " world" } + CONSTANT [29] { value: "d" } } } } @@ -3697,12 +5923,12 @@ CALL [1] { function: matches target: { IDENT [31] { - name: @index8 + name: @index4 } } args: { IDENT [32] { - name: @index3 + name: @index8 } } } @@ -3714,7 +5940,7 @@ Source: non_pure_custom_func(msg.oneof_type.payload.single_int64) + non_pure_cus CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { @@ -3731,60 +5957,54 @@ CALL [1] { name: @index1 }.single_int64 } - SELECT [9] { - IDENT [10] { - name: msg - }.single_int64 - } - SELECT [11] { - IDENT [12] { - name: @index1 - }.single_int32 - } } } - CALL [13] { + CALL [9] { function: _+_ args: { - CALL [14] { + CALL [10] { function: _+_ args: { - CALL [15] { + CALL [11] { function: _+_ args: { - CALL [16] { + CALL [12] { function: non_pure_custom_func args: { - IDENT [17] { + IDENT [13] { name: @index2 } } } - CALL [18] { + CALL [14] { function: non_pure_custom_func args: { - IDENT [19] { - name: @index4 + SELECT [15] { + IDENT [16] { + name: @index1 + }.single_int32 } } } } } - CALL [20] { + CALL [17] { function: non_pure_custom_func args: { - IDENT [21] { + IDENT [18] { name: @index2 } } } } } - CALL [22] { + CALL [19] { function: non_pure_custom_func args: { - IDENT [23] { - name: @index3 + SELECT [20] { + IDENT [21] { + name: msg + }.single_int64 } } } @@ -3798,7 +6018,7 @@ Source: pure_custom_func(msg.oneof_type.payload.single_int64) + pure_custom_func CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { @@ -3825,8 +6045,8 @@ CALL [1] { } SELECT [11] { IDENT [12] { - name: msg - }.single_int64 + name: @index1 + }.single_int32 } CALL [13] { function: pure_custom_func @@ -3836,38 +6056,38 @@ CALL [1] { } } } - SELECT [15] { - IDENT [16] { - name: @index1 - }.single_int32 - } - CALL [17] { - function: pure_custom_func + CALL [15] { + function: _+_ args: { - IDENT [18] { - name: @index6 + IDENT [16] { + name: @index3 + } + IDENT [17] { + name: @index5 } } } - CALL [19] { + CALL [18] { function: _+_ args: { + IDENT [19] { + name: @index6 + } IDENT [20] { name: @index3 } - IDENT [21] { - name: @index7 - } } } - CALL [22] { - function: _+_ + SELECT [21] { + IDENT [22] { + name: msg + }.single_int64 + } + CALL [23] { + function: pure_custom_func args: { - IDENT [23] { - name: @index8 - } IDENT [24] { - name: @index3 + name: @index8 } } } @@ -3877,12 +6097,12 @@ CALL [1] { function: _+_ args: { IDENT [26] { - name: @index9 + name: @index7 } IDENT [27] { - name: @index5 + name: @index9 } } } } -} +} \ No newline at end of file diff --git a/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_2.baseline b/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_2.baseline index ff087237a..317bc87aa 100644 --- a/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_2.baseline +++ b/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_2.baseline @@ -4,48 +4,45 @@ Source: size([1,2]) + size([1,2]) + 1 == 5 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - CONSTANT [5] { value: 2 } - } - } - CALL [6] { + CALL [3] { function: size args: { - IDENT [7] { - name: @index0 + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + CONSTANT [6] { value: 2 } + } } } } - CALL [8] { + CALL [7] { function: _+_ args: { - CALL [9] { + CALL [8] { function: _+_ args: { - IDENT [10] { - name: @index1 + IDENT [9] { + name: @index0 } - IDENT [11] { - name: @index1 + IDENT [10] { + name: @index0 } } } - CONSTANT [12] { value: 1 } + CONSTANT [11] { value: 1 } } } } } - CALL [13] { + CALL [12] { function: _==_ args: { - IDENT [14] { - name: @index2 + IDENT [13] { + name: @index1 } - CONSTANT [15] { value: 5 } + CONSTANT [14] { value: 5 } } } } @@ -56,57 +53,51 @@ Source: 2 + size([1,2]) + size([1,2]) + 1 == 7 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - CONSTANT [5] { value: 2 } - } - } - CALL [6] { + CALL [3] { function: size args: { - IDENT [7] { - name: @index0 + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + CONSTANT [6] { value: 2 } + } } } } - CALL [8] { + CALL [7] { function: _+_ args: { - CALL [9] { + CALL [8] { function: _+_ args: { - CONSTANT [10] { value: 2 } - IDENT [11] { - name: @index1 + CONSTANT [9] { value: 2 } + IDENT [10] { + name: @index0 } } } - IDENT [12] { - name: @index1 + IDENT [11] { + name: @index0 } } } + } + } + CALL [12] { + function: _==_ + args: { CALL [13] { function: _+_ args: { IDENT [14] { - name: @index2 + name: @index1 } CONSTANT [15] { value: 1 } } } - } - } - CALL [16] { - function: _==_ - args: { - IDENT [17] { - name: @index3 - } - CONSTANT [18] { value: 7 } + CONSTANT [16] { value: 7 } } } } @@ -117,74 +108,65 @@ Source: size([0]) + size([0]) + size([1,2]) + size([1,2]) == 6 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 0 } - } - } - CALL [5] { + CALL [3] { function: size args: { - IDENT [6] { - name: @index0 + LIST [4] { + elements: { + CONSTANT [5] { value: 0 } + } } } } - CREATE_LIST [7] { - elements: { - CONSTANT [8] { value: 1 } - CONSTANT [9] { value: 2 } - } - } - CALL [10] { + CALL [6] { function: size args: { - IDENT [11] { - name: @index2 + LIST [7] { + elements: { + CONSTANT [8] { value: 1 } + CONSTANT [9] { value: 2 } + } } } } - CALL [12] { + CALL [10] { function: _+_ args: { - CALL [13] { + CALL [11] { function: _+_ args: { - IDENT [14] { - name: @index1 + IDENT [12] { + name: @index0 } - IDENT [15] { - name: @index1 + IDENT [13] { + name: @index0 } } } - IDENT [16] { - name: @index3 + IDENT [14] { + name: @index1 } } } - CALL [17] { + } + } + CALL [15] { + function: _==_ + args: { + CALL [16] { function: _+_ args: { - IDENT [18] { - name: @index4 + IDENT [17] { + name: @index2 } - IDENT [19] { - name: @index3 + IDENT [18] { + name: @index1 } } } - } - } - CALL [20] { - function: _==_ - args: { - IDENT [21] { - name: @index5 - } - CONSTANT [22] { value: 6 } + CONSTANT [19] { value: 6 } } } } @@ -195,114 +177,105 @@ Source: 5 + size([0]) + size([0]) + size([1,2]) + size([1,2]) + size([1,2,3]) + CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 0 } - } - } - CALL [5] { + CALL [3] { function: size args: { - IDENT [6] { - name: @index0 + LIST [4] { + elements: { + CONSTANT [5] { value: 0 } + } } } } - CREATE_LIST [7] { - elements: { - CONSTANT [8] { value: 1 } - CONSTANT [9] { value: 2 } - } - } - CALL [10] { + CALL [6] { function: size args: { - IDENT [11] { - name: @index2 + LIST [7] { + elements: { + CONSTANT [8] { value: 1 } + CONSTANT [9] { value: 2 } + } } } } - CREATE_LIST [12] { - elements: { - CONSTANT [13] { value: 1 } - CONSTANT [14] { value: 2 } - CONSTANT [15] { value: 3 } - } - } - CALL [16] { + CALL [10] { function: size args: { - IDENT [17] { - name: @index4 + LIST [11] { + elements: { + CONSTANT [12] { value: 1 } + CONSTANT [13] { value: 2 } + CONSTANT [14] { value: 3 } + } } } } - CALL [18] { + CALL [15] { function: _+_ args: { - CALL [19] { + CALL [16] { function: _+_ args: { - CONSTANT [20] { value: 5 } - IDENT [21] { - name: @index1 + CONSTANT [17] { value: 5 } + IDENT [18] { + name: @index0 } } } - IDENT [22] { - name: @index1 + IDENT [19] { + name: @index0 } } } - CALL [23] { + CALL [20] { function: _+_ args: { - CALL [24] { + CALL [21] { function: _+_ args: { - IDENT [25] { - name: @index6 - } - IDENT [26] { + IDENT [22] { name: @index3 } + IDENT [23] { + name: @index1 + } } } - IDENT [27] { - name: @index3 + IDENT [24] { + name: @index1 } } } - CALL [28] { + CALL [25] { function: _+_ args: { - CALL [29] { + CALL [26] { function: _+_ args: { - IDENT [30] { - name: @index7 + IDENT [27] { + name: @index4 } - IDENT [31] { - name: @index5 + IDENT [28] { + name: @index2 } } } - IDENT [32] { - name: @index5 + IDENT [29] { + name: @index2 } } } } } - CALL [33] { + CALL [30] { function: _==_ args: { - IDENT [34] { - name: @index8 + IDENT [31] { + name: @index5 } - CONSTANT [35] { value: 17 } + CONSTANT [32] { value: 17 } } } } @@ -313,73 +286,79 @@ Source: timestamp(int(timestamp(1000000000))).getFullYear() + timestamp(int(time CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { - function: timestamp - args: { - CONSTANT [4] { value: 1000000000 } - } - } - CALL [5] { function: int args: { - IDENT [6] { - name: @index0 - } - } - } - CALL [7] { - function: timestamp - args: { - IDENT [8] { - name: @index1 + CALL [4] { + function: timestamp + args: { + CONSTANT [5] { value: 1000000000 } + } } } } - CALL [9] { + CALL [6] { function: getFullYear target: { - IDENT [10] { - name: @index2 + CALL [7] { + function: timestamp + args: { + IDENT [8] { + name: @index0 + } + } } } args: { } } - CALL [11] { - function: timestamp + CALL [9] { + function: int args: { - CONSTANT [12] { value: 50 } + CALL [10] { + function: timestamp + args: { + CONSTANT [11] { value: 50 } + } + } } } - CALL [13] { + CALL [12] { function: int args: { - IDENT [14] { - name: @index4 + CALL [13] { + function: timestamp + args: { + CONSTANT [14] { value: 200 } + } } } } CALL [15] { - function: timestamp - args: { - IDENT [16] { - name: @index5 + function: getFullYear + target: { + CALL [16] { + function: timestamp + args: { + IDENT [17] { + name: @index3 + } + } } } - } - CALL [17] { - function: timestamp args: { - CONSTANT [18] { value: 200 } } } - CALL [19] { + CALL [18] { function: int args: { - IDENT [20] { - name: @index7 + CALL [19] { + function: timestamp + args: { + CONSTANT [20] { value: 75 } + } } } } @@ -387,83 +366,29 @@ CALL [1] { function: timestamp args: { IDENT [22] { - name: @index8 + name: @index2 } } } CALL [23] { - function: getFullYear - target: { - IDENT [24] { - name: @index9 - } - } - args: { - } - } - CALL [25] { - function: timestamp - args: { - CONSTANT [26] { value: 75 } - } - } - CALL [27] { - function: int - args: { - IDENT [28] { - name: @index11 - } - } - } - CALL [29] { function: timestamp args: { - IDENT [30] { - name: @index12 - } - } - } - CALL [31] { - function: getMinutes - target: { - IDENT [32] { - name: @index13 - } - } - args: { - } - } - CALL [33] { - function: getSeconds - target: { - IDENT [34] { - name: @index6 - } - } - args: { - } - } - CALL [35] { - function: getFullYear - target: { - IDENT [36] { - name: @index6 + IDENT [24] { + name: @index5 } } - args: { - } } - CALL [37] { + CALL [25] { function: _+_ args: { - IDENT [38] { - name: @index3 + IDENT [26] { + name: @index1 } - CALL [39] { + CALL [27] { function: getFullYear target: { - IDENT [40] { - name: @index13 + IDENT [28] { + name: @index7 } } args: { @@ -471,83 +396,104 @@ CALL [1] { } } } - CALL [41] { + CALL [29] { function: _+_ args: { - CALL [42] { - function: _+_ - args: { - IDENT [43] { - name: @index17 - } - IDENT [44] { - name: @index16 + IDENT [30] { + name: @index8 + } + CALL [31] { + function: getFullYear + target: { + IDENT [32] { + name: @index6 } } - } - IDENT [45] { - name: @index3 + args: { + } } } } - CALL [46] { + CALL [33] { function: _+_ args: { - CALL [47] { + CALL [34] { function: _+_ args: { - IDENT [48] { - name: @index18 + IDENT [35] { + name: @index9 } - IDENT [49] { - name: @index15 + IDENT [36] { + name: @index1 } } } - IDENT [50] { - name: @index10 + CALL [37] { + function: getSeconds + target: { + IDENT [38] { + name: @index6 + } + } + args: { + } } } } - CALL [51] { + CALL [39] { function: _+_ args: { - CALL [52] { + CALL [40] { function: _+_ args: { - IDENT [53] { - name: @index19 - } - IDENT [54] { + IDENT [41] { name: @index10 } + IDENT [42] { + name: @index4 + } } } - IDENT [55] { - name: @index14 + IDENT [43] { + name: @index4 } } } - CALL [56] { + CALL [44] { function: _+_ args: { - IDENT [57] { - name: @index20 + IDENT [45] { + name: @index11 } - IDENT [58] { - name: @index3 + CALL [46] { + function: getMinutes + target: { + IDENT [47] { + name: @index7 + } + } + args: { + } } } } } } - CALL [59] { + CALL [48] { function: _==_ args: { - IDENT [60] { - name: @index21 + CALL [49] { + function: _+_ + args: { + IDENT [50] { + name: @index12 + } + IDENT [51] { + name: @index1 + } + } } - CONSTANT [61] { value: 13934 } + CONSTANT [52] { value: 13934 } } } } @@ -558,41 +504,38 @@ Source: {"a": 2}["a"] + {"a": 2}["a"] * {"a": 2}["a"] == 6 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_MAP [3] { - MAP_ENTRY [4] { - key: { - CONSTANT [5] { value: "a" } - } - value: { - CONSTANT [6] { value: 2 } - } - } - } - CALL [7] { + CALL [3] { function: _[_] args: { - IDENT [8] { - name: @index0 + MAP [4] { + MAP_ENTRY [5] { + key: { + CONSTANT [6] { value: "a" } + } + value: { + CONSTANT [7] { value: 2 } + } + } } - CONSTANT [9] { value: "a" } + CONSTANT [8] { value: "a" } } } - CALL [10] { + CALL [9] { function: _+_ args: { - IDENT [11] { - name: @index1 + IDENT [10] { + name: @index0 } - CALL [12] { + CALL [11] { function: _*_ args: { - IDENT [13] { - name: @index1 + IDENT [12] { + name: @index0 } - IDENT [14] { - name: @index1 + IDENT [13] { + name: @index0 } } } @@ -600,13 +543,13 @@ CALL [1] { } } } - CALL [15] { + CALL [14] { function: _==_ args: { - IDENT [16] { - name: @index2 + IDENT [15] { + name: @index1 } - CONSTANT [17] { value: 6 } + CONSTANT [16] { value: 6 } } } } @@ -617,56 +560,53 @@ Source: {'a': {'b': 1}, 'c': {'b': 1}, 'd': {'e': {'b': 1}}, 'e': {'e': {'b': 1} CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_MAP [3] { + MAP [3] { MAP_ENTRY [4] { key: { - CONSTANT [5] { value: "b" } + CONSTANT [5] { value: "e" } } value: { - CONSTANT [6] { value: 1 } + MAP [6] { + MAP_ENTRY [7] { + key: { + CONSTANT [8] { value: "b" } + } + value: { + CONSTANT [9] { value: 1 } + } + } + } } } } - CREATE_MAP [7] { - MAP_ENTRY [8] { + MAP [10] { + MAP_ENTRY [11] { key: { - CONSTANT [9] { value: "e" } + CONSTANT [12] { value: "b" } } value: { - IDENT [10] { - name: @index0 - } + CONSTANT [13] { value: 1 } } } } } } - CREATE_MAP [11] { - MAP_ENTRY [12] { - key: { - CONSTANT [13] { value: "a" } - } - value: { - IDENT [14] { - name: @index0 - } - } - } + MAP [14] { MAP_ENTRY [15] { key: { - CONSTANT [16] { value: "c" } + CONSTANT [16] { value: "a" } } value: { IDENT [17] { - name: @index0 + name: @index1 } } } MAP_ENTRY [18] { key: { - CONSTANT [19] { value: "d" } + CONSTANT [19] { value: "c" } } value: { IDENT [20] { @@ -676,11 +616,21 @@ CALL [1] { } MAP_ENTRY [21] { key: { - CONSTANT [22] { value: "e" } + CONSTANT [22] { value: "d" } } value: { IDENT [23] { - name: @index1 + name: @index0 + } + } + } + MAP_ENTRY [24] { + key: { + CONSTANT [25] { value: "e" } + } + value: { + IDENT [26] { + name: @index0 } } } @@ -693,9 +643,9 @@ Source: [1, [1,2,3,4], 2, [1,2,3,4], 5, [1,2,3,4], 7, [[1,2], [1,2,3,4]], [1,2]] CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { + LIST [3] { elements: { CONSTANT [4] { value: 1 } CONSTANT [5] { value: 2 } @@ -703,43 +653,40 @@ CALL [1] { CONSTANT [7] { value: 4 } } } - CREATE_LIST [8] { + LIST [8] { elements: { CONSTANT [9] { value: 1 } CONSTANT [10] { value: 2 } } } - CREATE_LIST [11] { - elements: { - IDENT [12] { - name: @index1 - } - IDENT [13] { - name: @index0 - } - } - } } } - CREATE_LIST [14] { + LIST [11] { elements: { - CONSTANT [15] { value: 1 } - IDENT [16] { + CONSTANT [12] { value: 1 } + IDENT [13] { name: @index0 } - CONSTANT [17] { value: 2 } - IDENT [18] { + CONSTANT [14] { value: 2 } + IDENT [15] { name: @index0 } - CONSTANT [19] { value: 5 } - IDENT [20] { + CONSTANT [16] { value: 5 } + IDENT [17] { name: @index0 } - CONSTANT [21] { value: 7 } - IDENT [22] { - name: @index2 + CONSTANT [18] { value: 7 } + LIST [19] { + elements: { + IDENT [20] { + name: @index1 + } + IDENT [21] { + name: @index0 + } + } } - IDENT [23] { + IDENT [22] { name: @index1 } } @@ -752,33 +699,30 @@ Source: msg.single_int64 + msg.single_int64 == 6 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { name: msg }.single_int64 } - CALL [5] { + } + } + CALL [5] { + function: _==_ + args: { + CALL [6] { function: _+_ args: { - IDENT [6] { + IDENT [7] { name: @index0 } - IDENT [7] { + IDENT [8] { name: @index0 } } } - } - } - CALL [8] { - function: _==_ - args: { - IDENT [9] { - name: @index1 - } - CONSTANT [10] { value: 6 } + CONSTANT [9] { value: 6 } } } } @@ -789,92 +733,83 @@ Source: msg.oneof_type.payload.single_int64 + msg.oneof_type.payload.single_int3 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 - }.single_int64 - } - SELECT [9] { - SELECT [10] { - IDENT [11] { - name: @index1 + SELECT [4] { + IDENT [5] { + name: msg }.oneof_type }.payload } - SELECT [12] { - IDENT [13] { - name: @index3 - }.single_int64 - } - SELECT [14] { - IDENT [15] { - name: msg + SELECT [6] { + IDENT [7] { + name: @index0 }.single_int64 } - CALL [16] { + CALL [8] { function: _+_ args: { - IDENT [17] { - name: @index2 + IDENT [9] { + name: @index1 } - SELECT [18] { - IDENT [19] { - name: @index1 + SELECT [10] { + IDENT [11] { + name: @index0 }.single_int32 } } } - CALL [20] { + CALL [12] { function: _+_ args: { - CALL [21] { + CALL [13] { function: _+_ args: { - IDENT [22] { - name: @index6 - } - IDENT [23] { + IDENT [14] { name: @index2 } + IDENT [15] { + name: @index1 + } } } - IDENT [24] { - name: @index5 + SELECT [16] { + IDENT [17] { + name: msg + }.single_int64 } } } - CALL [25] { + SELECT [18] { + SELECT [19] { + IDENT [20] { + name: @index0 + }.oneof_type + }.payload + } + CALL [21] { function: _+_ args: { - IDENT [26] { - name: @index7 + IDENT [22] { + name: @index3 } - IDENT [27] { - name: @index4 + SELECT [23] { + IDENT [24] { + name: @index4 + }.single_int64 } } } } } - CALL [28] { + CALL [25] { function: _==_ args: { - IDENT [29] { - name: @index8 + IDENT [26] { + name: @index5 } - CONSTANT [30] { value: 31 } + CONSTANT [27] { value: 31 } } } } @@ -885,79 +820,70 @@ Source: true || msg.oneof_type.payload.oneof_type.payload.oneof_type.payload.one CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 + SELECT [4] { + IDENT [5] { + name: msg + }.oneof_type }.payload } - SELECT [7] { - IDENT [8] { - name: @index1 - }.oneof_type + SELECT [6] { + SELECT [7] { + IDENT [8] { + name: @index0 + }.oneof_type + }.payload } SELECT [9] { IDENT [10] { - name: @index2 - }.payload + name: @index1 + }.oneof_type } SELECT [11] { - IDENT [12] { - name: @index3 + SELECT [12] { + IDENT [13] { + name: @index2 + }.payload }.oneof_type } - SELECT [13] { - SELECT [14] { - IDENT [15] { - name: @index4 - }.child - }.child - } - SELECT [16] { - SELECT [17] { - IDENT [18] { - name: @index5 + SELECT [14] { + SELECT [15] { + IDENT [16] { + name: @index3 }.payload }.single_bool } - SELECT [19] { - SELECT [20] { - IDENT [21] { - name: @index4 - }.payload - }.oneof_type + SELECT [17] { + SELECT [18] { + IDENT [19] { + name: @index2 + }.child + }.child } - SELECT [22] { - SELECT [23] { - IDENT [24] { - name: @index7 + SELECT [20] { + SELECT [21] { + IDENT [22] { + name: @index5 }.payload }.single_bool } - CALL [25] { - function: _||_ - args: { - CONSTANT [26] { value: true } - IDENT [27] { - name: @index8 - } - } - } } } - CALL [28] { + CALL [23] { function: _||_ args: { - IDENT [29] { - name: @index9 + CALL [24] { + function: _||_ + args: { + CONSTANT [25] { value: true } + IDENT [26] { + name: @index4 + } + } } - IDENT [30] { + IDENT [27] { name: @index6 } } @@ -970,60 +896,54 @@ Source: msg.oneof_type.payload.map_int32_int64[1] + msg.oneof_type.payload.map_i CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 + SELECT [4] { + IDENT [5] { + name: msg + }.oneof_type }.payload } - SELECT [7] { - IDENT [8] { - name: @index1 - }.map_int32_int64 - } - CALL [9] { + CALL [6] { function: _[_] args: { - IDENT [10] { - name: @index2 + SELECT [7] { + IDENT [8] { + name: @index0 + }.map_int32_int64 } - CONSTANT [11] { value: 1 } + CONSTANT [9] { value: 1 } } } - CALL [12] { + CALL [10] { function: _+_ args: { - CALL [13] { + CALL [11] { function: _+_ args: { - IDENT [14] { - name: @index3 + IDENT [12] { + name: @index1 } - IDENT [15] { - name: @index3 + IDENT [13] { + name: @index1 } } } - IDENT [16] { - name: @index3 + IDENT [14] { + name: @index1 } } } } } - CALL [17] { + CALL [15] { function: _==_ args: { - IDENT [18] { - name: @index4 + IDENT [16] { + name: @index2 } - CONSTANT [19] { value: 15 } + CONSTANT [17] { value: 15 } } } } @@ -1034,75 +954,69 @@ Source: msg.oneof_type.payload.map_int32_int64[0] + msg.oneof_type.payload.map_i CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 + SELECT [4] { + IDENT [5] { + name: msg + }.oneof_type }.payload } - SELECT [7] { - IDENT [8] { - name: @index1 + SELECT [6] { + IDENT [7] { + name: @index0 }.map_int32_int64 } - CALL [9] { - function: _[_] - args: { - IDENT [10] { - name: @index2 - } - CONSTANT [11] { value: 2 } - } - } - CALL [12] { + CALL [8] { function: _+_ args: { - CALL [13] { + CALL [9] { function: _[_] args: { - IDENT [14] { - name: @index2 + IDENT [10] { + name: @index1 } - CONSTANT [15] { value: 0 } + CONSTANT [11] { value: 0 } } } - CALL [16] { + CALL [12] { function: _[_] args: { - IDENT [17] { - name: @index2 + IDENT [13] { + name: @index1 } - CONSTANT [18] { value: 1 } + CONSTANT [14] { value: 1 } } } } } - CALL [19] { + CALL [15] { function: _+_ args: { - IDENT [20] { - name: @index4 + IDENT [16] { + name: @index2 } - IDENT [21] { - name: @index3 + CALL [17] { + function: _[_] + args: { + IDENT [18] { + name: @index1 + } + CONSTANT [19] { value: 2 } + } } } } } } - CALL [22] { + CALL [20] { function: _==_ args: { - IDENT [23] { - name: @index5 + IDENT [21] { + name: @index3 } - CONSTANT [24] { value: 8 } + CONSTANT [22] { value: 8 } } } } @@ -1113,7 +1027,7 @@ Source: msg.oneof_type.payload.oneof_type.payload.oneof_type.payload.oneof_type. CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { SELECT [4] { @@ -1158,7 +1072,7 @@ Source: (msg.single_int64 > 0 ? msg.single_int64 : 0) == 3 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { @@ -1202,7 +1116,7 @@ Source: false ? false : (msg.single_int64) + ((msg.single_int64 + 1) * 2) == 11 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { @@ -1261,7 +1175,7 @@ Source: (msg.single_int64 > 0 ? (msg.single_int32 > 0 ? msg.single_int64 + msg.s CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { @@ -1336,97 +1250,57 @@ Source: size([[1].exists(i, i > 0)]) + size([[1].exists(j, j > 0)]) + size([[2]. CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { + LIST [3] { elements: { CONSTANT [4] { value: 1 } } } - CALL [5] { - function: _>_ - args: { - IDENT [6] { - name: @c0:0 - } - CONSTANT [7] { value: 0 } - } - } - CALL [8] { - function: _||_ - args: { - IDENT [9] { - name: @x0:0 - } - IDENT [10] { - name: @index1 - } - } - } - CREATE_LIST [11] { + LIST [5] { elements: { - CONSTANT [12] { value: 2 } - } - } - CALL [13] { - function: _>_ - args: { - IDENT [14] { - name: @c0:0 - } - CONSTANT [15] { value: 1 } - } - } - CALL [16] { - function: _||_ - args: { - IDENT [17] { - name: @x0:0 - } - IDENT [18] { - name: @index4 - } + CONSTANT [6] { value: 2 } } } } } - CALL [19] { + CALL [7] { function: _==_ args: { - CALL [20] { + CALL [8] { function: _+_ args: { - CALL [21] { + CALL [9] { function: _+_ args: { - CALL [22] { + CALL [10] { function: _+_ args: { - CALL [23] { + CALL [11] { function: size args: { - CREATE_LIST [24] { + LIST [12] { elements: { - COMPREHENSION [25] { - iter_var: @c0:0 + COMPREHENSION [13] { + iter_var: @it:0:0 iter_range: { - IDENT [26] { + IDENT [14] { name: @index0 } } - accu_var: @x0:0 + accu_var: @ac:0:0 accu_init: { - CONSTANT [27] { value: false } + CONSTANT [15] { value: false } } loop_condition: { - CALL [28] { + CALL [16] { function: @not_strictly_false args: { - CALL [29] { + CALL [17] { function: !_ args: { - IDENT [30] { - name: @x0:0 + IDENT [18] { + name: @ac:0:0 } } } @@ -1434,13 +1308,27 @@ CALL [1] { } } loop_step: { - IDENT [31] { - name: @index2 + CALL [19] { + function: _||_ + args: { + IDENT [20] { + name: @ac:0:0 + } + CALL [21] { + function: _>_ + args: { + IDENT [22] { + name: @it:0:0 + } + CONSTANT [23] { value: 0 } + } + } + } } } result: { - IDENT [32] { - name: @x0:0 + IDENT [24] { + name: @ac:0:0 } } } @@ -1448,31 +1336,31 @@ CALL [1] { } } } - CALL [33] { + CALL [25] { function: size args: { - CREATE_LIST [34] { + LIST [26] { elements: { - COMPREHENSION [35] { - iter_var: @c0:0 + COMPREHENSION [27] { + iter_var: @it:0:0 iter_range: { - IDENT [36] { + IDENT [28] { name: @index0 } } - accu_var: @x0:0 + accu_var: @ac:0:0 accu_init: { - CONSTANT [37] { value: false } + CONSTANT [29] { value: false } } loop_condition: { - CALL [38] { + CALL [30] { function: @not_strictly_false args: { - CALL [39] { + CALL [31] { function: !_ args: { - IDENT [40] { - name: @x0:0 + IDENT [32] { + name: @ac:0:0 } } } @@ -1480,47 +1368,61 @@ CALL [1] { } } loop_step: { - IDENT [41] { - name: @index2 - } - } - result: { - IDENT [42] { - name: @x0:0 - } - } - } - } + CALL [33] { + function: _||_ + args: { + IDENT [34] { + name: @ac:0:0 + } + CALL [35] { + function: _>_ + args: { + IDENT [36] { + name: @it:0:0 + } + CONSTANT [37] { value: 0 } + } + } + } + } + } + result: { + IDENT [38] { + name: @ac:0:0 + } + } + } + } } } } } } - CALL [43] { + CALL [39] { function: size args: { - CREATE_LIST [44] { + LIST [40] { elements: { - COMPREHENSION [45] { - iter_var: @c0:0 + COMPREHENSION [41] { + iter_var: @it:0:0 iter_range: { - IDENT [46] { - name: @index3 + IDENT [42] { + name: @index1 } } - accu_var: @x0:0 + accu_var: @ac:0:0 accu_init: { - CONSTANT [47] { value: false } + CONSTANT [43] { value: false } } loop_condition: { - CALL [48] { + CALL [44] { function: @not_strictly_false args: { - CALL [49] { + CALL [45] { function: !_ args: { - IDENT [50] { - name: @x0:0 + IDENT [46] { + name: @ac:0:0 } } } @@ -1528,13 +1430,27 @@ CALL [1] { } } loop_step: { - IDENT [51] { - name: @index5 + CALL [47] { + function: _||_ + args: { + IDENT [48] { + name: @ac:0:0 + } + CALL [49] { + function: _>_ + args: { + IDENT [50] { + name: @it:0:0 + } + CONSTANT [51] { value: 1 } + } + } + } } } result: { IDENT [52] { - name: @x0:0 + name: @ac:0:0 } } } @@ -1547,16 +1463,16 @@ CALL [1] { CALL [53] { function: size args: { - CREATE_LIST [54] { + LIST [54] { elements: { COMPREHENSION [55] { - iter_var: @c0:0 + iter_var: @it:0:0 iter_range: { IDENT [56] { - name: @index3 + name: @index1 } } - accu_var: @x0:0 + accu_var: @ac:0:0 accu_init: { CONSTANT [57] { value: false } } @@ -1568,7 +1484,7 @@ CALL [1] { function: !_ args: { IDENT [60] { - name: @x0:0 + name: @ac:0:0 } } } @@ -1576,13 +1492,27 @@ CALL [1] { } } loop_step: { - IDENT [61] { - name: @index5 + CALL [61] { + function: _||_ + args: { + IDENT [62] { + name: @ac:0:0 + } + CALL [63] { + function: _>_ + args: { + IDENT [64] { + name: @it:0:0 + } + CONSTANT [65] { value: 1 } + } + } + } } } result: { - IDENT [62] { - name: @x0:0 + IDENT [66] { + name: @ac:0:0 } } } @@ -1592,7 +1522,7 @@ CALL [1] { } } } - CONSTANT [63] { value: 4 } + CONSTANT [67] { value: 4 } } } } @@ -1603,102 +1533,62 @@ Source: [[1].exists(i, i > 0)] + [[1].exists(j, j > 0)] + [['a'].exists(k, k == CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { + LIST [3] { elements: { CONSTANT [4] { value: 1 } } } - CALL [5] { - function: _>_ - args: { - IDENT [6] { - name: @c0:0 - } - CONSTANT [7] { value: 0 } - } - } - CALL [8] { - function: _||_ - args: { - IDENT [9] { - name: @x0:0 - } - IDENT [10] { - name: @index1 - } - } - } - CREATE_LIST [11] { + LIST [5] { elements: { - CONSTANT [12] { value: "a" } - } - } - CALL [13] { - function: _==_ - args: { - IDENT [14] { - name: @c0:1 - } - CONSTANT [15] { value: "a" } - } - } - CALL [16] { - function: _||_ - args: { - IDENT [17] { - name: @x0:1 - } - IDENT [18] { - name: @index4 - } + CONSTANT [6] { value: "a" } } } - CREATE_LIST [19] { + LIST [7] { elements: { - CONSTANT [20] { value: true } - CONSTANT [21] { value: true } - CONSTANT [22] { value: true } - CONSTANT [23] { value: true } + CONSTANT [8] { value: true } + CONSTANT [9] { value: true } + CONSTANT [10] { value: true } + CONSTANT [11] { value: true } } } } } - CALL [24] { + CALL [12] { function: _==_ args: { - CALL [25] { + CALL [13] { function: _+_ args: { - CALL [26] { + CALL [14] { function: _+_ args: { - CALL [27] { + CALL [15] { function: _+_ args: { - CREATE_LIST [28] { + LIST [16] { elements: { - COMPREHENSION [29] { - iter_var: @c0:0 + COMPREHENSION [17] { + iter_var: @it:0:0 iter_range: { - IDENT [30] { + IDENT [18] { name: @index0 } } - accu_var: @x0:0 + accu_var: @ac:0:0 accu_init: { - CONSTANT [31] { value: false } + CONSTANT [19] { value: false } } loop_condition: { - CALL [32] { + CALL [20] { function: @not_strictly_false args: { - CALL [33] { + CALL [21] { function: !_ args: { - IDENT [34] { - name: @x0:0 + IDENT [22] { + name: @ac:0:0 } } } @@ -1706,40 +1596,54 @@ CALL [1] { } } loop_step: { - IDENT [35] { - name: @index2 + CALL [23] { + function: _||_ + args: { + IDENT [24] { + name: @ac:0:0 + } + CALL [25] { + function: _>_ + args: { + IDENT [26] { + name: @it:0:0 + } + CONSTANT [27] { value: 0 } + } + } + } } } result: { - IDENT [36] { - name: @x0:0 + IDENT [28] { + name: @ac:0:0 } } } } } - CREATE_LIST [37] { + LIST [29] { elements: { - COMPREHENSION [38] { - iter_var: @c0:0 + COMPREHENSION [30] { + iter_var: @it:0:0 iter_range: { - IDENT [39] { + IDENT [31] { name: @index0 } } - accu_var: @x0:0 + accu_var: @ac:0:0 accu_init: { - CONSTANT [40] { value: false } + CONSTANT [32] { value: false } } loop_condition: { - CALL [41] { + CALL [33] { function: @not_strictly_false args: { - CALL [42] { + CALL [34] { function: !_ args: { - IDENT [43] { - name: @x0:0 + IDENT [35] { + name: @ac:0:0 } } } @@ -1747,13 +1651,27 @@ CALL [1] { } } loop_step: { - IDENT [44] { - name: @index2 + CALL [36] { + function: _||_ + args: { + IDENT [37] { + name: @ac:0:0 + } + CALL [38] { + function: _>_ + args: { + IDENT [39] { + name: @it:0:0 + } + CONSTANT [40] { value: 0 } + } + } + } } } result: { - IDENT [45] { - name: @x0:0 + IDENT [41] { + name: @ac:0:0 } } } @@ -1761,28 +1679,28 @@ CALL [1] { } } } - CREATE_LIST [46] { + LIST [42] { elements: { - COMPREHENSION [47] { - iter_var: @c0:1 + COMPREHENSION [43] { + iter_var: @it:0:1 iter_range: { - IDENT [48] { - name: @index3 + IDENT [44] { + name: @index1 } } - accu_var: @x0:1 + accu_var: @ac:0:1 accu_init: { - CONSTANT [49] { value: false } + CONSTANT [45] { value: false } } loop_condition: { - CALL [50] { + CALL [46] { function: @not_strictly_false args: { - CALL [51] { + CALL [47] { function: !_ args: { - IDENT [52] { - name: @x0:1 + IDENT [48] { + name: @ac:0:1 } } } @@ -1790,13 +1708,27 @@ CALL [1] { } } loop_step: { - IDENT [53] { - name: @index5 + CALL [49] { + function: _||_ + args: { + IDENT [50] { + name: @ac:0:1 + } + CALL [51] { + function: _==_ + args: { + IDENT [52] { + name: @it:0:1 + } + CONSTANT [53] { value: "a" } + } + } + } } } result: { IDENT [54] { - name: @x0:1 + name: @ac:0:1 } } } @@ -1804,16 +1736,16 @@ CALL [1] { } } } - CREATE_LIST [55] { + LIST [55] { elements: { COMPREHENSION [56] { - iter_var: @c0:1 + iter_var: @it:0:1 iter_range: { IDENT [57] { - name: @index3 + name: @index1 } } - accu_var: @x0:1 + accu_var: @ac:0:1 accu_init: { CONSTANT [58] { value: false } } @@ -1825,7 +1757,7 @@ CALL [1] { function: !_ args: { IDENT [61] { - name: @x0:1 + name: @ac:0:1 } } } @@ -1833,13 +1765,27 @@ CALL [1] { } } loop_step: { - IDENT [62] { - name: @index5 + CALL [62] { + function: _||_ + args: { + IDENT [63] { + name: @ac:0:1 + } + CALL [64] { + function: _==_ + args: { + IDENT [65] { + name: @it:0:1 + } + CONSTANT [66] { value: "a" } + } + } + } } } result: { - IDENT [63] { - name: @x0:1 + IDENT [67] { + name: @ac:0:1 } } } @@ -1847,683 +1793,2774 @@ CALL [1] { } } } - IDENT [64] { - name: @index6 + IDENT [68] { + name: @index2 } } } } } -Test case: NESTED_MACROS -Source: [1,2,3].map(i, [1, 2, 3].map(i, i + 1)) == [[2, 3, 4], [2, 3, 4], [2, 3, 4]] +Test case: MULTIPLE_MACROS_3 +Source: [1].exists(i, i > 0) && [1].exists(j, j > 0) && [1].exists(k, k > 1) && [2].exists(l, l > 1) =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { + LIST [3] { elements: { CONSTANT [4] { value: 1 } - CONSTANT [5] { value: 2 } - CONSTANT [6] { value: 3 } - } - } - CREATE_LIST [7] { - elements: { - CONSTANT [8] { value: 2 } - CONSTANT [9] { value: 3 } - CONSTANT [10] { value: 4 } } } - CREATE_LIST [11] { + LIST [5] { elements: { - IDENT [12] { - name: @index1 - } - IDENT [13] { - name: @index1 - } - IDENT [14] { - name: @index1 - } + CONSTANT [6] { value: 2 } } } - CREATE_LIST [15] { - elements: { - CALL [16] { - function: _+_ - args: { - IDENT [17] { - name: @c1:0 + } + } + CALL [7] { + function: _&&_ + args: { + CALL [8] { + function: _&&_ + args: { + COMPREHENSION [9] { + iter_var: @it:0:0 + iter_range: { + IDENT [10] { + name: @index0 } - CONSTANT [18] { value: 1 } } - } - } - } - COMPREHENSION [19] { - iter_var: @c1:0 - iter_range: { - IDENT [20] { - name: @index0 - } - } - accu_var: @x1:0 - accu_init: { - CREATE_LIST [21] { - elements: { + accu_var: @ac:0:0 + accu_init: { + CONSTANT [11] { value: false } } - } - } - loop_condition: { - CONSTANT [22] { value: true } - } - loop_step: { - CALL [23] { - function: _+_ - args: { - IDENT [24] { - name: @x1:0 + loop_condition: { + CALL [12] { + function: @not_strictly_false + args: { + CALL [13] { + function: !_ + args: { + IDENT [14] { + name: @ac:0:0 + } + } + } + } } - IDENT [25] { - name: @index3 + } + loop_step: { + CALL [15] { + function: _||_ + args: { + IDENT [16] { + name: @ac:0:0 + } + CALL [17] { + function: _>_ + args: { + IDENT [18] { + name: @it:0:0 + } + CONSTANT [19] { value: 0 } + } + } + } + } + } + result: { + IDENT [20] { + name: @ac:0:0 } } } - } - result: { - IDENT [26] { - name: @x1:0 + COMPREHENSION [21] { + iter_var: @it:0:0 + iter_range: { + IDENT [22] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [23] { value: false } + } + loop_condition: { + CALL [24] { + function: @not_strictly_false + args: { + CALL [25] { + function: !_ + args: { + IDENT [26] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [27] { + function: _||_ + args: { + IDENT [28] { + name: @ac:0:0 + } + CALL [29] { + function: _>_ + args: { + IDENT [30] { + name: @it:0:0 + } + CONSTANT [31] { value: 0 } + } + } + } + } + } + result: { + IDENT [32] { + name: @ac:0:0 + } + } } } } - CALL [27] { - function: _+_ + CALL [33] { + function: _&&_ args: { - IDENT [28] { - name: @x0:0 - } - CREATE_LIST [29] { - elements: { - IDENT [30] { - name: @index4 + COMPREHENSION [34] { + iter_var: @it:0:0 + iter_range: { + IDENT [35] { + name: @index0 } } - } - } - } - COMPREHENSION [31] { - iter_var: @c0:0 - iter_range: { - IDENT [32] { - name: @index0 - } - } - accu_var: @x0:0 - accu_init: { - CREATE_LIST [33] { - elements: { + accu_var: @ac:0:0 + accu_init: { + CONSTANT [36] { value: false } + } + loop_condition: { + CALL [37] { + function: @not_strictly_false + args: { + CALL [38] { + function: !_ + args: { + IDENT [39] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [40] { + function: _||_ + args: { + IDENT [41] { + name: @ac:0:0 + } + CALL [42] { + function: _>_ + args: { + IDENT [43] { + name: @it:0:0 + } + CONSTANT [44] { value: 1 } + } + } + } + } + } + result: { + IDENT [45] { + name: @ac:0:0 + } } } - } - loop_condition: { - CONSTANT [34] { value: true } - } - loop_step: { - IDENT [35] { - name: @index5 + COMPREHENSION [46] { + iter_var: @it:0:0 + iter_range: { + IDENT [47] { + name: @index1 + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [48] { value: false } + } + loop_condition: { + CALL [49] { + function: @not_strictly_false + args: { + CALL [50] { + function: !_ + args: { + IDENT [51] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [52] { + function: _||_ + args: { + IDENT [53] { + name: @ac:0:0 + } + CALL [54] { + function: _>_ + args: { + IDENT [55] { + name: @it:0:0 + } + CONSTANT [56] { value: 1 } + } + } + } + } + } + result: { + IDENT [57] { + name: @ac:0:0 + } + } + } + } + } + } + } + } +} +Test case: MULTIPLE_MACROS_COMP_V2_1 +Source: size([[1].exists(i,x,i > 0 && x >= 0)]) + size([[1].exists(j, y, j > 0 && y >= 0)]) + size([[2].exists(k,z,k > 1 && z >= 0)]) + size([[2].exists(l, w, l > 1 && w >= 0)]) == 4 +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + } + } + LIST [5] { + elements: { + CONSTANT [6] { value: 2 } + } + } + } + } + CALL [7] { + function: _==_ + args: { + CALL [8] { + function: _+_ + args: { + CALL [9] { + function: _+_ + args: { + CALL [10] { + function: _+_ + args: { + CALL [11] { + function: size + args: { + LIST [12] { + elements: { + COMPREHENSION [13] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + IDENT [14] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [15] { value: false } + } + loop_condition: { + CALL [16] { + function: @not_strictly_false + args: { + CALL [17] { + function: !_ + args: { + IDENT [18] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [19] { + function: _||_ + args: { + IDENT [20] { + name: @ac:0:0 + } + CALL [21] { + function: _&&_ + args: { + CALL [22] { + function: _>_ + args: { + IDENT [23] { + name: @it:0:0 + } + CONSTANT [24] { value: 0 } + } + } + CALL [25] { + function: _>=_ + args: { + IDENT [26] { + name: @it2:0:0 + } + CONSTANT [27] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [28] { + name: @ac:0:0 + } + } + } + } + } + } + } + CALL [29] { + function: size + args: { + LIST [30] { + elements: { + COMPREHENSION [31] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + IDENT [32] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [33] { value: false } + } + loop_condition: { + CALL [34] { + function: @not_strictly_false + args: { + CALL [35] { + function: !_ + args: { + IDENT [36] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [37] { + function: _||_ + args: { + IDENT [38] { + name: @ac:0:0 + } + CALL [39] { + function: _&&_ + args: { + CALL [40] { + function: _>_ + args: { + IDENT [41] { + name: @it:0:0 + } + CONSTANT [42] { value: 0 } + } + } + CALL [43] { + function: _>=_ + args: { + IDENT [44] { + name: @it2:0:0 + } + CONSTANT [45] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [46] { + name: @ac:0:0 + } + } + } + } + } + } + } + } + } + CALL [47] { + function: size + args: { + LIST [48] { + elements: { + COMPREHENSION [49] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + IDENT [50] { + name: @index1 + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [51] { value: false } + } + loop_condition: { + CALL [52] { + function: @not_strictly_false + args: { + CALL [53] { + function: !_ + args: { + IDENT [54] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [55] { + function: _||_ + args: { + IDENT [56] { + name: @ac:0:0 + } + CALL [57] { + function: _&&_ + args: { + CALL [58] { + function: _>_ + args: { + IDENT [59] { + name: @it:0:0 + } + CONSTANT [60] { value: 1 } + } + } + CALL [61] { + function: _>=_ + args: { + IDENT [62] { + name: @it2:0:0 + } + CONSTANT [63] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [64] { + name: @ac:0:0 + } + } + } + } + } + } + } + } + } + CALL [65] { + function: size + args: { + LIST [66] { + elements: { + COMPREHENSION [67] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + IDENT [68] { + name: @index1 + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [69] { value: false } + } + loop_condition: { + CALL [70] { + function: @not_strictly_false + args: { + CALL [71] { + function: !_ + args: { + IDENT [72] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [73] { + function: _||_ + args: { + IDENT [74] { + name: @ac:0:0 + } + CALL [75] { + function: _&&_ + args: { + CALL [76] { + function: _>_ + args: { + IDENT [77] { + name: @it:0:0 + } + CONSTANT [78] { value: 1 } + } + } + CALL [79] { + function: _>=_ + args: { + IDENT [80] { + name: @it2:0:0 + } + CONSTANT [81] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [82] { + name: @ac:0:0 + } + } + } + } + } + } + } + } + } + CONSTANT [83] { value: 4 } + } + } + } +} +Test case: MULTIPLE_MACROS_COMP_V2_2 +Source: [1].exists(i, x, i > 0 && x > 0) && [1].exists(j, y, j > 0 && y > 0) && [1].exists(k, z, k > 1 && z > 0) && [2].exists(l, w, l > 1 && w > 0) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + } + } + LIST [5] { + elements: { + CONSTANT [6] { value: 2 } + } + } + } + } + CALL [7] { + function: _&&_ + args: { + CALL [8] { + function: _&&_ + args: { + COMPREHENSION [9] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + IDENT [10] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [11] { value: false } + } + loop_condition: { + CALL [12] { + function: @not_strictly_false + args: { + CALL [13] { + function: !_ + args: { + IDENT [14] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [15] { + function: _||_ + args: { + IDENT [16] { + name: @ac:0:0 + } + CALL [17] { + function: _&&_ + args: { + CALL [18] { + function: _>_ + args: { + IDENT [19] { + name: @it:0:0 + } + CONSTANT [20] { value: 0 } + } + } + CALL [21] { + function: _>_ + args: { + IDENT [22] { + name: @it2:0:0 + } + CONSTANT [23] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [24] { + name: @ac:0:0 + } + } + } + COMPREHENSION [25] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + IDENT [26] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [27] { value: false } + } + loop_condition: { + CALL [28] { + function: @not_strictly_false + args: { + CALL [29] { + function: !_ + args: { + IDENT [30] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [31] { + function: _||_ + args: { + IDENT [32] { + name: @ac:0:0 + } + CALL [33] { + function: _&&_ + args: { + CALL [34] { + function: _>_ + args: { + IDENT [35] { + name: @it:0:0 + } + CONSTANT [36] { value: 0 } + } + } + CALL [37] { + function: _>_ + args: { + IDENT [38] { + name: @it2:0:0 + } + CONSTANT [39] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [40] { + name: @ac:0:0 + } + } + } + } + } + CALL [41] { + function: _&&_ + args: { + COMPREHENSION [42] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + IDENT [43] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [44] { value: false } + } + loop_condition: { + CALL [45] { + function: @not_strictly_false + args: { + CALL [46] { + function: !_ + args: { + IDENT [47] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [48] { + function: _||_ + args: { + IDENT [49] { + name: @ac:0:0 + } + CALL [50] { + function: _&&_ + args: { + CALL [51] { + function: _>_ + args: { + IDENT [52] { + name: @it:0:0 + } + CONSTANT [53] { value: 1 } + } + } + CALL [54] { + function: _>_ + args: { + IDENT [55] { + name: @it2:0:0 + } + CONSTANT [56] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [57] { + name: @ac:0:0 + } + } + } + COMPREHENSION [58] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + IDENT [59] { + name: @index1 + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [60] { value: false } + } + loop_condition: { + CALL [61] { + function: @not_strictly_false + args: { + CALL [62] { + function: !_ + args: { + IDENT [63] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [64] { + function: _||_ + args: { + IDENT [65] { + name: @ac:0:0 + } + CALL [66] { + function: _&&_ + args: { + CALL [67] { + function: _>_ + args: { + IDENT [68] { + name: @it:0:0 + } + CONSTANT [69] { value: 1 } + } + } + CALL [70] { + function: _>_ + args: { + IDENT [71] { + name: @it2:0:0 + } + CONSTANT [72] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [73] { + name: @ac:0:0 + } + } + } + } + } + } + } + } +} +Test case: NESTED_MACROS +Source: [1,2,3].map(i, [1, 2, 3].map(i, i + 1)) == [[2, 3, 4], [2, 3, 4], [2, 3, 4]] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + CONSTANT [5] { value: 2 } + CONSTANT [6] { value: 3 } + } + } + LIST [7] { + elements: { + CONSTANT [8] { value: 2 } + CONSTANT [9] { value: 3 } + CONSTANT [10] { value: 4 } + } + } + LIST [11] { + elements: { + IDENT [12] { + name: @index1 + } + IDENT [13] { + name: @index1 + } + IDENT [14] { + name: @index1 + } + } + } + } + } + CALL [15] { + function: _==_ + args: { + COMPREHENSION [16] { + iter_var: @it:1:0 + iter_range: { + IDENT [17] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [18] { + elements: { + } + } + } + loop_condition: { + CONSTANT [19] { value: true } + } + loop_step: { + CALL [20] { + function: _+_ + args: { + IDENT [21] { + name: @ac:1:0 + } + LIST [22] { + elements: { + COMPREHENSION [23] { + iter_var: @it:0:0 + iter_range: { + IDENT [24] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [25] { + elements: { + } + } + } + loop_condition: { + CONSTANT [26] { value: true } + } + loop_step: { + CALL [27] { + function: _+_ + args: { + IDENT [28] { + name: @ac:0:0 + } + LIST [29] { + elements: { + CALL [30] { + function: _+_ + args: { + IDENT [31] { + name: @it:0:0 + } + CONSTANT [32] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [33] { + name: @ac:0:0 + } + } + } + } + } + } + } + } + result: { + IDENT [34] { + name: @ac:1:0 + } + } + } + IDENT [35] { + name: @index2 + } + } + } + } +} +Test case: NESTED_MACROS_2 +Source: [1, 2].map(y, [1, 2, 3].filter(x, x == y)) == [[1], [2]] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + } + } + LIST [6] { + elements: { + CONSTANT [7] { value: 2 } + } + } + } + } + LIST [8] { + elements: { + CONSTANT [9] { value: 1 } + CONSTANT [10] { value: 2 } + } + } + LIST [11] { + elements: { + CONSTANT [12] { value: 1 } + CONSTANT [13] { value: 2 } + CONSTANT [14] { value: 3 } + } + } + } + } + CALL [15] { + function: _==_ + args: { + COMPREHENSION [16] { + iter_var: @it:1:0 + iter_range: { + IDENT [17] { + name: @index1 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [18] { + elements: { + } + } + } + loop_condition: { + CONSTANT [19] { value: true } + } + loop_step: { + CALL [20] { + function: _+_ + args: { + IDENT [21] { + name: @ac:1:0 + } + LIST [22] { + elements: { + COMPREHENSION [23] { + iter_var: @it:0:0 + iter_range: { + IDENT [24] { + name: @index2 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [25] { + elements: { + } + } + } + loop_condition: { + CONSTANT [26] { value: true } + } + loop_step: { + CALL [27] { + function: _?_:_ + args: { + CALL [28] { + function: _==_ + args: { + IDENT [29] { + name: @it:0:0 + } + IDENT [30] { + name: @it:1:0 + } + } + } + CALL [31] { + function: _+_ + args: { + IDENT [32] { + name: @ac:0:0 + } + LIST [33] { + elements: { + IDENT [34] { + name: @it:0:0 + } + } + } + } + } + IDENT [35] { + name: @ac:0:0 + } + } + } + } + result: { + IDENT [36] { + name: @ac:0:0 + } + } + } + } + } + } + } + } + result: { + IDENT [37] { + name: @ac:1:0 + } + } + } + IDENT [38] { + name: @index0 + } + } + } + } +} +Test case: NESTED_MACROS_3 +Source: [1,2,3].map(i, i * 2) + [1,2,3].map(i, i * 2).map(i, i * 2) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + CONSTANT [5] { value: 2 } + CONSTANT [6] { value: 3 } + } + } + } + } + CALL [7] { + function: _+_ + args: { + COMPREHENSION [8] { + iter_var: @it:0:0 + iter_range: { + IDENT [9] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [10] { + elements: { + } + } + } + loop_condition: { + CONSTANT [11] { value: true } + } + loop_step: { + CALL [12] { + function: _+_ + args: { + IDENT [13] { + name: @ac:0:0 + } + LIST [14] { + elements: { + CALL [15] { + function: _*_ + args: { + IDENT [16] { + name: @it:0:0 + } + CONSTANT [17] { value: 2 } + } + } + } + } + } + } + } + result: { + IDENT [18] { + name: @ac:0:0 + } + } + } + COMPREHENSION [19] { + iter_var: @it:1:0 + iter_range: { + COMPREHENSION [20] { + iter_var: @it:0:0 + iter_range: { + IDENT [21] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [22] { + elements: { + } + } + } + loop_condition: { + CONSTANT [23] { value: true } + } + loop_step: { + CALL [24] { + function: _+_ + args: { + IDENT [25] { + name: @ac:0:0 + } + LIST [26] { + elements: { + CALL [27] { + function: _*_ + args: { + IDENT [28] { + name: @it:0:0 + } + CONSTANT [29] { value: 2 } + } + } + } + } + } + } + } + result: { + IDENT [30] { + name: @ac:0:0 + } + } + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [31] { + elements: { + } + } + } + loop_condition: { + CONSTANT [32] { value: true } + } + loop_step: { + CALL [33] { + function: _+_ + args: { + IDENT [34] { + name: @ac:1:0 + } + LIST [35] { + elements: { + CALL [36] { + function: _*_ + args: { + IDENT [37] { + name: @it:1:0 + } + CONSTANT [38] { value: 2 } + } + } + } + } + } + } + } + result: { + IDENT [39] { + name: @ac:1:0 + } + } + } + } + } + } +} +Test case: NESTED_MACROS_COMP_V2_1 +Source: [1,2,3].transformList(i, v, [1, 2, 3].transformList(i, v, i + v + 1)) == [[2, 4, 6], [2, 4, 6], [2, 4, 6]] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + CONSTANT [5] { value: 2 } + CONSTANT [6] { value: 3 } + } + } + LIST [7] { + elements: { + CONSTANT [8] { value: 2 } + CONSTANT [9] { value: 4 } + CONSTANT [10] { value: 6 } + } + } + LIST [11] { + elements: { + IDENT [12] { + name: @index1 + } + IDENT [13] { + name: @index1 + } + IDENT [14] { + name: @index1 + } + } + } + } + } + CALL [15] { + function: _==_ + args: { + COMPREHENSION [16] { + iter_var: @it:1:0 + iter_var2: @it2:1:0 + iter_range: { + IDENT [17] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [18] { + elements: { + } + } + } + loop_condition: { + CONSTANT [19] { value: true } + } + loop_step: { + CALL [20] { + function: _+_ + args: { + IDENT [21] { + name: @ac:1:0 + } + LIST [22] { + elements: { + COMPREHENSION [23] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + IDENT [24] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [25] { + elements: { + } + } + } + loop_condition: { + CONSTANT [26] { value: true } + } + loop_step: { + CALL [27] { + function: _+_ + args: { + IDENT [28] { + name: @ac:0:0 + } + LIST [29] { + elements: { + CALL [30] { + function: _+_ + args: { + CALL [31] { + function: _+_ + args: { + IDENT [32] { + name: @it:0:0 + } + IDENT [33] { + name: @it2:0:0 + } + } + } + CONSTANT [34] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [35] { + name: @ac:0:0 + } + } + } + } + } + } + } + } + result: { + IDENT [36] { + name: @ac:1:0 + } + } + } + IDENT [37] { + name: @index2 + } + } + } + } +} +Test case: NESTED_MACROS_COMP_V2_2 +Source: [1, 2].transformList(i, y, [1, 2, 3].filter(x, x == y && i < y)) == [[1], [2]] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + } + } + LIST [6] { + elements: { + CONSTANT [7] { value: 2 } + } + } + } + } + LIST [8] { + elements: { + CONSTANT [9] { value: 1 } + CONSTANT [10] { value: 2 } + } + } + LIST [11] { + elements: { + CONSTANT [12] { value: 1 } + CONSTANT [13] { value: 2 } + CONSTANT [14] { value: 3 } + } + } + } + } + CALL [15] { + function: _==_ + args: { + COMPREHENSION [16] { + iter_var: @it:1:0 + iter_var2: @it2:1:0 + iter_range: { + IDENT [17] { + name: @index1 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [18] { + elements: { + } + } + } + loop_condition: { + CONSTANT [19] { value: true } + } + loop_step: { + CALL [20] { + function: _+_ + args: { + IDENT [21] { + name: @ac:1:0 + } + LIST [22] { + elements: { + COMPREHENSION [23] { + iter_var: @it:0:0 + iter_range: { + IDENT [24] { + name: @index2 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [25] { + elements: { + } + } + } + loop_condition: { + CONSTANT [26] { value: true } + } + loop_step: { + CALL [27] { + function: _?_:_ + args: { + CALL [28] { + function: _&&_ + args: { + CALL [29] { + function: _==_ + args: { + IDENT [30] { + name: @it:0:0 + } + IDENT [31] { + name: @it2:1:0 + } + } + } + CALL [32] { + function: _<_ + args: { + IDENT [33] { + name: @it:1:0 + } + IDENT [34] { + name: @it2:1:0 + } + } + } + } + } + CALL [35] { + function: _+_ + args: { + IDENT [36] { + name: @ac:0:0 + } + LIST [37] { + elements: { + IDENT [38] { + name: @it:0:0 + } + } + } + } + } + IDENT [39] { + name: @ac:0:0 + } + } + } + } + result: { + IDENT [40] { + name: @ac:0:0 + } + } + } + } + } + } + } + } + result: { + IDENT [41] { + name: @ac:1:0 + } + } + } + IDENT [42] { + name: @index0 + } + } + } + } +} +Test case: ADJACENT_NESTED_MACROS +Source: [1,2,3].map(i, [1, 2, 3].map(i, i + 1)) == [1,2,3].map(j, [1, 2, 3].map(j, j + 1)) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + CONSTANT [5] { value: 2 } + CONSTANT [6] { value: 3 } + } + } + } + } + CALL [7] { + function: _==_ + args: { + COMPREHENSION [8] { + iter_var: @it:1:0 + iter_range: { + IDENT [9] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [10] { + elements: { + } + } + } + loop_condition: { + CONSTANT [11] { value: true } + } + loop_step: { + CALL [12] { + function: _+_ + args: { + IDENT [13] { + name: @ac:1:0 + } + LIST [14] { + elements: { + COMPREHENSION [15] { + iter_var: @it:0:0 + iter_range: { + IDENT [16] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [17] { + elements: { + } + } + } + loop_condition: { + CONSTANT [18] { value: true } + } + loop_step: { + CALL [19] { + function: _+_ + args: { + IDENT [20] { + name: @ac:0:0 + } + LIST [21] { + elements: { + CALL [22] { + function: _+_ + args: { + IDENT [23] { + name: @it:0:0 + } + CONSTANT [24] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [25] { + name: @ac:0:0 + } + } + } + } + } + } + } + } + result: { + IDENT [26] { + name: @ac:1:0 + } + } + } + COMPREHENSION [27] { + iter_var: @it:1:0 + iter_range: { + IDENT [28] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [29] { + elements: { + } + } + } + loop_condition: { + CONSTANT [30] { value: true } + } + loop_step: { + CALL [31] { + function: _+_ + args: { + IDENT [32] { + name: @ac:1:0 + } + LIST [33] { + elements: { + COMPREHENSION [34] { + iter_var: @it:0:0 + iter_range: { + IDENT [35] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [36] { + elements: { + } + } + } + loop_condition: { + CONSTANT [37] { value: true } + } + loop_step: { + CALL [38] { + function: _+_ + args: { + IDENT [39] { + name: @ac:0:0 + } + LIST [40] { + elements: { + CALL [41] { + function: _+_ + args: { + IDENT [42] { + name: @it:0:0 + } + CONSTANT [43] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [44] { + name: @ac:0:0 + } + } + } + } + } + } + } + } + result: { + IDENT [45] { + name: @ac:1:0 + } + } + } + } + } + } +} +Test case: ADJACENT_NESTED_MACROS_COMP_V2 +Source: [1,2,3].transformMap(i, x, [1, 2, 3].transformMap(i, x, i + x + 1)) == [1,2,3].transformMap(j, y, [1, 2, 3].transformMap(j, y, j + y + 1)) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + CONSTANT [5] { value: 2 } + CONSTANT [6] { value: 3 } + } + } + } + } + CALL [7] { + function: _==_ + args: { + COMPREHENSION [8] { + iter_var: @it:1:0 + iter_var2: @it2:1:0 + iter_range: { + IDENT [9] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + MAP [10] { + + } + } + loop_condition: { + CONSTANT [11] { value: true } + } + loop_step: { + CALL [12] { + function: cel.@mapInsert + args: { + IDENT [13] { + name: @ac:1:0 + } + IDENT [14] { + name: @it:1:0 + } + COMPREHENSION [15] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + IDENT [16] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + MAP [17] { + + } + } + loop_condition: { + CONSTANT [18] { value: true } + } + loop_step: { + CALL [19] { + function: cel.@mapInsert + args: { + IDENT [20] { + name: @ac:0:0 + } + IDENT [21] { + name: @it:0:0 + } + CALL [22] { + function: _+_ + args: { + CALL [23] { + function: _+_ + args: { + IDENT [24] { + name: @it:0:0 + } + IDENT [25] { + name: @it2:0:0 + } + } + } + CONSTANT [26] { value: 1 } + } + } + } + } + } + result: { + IDENT [27] { + name: @ac:0:0 + } + } + } + } + } + } + result: { + IDENT [28] { + name: @ac:1:0 + } + } + } + COMPREHENSION [29] { + iter_var: @it:1:0 + iter_var2: @it2:1:0 + iter_range: { + IDENT [30] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + MAP [31] { + + } + } + loop_condition: { + CONSTANT [32] { value: true } + } + loop_step: { + CALL [33] { + function: cel.@mapInsert + args: { + IDENT [34] { + name: @ac:1:0 + } + IDENT [35] { + name: @it:1:0 + } + COMPREHENSION [36] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + IDENT [37] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + MAP [38] { + + } + } + loop_condition: { + CONSTANT [39] { value: true } + } + loop_step: { + CALL [40] { + function: cel.@mapInsert + args: { + IDENT [41] { + name: @ac:0:0 + } + IDENT [42] { + name: @it:0:0 + } + CALL [43] { + function: _+_ + args: { + CALL [44] { + function: _+_ + args: { + IDENT [45] { + name: @it:0:0 + } + IDENT [46] { + name: @it2:0:0 + } + } + } + CONSTANT [47] { value: 1 } + } + } + } + } + } + result: { + IDENT [48] { + name: @ac:0:0 + } + } + } + } + } + } + result: { + IDENT [49] { + name: @ac:1:0 + } + } + } + } + } + } +} +Test case: INCLUSION_LIST +Source: 1 in [1,2,3] && 2 in [1,2,3] && 3 in [3, [1,2,3]] && 1 in [1,2,3] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + CALL [3] { + function: @in + args: { + CONSTANT [4] { value: 1 } + LIST [5] { + elements: { + CONSTANT [6] { value: 1 } + CONSTANT [7] { value: 2 } + CONSTANT [8] { value: 3 } + } + } + } + } + LIST [9] { + elements: { + CONSTANT [10] { value: 1 } + CONSTANT [11] { value: 2 } + CONSTANT [12] { value: 3 } + } + } + CALL [13] { + function: _&&_ + args: { + IDENT [14] { + name: @index0 + } + CALL [15] { + function: @in + args: { + CONSTANT [16] { value: 2 } + IDENT [17] { + name: @index1 + } + } } } - result: { - IDENT [36] { - name: @x0:0 + } + CALL [18] { + function: @in + args: { + CONSTANT [19] { value: 3 } + LIST [20] { + elements: { + CONSTANT [21] { value: 3 } + IDENT [22] { + name: @index1 + } + } } } } } } - CALL [37] { - function: _==_ + CALL [23] { + function: _&&_ args: { - IDENT [38] { - name: @index6 - } - IDENT [39] { + IDENT [24] { name: @index2 } + CALL [25] { + function: _&&_ + args: { + IDENT [26] { + name: @index3 + } + IDENT [27] { + name: @index0 + } + } + } } } } } -Test case: NESTED_MACROS_2 -Source: [1, 2].map(y, [1, 2, 3].filter(x, x == y)) == [[1], [2]] +Test case: INCLUSION_MAP +Source: 2 in {'a': 1, 2: {true: false}, 3: {true: false}} +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + MAP [3] { + MAP_ENTRY [4] { + key: { + CONSTANT [5] { value: true } + } + value: { + CONSTANT [6] { value: false } + } + } + } + } + } + CALL [7] { + function: @in + args: { + CONSTANT [8] { value: 2 } + MAP [9] { + MAP_ENTRY [10] { + key: { + CONSTANT [11] { value: "a" } + } + value: { + CONSTANT [12] { value: 1 } + } + } + MAP_ENTRY [13] { + key: { + CONSTANT [14] { value: 2 } + } + value: { + IDENT [15] { + name: @index0 + } + } + } + MAP_ENTRY [16] { + key: { + CONSTANT [17] { value: 3 } + } + value: { + IDENT [18] { + name: @index0 + } + } + } + } + } + } + } +} +Test case: MACRO_ITER_VAR_NOT_REFERENCED +Source: [1,2].map(i, [1, 2].map(i, [3,4])) == [[[3, 4], [3, 4]], [[3, 4], [3, 4]]] =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { + LIST [3] { elements: { - CREATE_LIST [4] { + LIST [4] { elements: { - CONSTANT [5] { value: 1 } + CONSTANT [5] { value: 3 } + CONSTANT [6] { value: 4 } } } - CREATE_LIST [6] { + LIST [7] { elements: { - CONSTANT [7] { value: 2 } + CONSTANT [8] { value: 3 } + CONSTANT [9] { value: 4 } } } } } - CALL [8] { - function: _+_ - args: { - IDENT [9] { - name: @x1:0 - } - CREATE_LIST [10] { + LIST [10] { + elements: { + CONSTANT [11] { value: 1 } + CONSTANT [12] { value: 2 } + } + } + LIST [13] { + elements: { + LIST [14] { elements: { - IDENT [11] { - name: @c1:0 - } + CONSTANT [15] { value: 3 } + CONSTANT [16] { value: 4 } } } } } - CALL [12] { - function: _?_:_ - args: { - CALL [13] { - function: _==_ + COMPREHENSION [17] { + iter_var: @it:0:0 + iter_range: { + IDENT [18] { + name: @index1 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [19] { + elements: { + } + } + } + loop_condition: { + CONSTANT [20] { value: true } + } + loop_step: { + CALL [21] { + function: _+_ args: { - IDENT [14] { - name: @c1:0 + IDENT [22] { + name: @ac:0:0 } - IDENT [15] { - name: @c0:0 + IDENT [23] { + name: @index2 } } } - IDENT [16] { - name: @index1 + } + result: { + IDENT [24] { + name: @ac:0:0 } - IDENT [17] { - name: @x1:0 + } + } + LIST [25] { + elements: { + IDENT [26] { + name: @index3 } } } - COMPREHENSION [18] { - iter_var: @c1:0 + } + } + CALL [27] { + function: _==_ + args: { + COMPREHENSION [28] { + iter_var: @it:1:0 iter_range: { - CREATE_LIST [19] { - elements: { - CONSTANT [20] { value: 1 } - CONSTANT [21] { value: 2 } - CONSTANT [22] { value: 3 } - } + IDENT [29] { + name: @index1 } } - accu_var: @x1:0 + accu_var: @ac:1:0 accu_init: { - CREATE_LIST [23] { + LIST [30] { elements: { } } } loop_condition: { - CONSTANT [24] { value: true } + CONSTANT [31] { value: true } } loop_step: { - IDENT [25] { - name: @index2 + CALL [32] { + function: _+_ + args: { + IDENT [33] { + name: @ac:1:0 + } + IDENT [34] { + name: @index4 + } + } } } result: { - IDENT [26] { - name: @x1:0 + IDENT [35] { + name: @ac:1:0 } } } - CALL [27] { - function: _+_ - args: { - IDENT [28] { - name: @x0:0 + LIST [36] { + elements: { + IDENT [37] { + name: @index0 + } + IDENT [38] { + name: @index0 + } + } + } + } + } + } +} +Test case: MACRO_ITER_VAR2_NOT_REFERENCED +Source: [1,2].transformList(i, v, [1, 2].transformList(i, v, [3,4])) == [[[3, 4], [3, 4]], [[3, 4], [3, 4]]] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + LIST [4] { + elements: { + CONSTANT [5] { value: 3 } + CONSTANT [6] { value: 4 } + } } - CREATE_LIST [29] { + LIST [7] { elements: { - IDENT [30] { - name: @index3 - } + CONSTANT [8] { value: 3 } + CONSTANT [9] { value: 4 } + } + } + } + } + LIST [10] { + elements: { + CONSTANT [11] { value: 1 } + CONSTANT [12] { value: 2 } + } + } + LIST [13] { + elements: { + LIST [14] { + elements: { + CONSTANT [15] { value: 3 } + CONSTANT [16] { value: 4 } } } } } - COMPREHENSION [31] { - iter_var: @c0:0 + COMPREHENSION [17] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 iter_range: { - CREATE_LIST [32] { + IDENT [18] { + name: @index1 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [19] { elements: { - CONSTANT [33] { value: 1 } - CONSTANT [34] { value: 2 } } } } - accu_var: @x0:0 + loop_condition: { + CONSTANT [20] { value: true } + } + loop_step: { + CALL [21] { + function: _+_ + args: { + IDENT [22] { + name: @ac:0:0 + } + IDENT [23] { + name: @index2 + } + } + } + } + result: { + IDENT [24] { + name: @ac:0:0 + } + } + } + LIST [25] { + elements: { + IDENT [26] { + name: @index3 + } + } + } + } + } + CALL [27] { + function: _==_ + args: { + COMPREHENSION [28] { + iter_var: @it:1:0 + iter_var2: @it2:1:0 + iter_range: { + IDENT [29] { + name: @index1 + } + } + accu_var: @ac:1:0 accu_init: { - CREATE_LIST [35] { + LIST [30] { elements: { } } } loop_condition: { - CONSTANT [36] { value: true } + CONSTANT [31] { value: true } } loop_step: { - IDENT [37] { - name: @index4 + CALL [32] { + function: _+_ + args: { + IDENT [33] { + name: @ac:1:0 + } + IDENT [34] { + name: @index4 + } + } } } result: { + IDENT [35] { + name: @ac:1:0 + } + } + } + LIST [36] { + elements: { + IDENT [37] { + name: @index0 + } IDENT [38] { - name: @x0:0 + name: @index0 } } } } } - CALL [39] { - function: _==_ - args: { - IDENT [40] { - name: @index5 - } - IDENT [41] { - name: @index0 - } - } - } } } -Test case: INCLUSION_LIST -Source: 1 in [1,2,3] && 2 in [1,2,3] && 3 in [3, [1,2,3]] && 1 in [1,2,3] +Test case: MACRO_SHADOWED_VARIABLE +Source: [x - 1 > 3 ? x - 1 : 5].exists(x, x - 1 > 3) || x - 1 > 3 =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - CONSTANT [5] { value: 2 } - CONSTANT [6] { value: 3 } - } - } - CALL [7] { - function: @in - args: { - CONSTANT [8] { value: 1 } - IDENT [9] { - name: @index0 - } - } - } - CALL [10] { - function: @in + CALL [3] { + function: _>_ args: { - CONSTANT [11] { value: 3 } - CREATE_LIST [12] { - elements: { - CONSTANT [13] { value: 3 } - IDENT [14] { - name: @index0 + CALL [4] { + function: _-_ + args: { + IDENT [5] { + name: x } + CONSTANT [6] { value: 1 } } } + CONSTANT [7] { value: 3 } } } - CALL [15] { - function: _&&_ + CALL [8] { + function: _?_:_ args: { - IDENT [16] { - name: @index2 + IDENT [9] { + name: @index0 } - IDENT [17] { - name: @index1 + CALL [10] { + function: _-_ + args: { + IDENT [11] { + name: x + } + CONSTANT [12] { value: 1 } + } } + CONSTANT [13] { value: 5 } } } - CALL [18] { - function: _&&_ - args: { - IDENT [19] { + LIST [14] { + elements: { + IDENT [15] { name: @index1 } - CALL [20] { - function: @in - args: { - CONSTANT [21] { value: 2 } - IDENT [22] { - name: @index0 - } - } - } } } } } - CALL [23] { - function: _&&_ + CALL [16] { + function: _||_ args: { - IDENT [24] { - name: @index4 - } - IDENT [25] { - name: @index3 - } - } - } - } -} -Test case: INCLUSION_MAP -Source: 2 in {'a': 1, 2: {true: false}, 3: {true: false}} -=====> -CALL [1] { - function: cel.@block - args: { - CREATE_LIST [2] { - elements: { - CREATE_MAP [3] { - MAP_ENTRY [4] { - key: { - CONSTANT [5] { value: true } - } - value: { - CONSTANT [6] { value: false } + COMPREHENSION [17] { + iter_var: @it:0:0 + iter_range: { + IDENT [18] { + name: @index2 } } - } - CREATE_MAP [7] { - MAP_ENTRY [8] { - key: { - CONSTANT [9] { value: "a" } - } - value: { - CONSTANT [10] { value: 1 } - } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [19] { value: false } } - MAP_ENTRY [11] { - key: { - CONSTANT [12] { value: 2 } - } - value: { - IDENT [13] { - name: @index0 + loop_condition: { + CALL [20] { + function: @not_strictly_false + args: { + CALL [21] { + function: !_ + args: { + IDENT [22] { + name: @ac:0:0 + } + } + } } } } - MAP_ENTRY [14] { - key: { - CONSTANT [15] { value: 3 } - } - value: { - IDENT [16] { - name: @index0 + loop_step: { + CALL [23] { + function: _||_ + args: { + IDENT [24] { + name: @ac:0:0 + } + CALL [25] { + function: _>_ + args: { + CALL [26] { + function: _-_ + args: { + IDENT [27] { + name: @it:0:0 + } + CONSTANT [28] { value: 1 } + } + } + CONSTANT [29] { value: 3 } + } + } } } } + result: { + IDENT [30] { + name: @ac:0:0 + } + } } - } - } - CALL [17] { - function: @in - args: { - CONSTANT [18] { value: 2 } - IDENT [19] { - name: @index1 + IDENT [31] { + name: @index0 } } } } } -Test case: MACRO_ITER_VAR_NOT_REFERENCED -Source: [1,2].map(i, [1, 2].map(i, [3,4])) == [[[3, 4], [3, 4]], [[3, 4], [3, 4]]] +Test case: MACRO_SHADOWED_VARIABLE_2 +Source: ["foo", "bar"].map(x, [x + x, x + x]).map(x, [x + x, x + x]) =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - CONSTANT [5] { value: 2 } - } - } - CREATE_LIST [6] { - elements: { - CONSTANT [7] { value: 3 } - CONSTANT [8] { value: 4 } - } - } - CREATE_LIST [9] { - elements: { - IDENT [10] { - name: @index1 - } - IDENT [11] { - name: @index1 - } - } - } - CREATE_LIST [12] { + LIST [3] { elements: { - IDENT [13] { - name: @index2 - } - IDENT [14] { - name: @index2 - } - } - } - CALL [15] { - function: _+_ - args: { - IDENT [16] { - name: @x1:0 - } - CREATE_LIST [17] { - elements: { - IDENT [18] { - name: @index1 - } - } - } + CONSTANT [4] { value: "foo" } + CONSTANT [5] { value: "bar" } } } - COMPREHENSION [19] { - iter_var: @c1:0 + } + } + COMPREHENSION [6] { + iter_var: @it:1:0 + iter_range: { + COMPREHENSION [7] { + iter_var: @it:0:0 iter_range: { - IDENT [20] { + IDENT [8] { name: @index0 } } - accu_var: @x1:0 + accu_var: @ac:0:0 accu_init: { - CREATE_LIST [21] { + LIST [9] { elements: { } } } loop_condition: { - CONSTANT [22] { value: true } + CONSTANT [10] { value: true } } loop_step: { - IDENT [23] { - name: @index4 + CALL [11] { + function: _+_ + args: { + IDENT [12] { + name: @ac:0:0 + } + LIST [13] { + elements: { + LIST [14] { + elements: { + CALL [15] { + function: _+_ + args: { + IDENT [16] { + name: @it:0:0 + } + IDENT [17] { + name: @it:0:0 + } + } + } + CALL [18] { + function: _+_ + args: { + IDENT [19] { + name: @it:0:0 + } + IDENT [20] { + name: @it:0:0 + } + } + } + } + } + } + } + } } } result: { - IDENT [24] { - name: @x1:0 + IDENT [21] { + name: @ac:0:0 } } } - CALL [25] { - function: _+_ - args: { - IDENT [26] { - name: @x0:0 - } - CREATE_LIST [27] { - elements: { - IDENT [28] { - name: @index5 - } - } - } + } + accu_var: @ac:1:0 + accu_init: { + LIST [22] { + elements: { } } - COMPREHENSION [29] { - iter_var: @c0:0 - iter_range: { - IDENT [30] { - name: @index0 + } + loop_condition: { + CONSTANT [23] { value: true } + } + loop_step: { + CALL [24] { + function: _+_ + args: { + IDENT [25] { + name: @ac:1:0 } - } - accu_var: @x0:0 - accu_init: { - CREATE_LIST [31] { + LIST [26] { elements: { + LIST [27] { + elements: { + CALL [28] { + function: _+_ + args: { + IDENT [29] { + name: @it:1:0 + } + IDENT [30] { + name: @it:1:0 + } + } + } + CALL [31] { + function: _+_ + args: { + IDENT [32] { + name: @it:1:0 + } + IDENT [33] { + name: @it:1:0 + } + } + } + } + } } } } - loop_condition: { - CONSTANT [32] { value: true } - } - loop_step: { - IDENT [33] { - name: @index6 - } - } - result: { - IDENT [34] { - name: @x0:0 - } - } } } - } - CALL [35] { - function: _==_ - args: { - IDENT [36] { - name: @index7 - } - IDENT [37] { - name: @index3 + result: { + IDENT [34] { + name: @ac:1:0 } } } } } -Test case: MACRO_SHADOWED_VARIABLE -Source: [x - 1 > 3 ? x - 1 : 5].exists(x, x - 1 > 3) || x - 1 > 3 +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 =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { function: _-_ args: { - IDENT [4] { - name: x - } - CONSTANT [5] { value: 1 } - } - } - CALL [6] { - function: _>_ - args: { - IDENT [7] { - name: @index0 - } - CONSTANT [8] { value: 3 } - } - } - CALL [9] { - function: _>_ - args: { - CALL [10] { + CALL [4] { function: _-_ args: { - IDENT [11] { - name: @c0:0 + IDENT [5] { + name: x + } + IDENT [6] { + name: y } - CONSTANT [12] { value: 1 } } } - CONSTANT [13] { value: 3 } + CONSTANT [7] { value: 1 } } } - CALL [14] { - function: _||_ + CALL [8] { + function: _>_ args: { - IDENT [15] { - name: @x0:0 - } - IDENT [16] { - name: @index2 + IDENT [9] { + name: @index0 } + CONSTANT [10] { value: 3 } } } - CREATE_LIST [17] { + LIST [11] { elements: { - CALL [18] { + CALL [12] { function: _?_:_ args: { - IDENT [19] { + IDENT [13] { name: @index1 } - IDENT [20] { + IDENT [14] { name: @index0 } - CONSTANT [21] { value: 5 } + CONSTANT [15] { value: 5 } } } } } } } - CALL [22] { + CALL [16] { function: _||_ args: { - COMPREHENSION [23] { - iter_var: @c0:0 + COMPREHENSION [17] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 iter_range: { - IDENT [24] { - name: @index4 + IDENT [18] { + name: @index2 } } - accu_var: @x0:0 + accu_var: @ac:0:0 accu_init: { - CONSTANT [25] { value: false } + CONSTANT [19] { value: false } } loop_condition: { - CALL [26] { + CALL [20] { function: @not_strictly_false args: { - CALL [27] { + CALL [21] { function: !_ args: { - IDENT [28] { - name: @x0:0 + IDENT [22] { + name: @ac:0:0 } } } @@ -2531,158 +4568,185 @@ CALL [1] { } } loop_step: { - IDENT [29] { - name: @index3 + CALL [23] { + function: _||_ + args: { + IDENT [24] { + name: @ac:0:0 + } + CALL [25] { + function: _>_ + args: { + CALL [26] { + function: _-_ + args: { + CALL [27] { + function: _-_ + args: { + IDENT [28] { + name: @it:0:0 + } + IDENT [29] { + name: @it2:0:0 + } + } + } + CONSTANT [30] { value: 1 } + } + } + CONSTANT [31] { value: 3 } + } + } + } } } result: { - IDENT [30] { - name: @x0:0 + IDENT [32] { + name: @ac:0:0 } } } - IDENT [31] { + IDENT [33] { name: @index1 } } } } } -Test case: MACRO_SHADOWED_VARIABLE_2 -Source: ["foo", "bar"].map(x, [x + x, x + x]).map(x, [x + x, x + x]) +Test case: MACRO_SHADOWED_VARIABLE_COMP_V2_2 +Source: ["foo", "bar"].transformMap(x, y, [x + x, y + y]).transformMap(x, y, [x + x, y + y]) =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CALL [3] { - function: _+_ - args: { - IDENT [4] { - name: @c1:0 - } - IDENT [5] { - name: @c1:0 - } - } - } - CALL [6] { - function: _+_ - args: { - IDENT [7] { - name: @c0:0 - } - IDENT [8] { - name: @c0:0 - } - } - } - CREATE_LIST [9] { - elements: { - CREATE_LIST [10] { - elements: { - IDENT [11] { - name: @index1 - } - IDENT [12] { - name: @index1 - } - } - } - } - } - CALL [13] { - function: _+_ - args: { - IDENT [14] { - name: @x0:0 - } - IDENT [15] { - name: @index2 - } - } - } - CREATE_LIST [16] { + LIST [3] { elements: { - CREATE_LIST [17] { - elements: { - IDENT [18] { - name: @index0 - } - IDENT [19] { - name: @index0 - } - } - } + CONSTANT [4] { value: "foo" } + CONSTANT [5] { value: "bar" } } } - COMPREHENSION [20] { - iter_var: @c1:0 + } + } + COMPREHENSION [6] { + iter_var: @it:1:0 + iter_var2: @it2:1:0 + iter_range: { + COMPREHENSION [7] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 iter_range: { - CREATE_LIST [21] { - elements: { - CONSTANT [22] { value: "foo" } - CONSTANT [23] { value: "bar" } - } + IDENT [8] { + name: @index0 } } - accu_var: @x1:0 + accu_var: @ac:0:0 accu_init: { - CREATE_LIST [24] { - elements: { - } + MAP [9] { + } } loop_condition: { - CONSTANT [25] { value: true } + CONSTANT [10] { value: true } } loop_step: { - CALL [26] { - function: _+_ + CALL [11] { + function: cel.@mapInsert args: { - IDENT [27] { - name: @x1:0 + IDENT [12] { + name: @ac:0:0 } - IDENT [28] { - name: @index4 + IDENT [13] { + name: @it:0:0 + } + LIST [14] { + elements: { + CALL [15] { + function: _+_ + args: { + IDENT [16] { + name: @it:0:0 + } + IDENT [17] { + name: @it:0:0 + } + } + } + CALL [18] { + function: _+_ + args: { + IDENT [19] { + name: @it2:0:0 + } + IDENT [20] { + name: @it2:0:0 + } + } + } + } } } } } result: { - IDENT [29] { - name: @x1:0 + IDENT [21] { + name: @ac:0:0 } } } } - } - COMPREHENSION [30] { - iter_var: @c0:0 - iter_range: { - IDENT [31] { - name: @index5 - } - } - accu_var: @x0:0 + accu_var: @ac:1:0 accu_init: { - CREATE_LIST [32] { - elements: { - } + MAP [22] { + } } loop_condition: { - CONSTANT [33] { value: true } + CONSTANT [23] { value: true } } loop_step: { - IDENT [34] { - name: @index3 + CALL [24] { + function: cel.@mapInsert + args: { + IDENT [25] { + name: @ac:1:0 + } + IDENT [26] { + name: @it:1:0 + } + LIST [27] { + elements: { + CALL [28] { + function: _+_ + args: { + IDENT [29] { + name: @it:1:0 + } + IDENT [30] { + name: @it:1:0 + } + } + } + CALL [31] { + function: _+_ + args: { + IDENT [32] { + name: @it2:1:0 + } + IDENT [33] { + name: @it2:1:0 + } + } + } + } + } + } } } result: { - IDENT [35] { - name: @x0:0 + IDENT [34] { + name: @ac:1:0 } } } @@ -2694,9 +4758,9 @@ Source: has({'a': true}.a) && {'a':true}['a'] CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_MAP [3] { + MAP [3] { MAP_ENTRY [4] { key: { CONSTANT [5] { value: "a" } @@ -2706,27 +4770,59 @@ CALL [1] { } } } - CALL [7] { + } + } + CALL [7] { + function: _&&_ + args: { + SELECT [8] { + IDENT [9] { + name: @index0 + }.a~presence_test + } + CALL [10] { function: _[_] args: { - IDENT [8] { + IDENT [11] { name: @index0 } - CONSTANT [9] { value: "a" } + CONSTANT [12] { value: "a" } } } } } - CALL [10] { + } +} +Test case: PRESENCE_TEST_2 +Source: has({'a': true}.a) && has({'a': true}.a) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + SELECT [3] { + MAP [4] { + MAP_ENTRY [5] { + key: { + CONSTANT [6] { value: "a" } + } + value: { + CONSTANT [7] { value: true } + } + } + }.a~presence_test + } + } + } + CALL [8] { function: _&&_ args: { - SELECT [11] { - IDENT [12] { - name: @index0 - }.a~presence_test + IDENT [9] { + name: @index0 } - IDENT [13] { - name: @index1 + IDENT [10] { + name: @index0 } } } @@ -2738,7 +4834,7 @@ Source: (has(msg.oneof_type.payload) ? msg.oneof_type.payload.single_int64 : 0) CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { @@ -2785,54 +4881,56 @@ Source: (has(msg.oneof_type.payload) ? msg.oneof_type.payload.single_int64 : msg CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 + SELECT [4] { + IDENT [5] { + name: msg + }.oneof_type }.payload } - SELECT [7] { - IDENT [8] { - name: @index1 + SELECT [6] { + IDENT [7] { + name: @index0 }.single_int64 } - CALL [9] { + SELECT [8] { + SELECT [9] { + IDENT [10] { + name: msg + }.oneof_type + }.payload~presence_test + } + CALL [11] { function: _?_:_ args: { - SELECT [10] { - IDENT [11] { - name: @index0 - }.payload~presence_test - } IDENT [12] { name: @index2 } - CALL [13] { + IDENT [13] { + name: @index1 + } + CALL [14] { function: _*_ args: { - IDENT [14] { - name: @index2 + IDENT [15] { + name: @index1 } - CONSTANT [15] { value: 0 } + CONSTANT [16] { value: 0 } } } } } } } - CALL [16] { + CALL [17] { function: _==_ args: { - IDENT [17] { + IDENT [18] { name: @index3 } - CONSTANT [18] { value: 10 } + CONSTANT [19] { value: 10 } } } } @@ -2843,54 +4941,51 @@ Source: (has(msg.oneof_type.payload.single_int64) ? msg.oneof_type.payload.singl CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 + SELECT [4] { + IDENT [5] { + name: msg + }.oneof_type }.payload } - SELECT [7] { - IDENT [8] { - name: @index1 + SELECT [6] { + IDENT [7] { + name: @index0 }.single_int64 } - CALL [9] { + CALL [8] { function: _?_:_ args: { - SELECT [10] { - IDENT [11] { - name: @index1 + SELECT [9] { + IDENT [10] { + name: @index0 }.single_int64~presence_test } - IDENT [12] { - name: @index2 + IDENT [11] { + name: @index1 } - CALL [13] { + CALL [12] { function: _*_ args: { - IDENT [14] { - name: @index2 + IDENT [13] { + name: @index1 } - CONSTANT [15] { value: 0 } + CONSTANT [14] { value: 0 } } } } } } } - CALL [16] { + CALL [15] { function: _==_ args: { - IDENT [17] { - name: @index3 + IDENT [16] { + name: @index2 } - CONSTANT [18] { value: 10 } + CONSTANT [17] { value: 10 } } } } @@ -2901,99 +4996,98 @@ Source: (has(msg.oneof_type) && has(msg.oneof_type.payload) && has(msg.oneof_typ CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 + SELECT [4] { + IDENT [5] { + name: msg + }.oneof_type }.payload } - SELECT [7] { - IDENT [8] { - name: @index1 + SELECT [6] { + IDENT [7] { + name: @index0 }.map_string_string } - CALL [9] { - function: _==_ - args: { - SELECT [10] { - IDENT [11] { - name: @index2 - }.key - } - CONSTANT [12] { value: "A" } - } + SELECT [8] { + SELECT [9] { + IDENT [10] { + name: msg + }.oneof_type + }.payload~presence_test } - CALL [13] { + CALL [11] { function: _&&_ args: { - SELECT [14] { - IDENT [15] { - name: @index1 - }.map_string_string~presence_test + SELECT [12] { + IDENT [13] { + name: msg + }.oneof_type~presence_test } - SELECT [16] { - IDENT [17] { - name: @index2 - }.key~presence_test + IDENT [14] { + name: @index2 } } } - CALL [18] { - function: _?_:_ + CALL [15] { + function: _&&_ args: { - IDENT [19] { - name: @index4 - } - IDENT [20] { + IDENT [16] { name: @index3 } - CONSTANT [21] { value: false } + SELECT [17] { + IDENT [18] { + name: @index0 + }.single_int64~presence_test + } } } - CALL [22] { + CALL [19] { function: _&&_ args: { - SELECT [23] { - IDENT [24] { - name: msg - }.oneof_type~presence_test - } - SELECT [25] { - IDENT [26] { + SELECT [20] { + IDENT [21] { name: @index0 - }.payload~presence_test + }.map_string_string~presence_test + } + SELECT [22] { + IDENT [23] { + name: @index1 + }.key~presence_test } } } - CALL [27] { - function: _&&_ + CALL [24] { + function: _==_ args: { - IDENT [28] { - name: @index6 - } - SELECT [29] { - IDENT [30] { + SELECT [25] { + IDENT [26] { name: @index1 - }.single_int64~presence_test + }.key } + CONSTANT [27] { value: "A" } } } } } - CALL [31] { + CALL [28] { function: _?_:_ args: { - IDENT [32] { - name: @index7 + IDENT [29] { + name: @index4 } - IDENT [33] { - name: @index5 + CALL [30] { + function: _?_:_ + args: { + IDENT [31] { + name: @index5 + } + IDENT [32] { + name: @index6 + } + CONSTANT [33] { value: false } + } } CONSTANT [34] { value: false } } @@ -3006,65 +5100,61 @@ Source: [10, ?optional.none(), [?optional.none(), ?opt_x], [?optional.none(), ?o CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CALL [3] { - function: optional.none - args: { - } - } - CREATE_LIST [4] { + LIST [3] { elements: { - IDENT [5] { - name: @index0 + CALL [4] { + function: optional.none + args: { + } } - IDENT [6] { + IDENT [5] { name: opt_x } } optional_indices: [0, 1] } - CREATE_LIST [7] { + LIST [6] { elements: { - CONSTANT [8] { value: 5 } + CONSTANT [7] { value: 5 } } } - CREATE_LIST [9] { + LIST [8] { elements: { - CONSTANT [10] { value: 10 } + CONSTANT [9] { value: 10 } + CALL [10] { + function: optional.none + args: { + } + } IDENT [11] { - name: @index2 + name: @index0 } IDENT [12] { - name: @index2 - } - } - } - CREATE_LIST [13] { - elements: { - CONSTANT [14] { value: 10 } - IDENT [15] { name: @index0 } - IDENT [16] { - name: @index1 - } - IDENT [17] { - name: @index1 - } } optional_indices: [0] } } } - CALL [18] { + CALL [13] { function: _==_ args: { - IDENT [19] { - name: @index4 + IDENT [14] { + name: @index2 } - IDENT [20] { - name: @index3 + LIST [15] { + elements: { + CONSTANT [16] { value: 10 } + IDENT [17] { + name: @index1 + } + IDENT [18] { + name: @index1 + } + } } } } @@ -3076,56 +5166,50 @@ Source: {?'hello': optional.of('hello')}['hello'] + {?'hello': optional.of('hell CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CALL [3] { - function: optional.of - args: { - CONSTANT [4] { value: "hello" } - } - } - CREATE_MAP [5] { - MAP_ENTRY [6] { + MAP [3] { + MAP_ENTRY [4] { key: { - CONSTANT [7] { value: "hello" } + CONSTANT [5] { value: "hello" } } optional_entry: true value: { - IDENT [8] { - name: @index0 + CALL [6] { + function: optional.of + args: { + CONSTANT [7] { value: "hello" } + } } } } } - CALL [9] { + CALL [8] { function: _[_] args: { - IDENT [10] { - name: @index1 + IDENT [9] { + name: @index0 } - CONSTANT [11] { value: "hello" } + CONSTANT [10] { value: "hello" } } } + } + } + CALL [11] { + function: _==_ + args: { CALL [12] { function: _+_ args: { IDENT [13] { - name: @index2 + name: @index1 } IDENT [14] { - name: @index2 + name: @index1 } } } - } - } - CALL [15] { - function: _==_ - args: { - IDENT [16] { - name: @index3 - } - CONSTANT [17] { value: "hellohello" } + CONSTANT [15] { value: "hellohello" } } } } @@ -3136,9 +5220,9 @@ Source: {?'key': optional.of('test')}[?'bogus'].or({'key': 'test'}[?'bogus']).or CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_MAP [3] { + MAP [3] { MAP_ENTRY [4] { key: { CONSTANT [5] { value: "key" } @@ -3148,81 +5232,75 @@ CALL [1] { } } } - CALL [7] { - function: _[_] - args: { - IDENT [8] { - name: @index0 - } - CONSTANT [9] { value: "key" } - } - } - CALL [10] { - function: _[?_] - args: { - IDENT [11] { - name: @index0 - } - CONSTANT [12] { value: "bogus" } - } - } - CREATE_MAP [13] { - MAP_ENTRY [14] { + MAP [7] { + MAP_ENTRY [8] { key: { - CONSTANT [15] { value: "key" } + CONSTANT [9] { value: "key" } } optional_entry: true value: { - CALL [16] { + CALL [10] { function: optional.of args: { - CONSTANT [17] { value: "test" } + CONSTANT [11] { value: "test" } } } } } } - CALL [18] { + CALL [12] { function: or target: { - CALL [19] { + CALL [13] { function: _[?_] args: { - IDENT [20] { - name: @index3 + IDENT [14] { + name: @index1 + } + CONSTANT [15] { value: "bogus" } + } + } + } + args: { + CALL [16] { + function: _[?_] + args: { + IDENT [17] { + name: @index0 } - CONSTANT [21] { value: "bogus" } + CONSTANT [18] { value: "bogus" } } } } - args: { - IDENT [22] { - name: @index2 - } - } } - CALL [23] { + CALL [19] { function: orValue target: { - IDENT [24] { - name: @index4 + IDENT [20] { + name: @index2 } } args: { - IDENT [25] { - name: @index1 + CALL [21] { + function: _[_] + args: { + IDENT [22] { + name: @index0 + } + CONSTANT [23] { value: "key" } + } } } } } } - CALL [26] { + CALL [24] { function: _==_ args: { - IDENT [27] { - name: @index5 + IDENT [25] { + name: @index3 } - CONSTANT [28] { value: "test" } + CONSTANT [26] { value: "test" } } } } @@ -3233,67 +5311,61 @@ Source: TestAllTypes{?single_int64: optional.ofNonZeroValue(1), ?single_int32: o CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CALL [3] { - function: optional.ofNonZeroValue - args: { - CONSTANT [4] { value: 1 } - } - } - CALL [5] { - function: optional.of - args: { - CONSTANT [6] { value: 4 } - } - } - CREATE_STRUCT [7] { - name: TestAllTypes + STRUCT [3] { + name: cel.expr.conformance.proto3.TestAllTypes entries: { - ENTRY [8] { + ENTRY [4] { field_key: single_int64 optional_entry: true value: { - IDENT [9] { - name: @index0 + CALL [5] { + function: optional.ofNonZeroValue + args: { + CONSTANT [6] { value: 1 } + } } } } - ENTRY [10] { + ENTRY [7] { field_key: single_int32 optional_entry: true value: { - IDENT [11] { - name: @index1 + CALL [8] { + function: optional.of + args: { + CONSTANT [9] { value: 4 } + } } } } } } - CALL [12] { + CALL [10] { function: _+_ args: { - SELECT [13] { - IDENT [14] { - name: @index2 + SELECT [11] { + IDENT [12] { + name: @index0 }.single_int32 } - SELECT [15] { - IDENT [16] { - name: @index2 + SELECT [13] { + IDENT [14] { + name: @index0 }.single_int64 } } } } } - CALL [17] { + CALL [15] { function: _==_ args: { - IDENT [18] { - name: @index3 + IDENT [16] { + name: @index1 } - CONSTANT [19] { value: 5 } + CONSTANT [17] { value: 5 } } } } @@ -3304,63 +5376,54 @@ Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('h' + 'e' + 'l' + 'l' + CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { function: _+_ args: { - CONSTANT [4] { value: "h" } - CONSTANT [5] { value: "e" } - } - } - CALL [6] { - function: _+_ - args: { - IDENT [7] { - name: @index0 - } - CONSTANT [8] { value: "l" } - } - } - CALL [9] { - function: _+_ - args: { - IDENT [10] { - name: @index1 - } - CONSTANT [11] { value: "l" } - } - } - CALL [12] { - function: _+_ - args: { - IDENT [13] { - name: @index2 + CALL [4] { + function: _+_ + args: { + CONSTANT [5] { value: "h" } + CONSTANT [6] { value: "e" } + } } - CONSTANT [14] { value: "o" } + CONSTANT [7] { value: "l" } } } - CALL [15] { + CALL [8] { function: _+_ args: { - IDENT [16] { - name: @index3 + CALL [9] { + function: _+_ + args: { + IDENT [10] { + name: @index0 + } + CONSTANT [11] { value: "l" } + } } - CONSTANT [17] { value: " world" } + CONSTANT [12] { value: "o" } } } } } - CALL [18] { + CALL [13] { function: matches target: { - IDENT [19] { - name: @index4 + CALL [14] { + function: _+_ + args: { + IDENT [15] { + name: @index1 + } + CONSTANT [16] { value: " world" } + } } } args: { - IDENT [20] { - name: @index3 + IDENT [17] { + name: @index1 } } } @@ -3372,7 +5435,7 @@ Source: 'hello world'.matches('h' + 'e' + 'l' + 'l' + 'o') CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { function: _+_ @@ -3423,7 +5486,7 @@ Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('hello') CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { function: _+_ @@ -3453,26 +5516,23 @@ CALL [1] { CONSTANT [12] { value: "o" } } } - CALL [13] { + } + } + CALL [13] { + function: matches + target: { + CALL [14] { function: _+_ args: { - IDENT [14] { + IDENT [15] { name: @index1 } - CONSTANT [15] { value: " world" } + CONSTANT [16] { value: " world" } } } } - } - CALL [16] { - function: matches - target: { - IDENT [17] { - name: @index2 - } - } args: { - CONSTANT [18] { value: "hello" } + CONSTANT [17] { value: "hello" } } } } @@ -3483,7 +5543,7 @@ Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('w' + 'o' + 'r' + 'l' + CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { function: _+_ @@ -3491,11 +5551,11 @@ CALL [1] { CALL [4] { function: _+_ args: { - CONSTANT [5] { value: "w" } - CONSTANT [6] { value: "o" } + CONSTANT [5] { value: "h" } + CONSTANT [6] { value: "e" } } } - CONSTANT [7] { value: "r" } + CONSTANT [7] { value: "l" } } } CALL [8] { @@ -3510,7 +5570,7 @@ CALL [1] { CONSTANT [11] { value: "l" } } } - CONSTANT [12] { value: "d" } + CONSTANT [12] { value: "o" } } } CALL [13] { @@ -3519,11 +5579,11 @@ CALL [1] { CALL [14] { function: _+_ args: { - CONSTANT [15] { value: "h" } - CONSTANT [16] { value: "e" } + CONSTANT [15] { value: "w" } + CONSTANT [16] { value: "o" } } } - CONSTANT [17] { value: "l" } + CONSTANT [17] { value: "r" } } } CALL [18] { @@ -3538,30 +5598,27 @@ CALL [1] { CONSTANT [21] { value: "l" } } } - CONSTANT [22] { value: "o" } - } - } - CALL [23] { - function: _+_ - args: { - IDENT [24] { - name: @index3 - } - CONSTANT [25] { value: " world" } + CONSTANT [22] { value: "d" } } } } } - CALL [26] { + CALL [23] { function: matches target: { - IDENT [27] { - name: @index4 + CALL [24] { + function: _+_ + args: { + IDENT [25] { + name: @index1 + } + CONSTANT [26] { value: " world" } + } } } args: { - IDENT [28] { - name: @index1 + IDENT [27] { + name: @index3 } } } @@ -3573,77 +5630,68 @@ Source: non_pure_custom_func(msg.oneof_type.payload.single_int64) + non_pure_cus CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 + SELECT [4] { + IDENT [5] { + name: msg + }.oneof_type }.payload } - SELECT [7] { - IDENT [8] { - name: @index1 - }.single_int64 - } - SELECT [9] { - IDENT [10] { - name: msg + SELECT [6] { + IDENT [7] { + name: @index0 }.single_int64 } - SELECT [11] { - IDENT [12] { - name: @index1 - }.single_int32 - } } } - CALL [13] { + CALL [8] { function: _+_ args: { - CALL [14] { + CALL [9] { function: _+_ args: { - CALL [15] { + CALL [10] { function: _+_ args: { - CALL [16] { + CALL [11] { function: non_pure_custom_func args: { - IDENT [17] { - name: @index2 + IDENT [12] { + name: @index1 } } } - CALL [18] { + CALL [13] { function: non_pure_custom_func args: { - IDENT [19] { - name: @index4 + SELECT [14] { + IDENT [15] { + name: @index0 + }.single_int32 } } } } } - CALL [20] { + CALL [16] { function: non_pure_custom_func args: { - IDENT [21] { - name: @index2 + IDENT [17] { + name: @index1 } } } } } - CALL [22] { + CALL [18] { function: non_pure_custom_func args: { - IDENT [23] { - name: @index3 + SELECT [19] { + IDENT [20] { + name: msg + }.single_int64 } } } @@ -3657,82 +5705,76 @@ Source: pure_custom_func(msg.oneof_type.payload.single_int64) + pure_custom_func CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 + SELECT [4] { + IDENT [5] { + name: msg + }.oneof_type }.payload } - SELECT [7] { - IDENT [8] { - name: @index1 - }.single_int64 - } - CALL [9] { - function: pure_custom_func - args: { - IDENT [10] { - name: @index2 - } - } - } - CALL [11] { + CALL [6] { function: pure_custom_func args: { - SELECT [12] { - IDENT [13] { - name: msg + SELECT [7] { + IDENT [8] { + name: @index0 }.single_int64 } } } - CALL [14] { + CALL [9] { function: pure_custom_func args: { - SELECT [15] { - IDENT [16] { - name: @index1 + SELECT [10] { + IDENT [11] { + name: @index0 }.single_int32 } } } - CALL [17] { + CALL [12] { function: _+_ args: { - CALL [18] { + CALL [13] { function: _+_ args: { - IDENT [19] { - name: @index3 + IDENT [14] { + name: @index1 } - IDENT [20] { - name: @index5 + IDENT [15] { + name: @index2 } } } - IDENT [21] { - name: @index3 + IDENT [16] { + name: @index1 + } + } + } + CALL [17] { + function: pure_custom_func + args: { + SELECT [18] { + IDENT [19] { + name: msg + }.single_int64 } } } } } - CALL [22] { + CALL [20] { function: _+_ args: { - IDENT [23] { - name: @index6 + IDENT [21] { + name: @index3 } - IDENT [24] { + IDENT [22] { name: @index4 } } } } -} +} \ No newline at end of file diff --git a/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_3.baseline b/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_3.baseline index a0ff868b1..89b1069ad 100644 --- a/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_3.baseline +++ b/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_3.baseline @@ -4,22 +4,24 @@ Source: size([1,2]) + size([1,2]) + 1 == 5 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - CONSTANT [5] { value: 2 } - } - } - CALL [6] { + CALL [3] { function: size args: { - IDENT [7] { - name: @index0 + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + CONSTANT [6] { value: 2 } + } } } } + } + } + CALL [7] { + function: _==_ + args: { CALL [8] { function: _+_ args: { @@ -27,25 +29,17 @@ CALL [1] { function: _+_ args: { IDENT [10] { - name: @index1 + name: @index0 } IDENT [11] { - name: @index1 + name: @index0 } } } CONSTANT [12] { value: 1 } } } - } - } - CALL [13] { - function: _==_ - args: { - IDENT [14] { - name: @index2 - } - CONSTANT [15] { value: 5 } + CONSTANT [13] { value: 5 } } } } @@ -56,54 +50,51 @@ Source: 2 + size([1,2]) + size([1,2]) + 1 == 7 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - CONSTANT [5] { value: 2 } - } - } - CALL [6] { + CALL [3] { function: size args: { - IDENT [7] { - name: @index0 + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + CONSTANT [6] { value: 2 } + } } } } - CALL [8] { + CALL [7] { function: _+_ args: { - CALL [9] { + CALL [8] { function: _+_ args: { - CALL [10] { + CALL [9] { function: _+_ args: { - CONSTANT [11] { value: 2 } - IDENT [12] { - name: @index1 + CONSTANT [10] { value: 2 } + IDENT [11] { + name: @index0 } } } - IDENT [13] { - name: @index1 + IDENT [12] { + name: @index0 } } } - CONSTANT [14] { value: 1 } + CONSTANT [13] { value: 1 } } } } } - CALL [15] { + CALL [14] { function: _==_ args: { - IDENT [16] { - name: @index2 + IDENT [15] { + name: @index1 } - CONSTANT [17] { value: 7 } + CONSTANT [16] { value: 7 } } } } @@ -114,71 +105,65 @@ Source: size([0]) + size([0]) + size([1,2]) + size([1,2]) == 6 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 0 } - } - } - CALL [5] { + CALL [3] { function: size args: { - IDENT [6] { - name: @index0 + LIST [4] { + elements: { + CONSTANT [5] { value: 0 } + } } } } - CREATE_LIST [7] { - elements: { - CONSTANT [8] { value: 1 } - CONSTANT [9] { value: 2 } - } - } - CALL [10] { + CALL [6] { function: size args: { - IDENT [11] { - name: @index2 + LIST [7] { + elements: { + CONSTANT [8] { value: 1 } + CONSTANT [9] { value: 2 } + } } } } - CALL [12] { + CALL [10] { function: _+_ args: { - CALL [13] { + CALL [11] { function: _+_ args: { - CALL [14] { + CALL [12] { function: _+_ args: { - IDENT [15] { - name: @index1 + IDENT [13] { + name: @index0 } - IDENT [16] { - name: @index1 + IDENT [14] { + name: @index0 } } } - IDENT [17] { - name: @index3 + IDENT [15] { + name: @index1 } } } - IDENT [18] { - name: @index3 + IDENT [16] { + name: @index1 } } } } } - CALL [19] { + CALL [17] { function: _==_ args: { - IDENT [20] { - name: @index4 + IDENT [18] { + name: @index2 } - CONSTANT [21] { value: 6 } + CONSTANT [19] { value: 6 } } } } @@ -189,111 +174,102 @@ Source: 5 + size([0]) + size([0]) + size([1,2]) + size([1,2]) + size([1,2,3]) + CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 0 } - } - } - CALL [5] { + CALL [3] { function: size args: { - IDENT [6] { - name: @index0 + LIST [4] { + elements: { + CONSTANT [5] { value: 0 } + } } } } - CREATE_LIST [7] { - elements: { - CONSTANT [8] { value: 1 } - CONSTANT [9] { value: 2 } - } - } - CALL [10] { + CALL [6] { function: size args: { - IDENT [11] { - name: @index2 + LIST [7] { + elements: { + CONSTANT [8] { value: 1 } + CONSTANT [9] { value: 2 } + } } } } - CREATE_LIST [12] { - elements: { - CONSTANT [13] { value: 1 } - CONSTANT [14] { value: 2 } - CONSTANT [15] { value: 3 } - } - } - CALL [16] { + CALL [10] { function: size args: { - IDENT [17] { - name: @index4 + LIST [11] { + elements: { + CONSTANT [12] { value: 1 } + CONSTANT [13] { value: 2 } + CONSTANT [14] { value: 3 } + } } } } - CALL [18] { + CALL [15] { function: _+_ args: { - CALL [19] { + CALL [16] { function: _+_ args: { - CALL [20] { + CALL [17] { function: _+_ args: { - CONSTANT [21] { value: 5 } - IDENT [22] { - name: @index1 + CONSTANT [18] { value: 5 } + IDENT [19] { + name: @index0 } } } - IDENT [23] { - name: @index1 + IDENT [20] { + name: @index0 } } } - IDENT [24] { - name: @index3 + IDENT [21] { + name: @index1 } } } - CALL [25] { + CALL [22] { function: _+_ args: { - CALL [26] { + CALL [23] { function: _+_ args: { - CALL [27] { + CALL [24] { function: _+_ args: { - IDENT [28] { - name: @index6 - } - IDENT [29] { + IDENT [25] { name: @index3 } + IDENT [26] { + name: @index1 + } } } - IDENT [30] { - name: @index5 + IDENT [27] { + name: @index2 } } } - IDENT [31] { - name: @index5 + IDENT [28] { + name: @index2 } } } } } - CALL [32] { + CALL [29] { function: _==_ args: { - IDENT [33] { - name: @index7 + IDENT [30] { + name: @index4 } - CONSTANT [34] { value: 17 } + CONSTANT [31] { value: 17 } } } } @@ -304,162 +280,118 @@ Source: timestamp(int(timestamp(1000000000))).getFullYear() + timestamp(int(time CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { function: timestamp args: { - CONSTANT [4] { value: 1000000000 } - } - } - CALL [5] { - function: int - args: { - IDENT [6] { - name: @index0 + CALL [4] { + function: int + args: { + CALL [5] { + function: timestamp + args: { + CONSTANT [6] { value: 1000000000 } + } + } + } } } } CALL [7] { function: timestamp args: { - IDENT [8] { - name: @index1 - } - } - } - CALL [9] { - function: getFullYear - target: { - IDENT [10] { - name: @index2 + CALL [8] { + function: int + args: { + CALL [9] { + function: timestamp + args: { + CONSTANT [10] { value: 50 } + } + } + } } } - args: { - } } CALL [11] { function: timestamp args: { - CONSTANT [12] { value: 50 } - } - } - CALL [13] { - function: int - args: { - IDENT [14] { - name: @index4 + CALL [12] { + function: int + args: { + CALL [13] { + function: timestamp + args: { + CONSTANT [14] { value: 200 } + } + } + } } } } CALL [15] { function: timestamp args: { - IDENT [16] { - name: @index5 + CALL [16] { + function: int + args: { + CALL [17] { + function: timestamp + args: { + CONSTANT [18] { value: 75 } + } + } + } } } } - CALL [17] { - function: timestamp - args: { - CONSTANT [18] { value: 200 } - } - } CALL [19] { - function: int - args: { + function: getFullYear + target: { IDENT [20] { - name: @index7 + name: @index0 } } - } - CALL [21] { - function: timestamp args: { - IDENT [22] { - name: @index8 - } } } - CALL [23] { + CALL [21] { function: getFullYear target: { - IDENT [24] { - name: @index9 + IDENT [22] { + name: @index2 } } args: { } } - CALL [25] { - function: timestamp - args: { - CONSTANT [26] { value: 75 } - } - } - CALL [27] { - function: int + CALL [23] { + function: _+_ args: { - IDENT [28] { - name: @index11 + CALL [24] { + function: _+_ + args: { + IDENT [25] { + name: @index4 + } + CALL [26] { + function: getFullYear + target: { + IDENT [27] { + name: @index3 + } + } + args: { + } + } + } } - } - } - CALL [29] { - function: timestamp - args: { - IDENT [30] { - name: @index12 - } - } - } - CALL [31] { - function: getMinutes - target: { - IDENT [32] { - name: @index13 - } - } - args: { - } - } - CALL [33] { - function: getSeconds - target: { - IDENT [34] { - name: @index6 - } - } - args: { - } - } - CALL [35] { - function: _+_ - args: { - CALL [36] { - function: _+_ - args: { - IDENT [37] { - name: @index3 - } - CALL [38] { - function: getFullYear - target: { - IDENT [39] { - name: @index13 - } - } - args: { - } - } - } - } - CALL [40] { + CALL [28] { function: getFullYear target: { - IDENT [41] { - name: @index6 + IDENT [29] { + name: @index1 } } args: { @@ -467,69 +399,83 @@ CALL [1] { } } } - CALL [42] { + CALL [30] { function: _+_ args: { - CALL [43] { + CALL [31] { function: _+_ args: { - CALL [44] { + CALL [32] { function: _+_ args: { - IDENT [45] { - name: @index16 + IDENT [33] { + name: @index6 } - IDENT [46] { - name: @index3 + IDENT [34] { + name: @index4 } } } - IDENT [47] { - name: @index15 + CALL [35] { + function: getSeconds + target: { + IDENT [36] { + name: @index1 + } + } + args: { + } } } } - IDENT [48] { - name: @index10 + IDENT [37] { + name: @index5 } } } - CALL [49] { + CALL [38] { function: _+_ args: { - CALL [50] { + CALL [39] { function: _+_ args: { - CALL [51] { + CALL [40] { function: _+_ args: { - IDENT [52] { - name: @index17 + IDENT [41] { + name: @index7 } - IDENT [53] { - name: @index10 + IDENT [42] { + name: @index5 } } } - IDENT [54] { - name: @index14 + CALL [43] { + function: getMinutes + target: { + IDENT [44] { + name: @index3 + } + } + args: { + } } } } - IDENT [55] { - name: @index3 + IDENT [45] { + name: @index4 } } } } } - CALL [56] { + CALL [46] { function: _==_ args: { - IDENT [57] { - name: @index18 + IDENT [47] { + name: @index8 } - CONSTANT [58] { value: 13934 } + CONSTANT [48] { value: 13934 } } } } @@ -540,55 +486,49 @@ Source: {"a": 2}["a"] + {"a": 2}["a"] * {"a": 2}["a"] == 6 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_MAP [3] { - MAP_ENTRY [4] { - key: { - CONSTANT [5] { value: "a" } - } - value: { - CONSTANT [6] { value: 2 } - } - } - } - CALL [7] { + CALL [3] { function: _[_] args: { - IDENT [8] { - name: @index0 + MAP [4] { + MAP_ENTRY [5] { + key: { + CONSTANT [6] { value: "a" } + } + value: { + CONSTANT [7] { value: 2 } + } + } } - CONSTANT [9] { value: "a" } + CONSTANT [8] { value: "a" } } } + } + } + CALL [9] { + function: _==_ + args: { CALL [10] { function: _+_ args: { IDENT [11] { - name: @index1 + name: @index0 } CALL [12] { function: _*_ args: { IDENT [13] { - name: @index1 + name: @index0 } IDENT [14] { - name: @index1 + name: @index0 } } } } } - } - } - CALL [15] { - function: _==_ - args: { - IDENT [16] { - name: @index2 - } - CONSTANT [17] { value: 6 } + CONSTANT [15] { value: 6 } } } } @@ -599,56 +539,53 @@ Source: {'a': {'b': 1}, 'c': {'b': 1}, 'd': {'e': {'b': 1}}, 'e': {'e': {'b': 1} CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_MAP [3] { + MAP [3] { MAP_ENTRY [4] { key: { - CONSTANT [5] { value: "b" } + CONSTANT [5] { value: "e" } } value: { - CONSTANT [6] { value: 1 } + MAP [6] { + MAP_ENTRY [7] { + key: { + CONSTANT [8] { value: "b" } + } + value: { + CONSTANT [9] { value: 1 } + } + } + } } } } - CREATE_MAP [7] { - MAP_ENTRY [8] { + MAP [10] { + MAP_ENTRY [11] { key: { - CONSTANT [9] { value: "e" } + CONSTANT [12] { value: "b" } } value: { - IDENT [10] { - name: @index0 - } + CONSTANT [13] { value: 1 } } } } } } - CREATE_MAP [11] { - MAP_ENTRY [12] { - key: { - CONSTANT [13] { value: "a" } - } - value: { - IDENT [14] { - name: @index0 - } - } - } + MAP [14] { MAP_ENTRY [15] { key: { - CONSTANT [16] { value: "c" } + CONSTANT [16] { value: "a" } } value: { IDENT [17] { - name: @index0 + name: @index1 } } } MAP_ENTRY [18] { key: { - CONSTANT [19] { value: "d" } + CONSTANT [19] { value: "c" } } value: { IDENT [20] { @@ -658,11 +595,21 @@ CALL [1] { } MAP_ENTRY [21] { key: { - CONSTANT [22] { value: "e" } + CONSTANT [22] { value: "d" } } value: { IDENT [23] { - name: @index1 + name: @index0 + } + } + } + MAP_ENTRY [24] { + key: { + CONSTANT [25] { value: "e" } + } + value: { + IDENT [26] { + name: @index0 } } } @@ -675,9 +622,9 @@ Source: [1, [1,2,3,4], 2, [1,2,3,4], 5, [1,2,3,4], 7, [[1,2], [1,2,3,4]], [1,2]] CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { + LIST [3] { elements: { CONSTANT [4] { value: 1 } CONSTANT [5] { value: 2 } @@ -685,43 +632,40 @@ CALL [1] { CONSTANT [7] { value: 4 } } } - CREATE_LIST [8] { + LIST [8] { elements: { CONSTANT [9] { value: 1 } CONSTANT [10] { value: 2 } } } - CREATE_LIST [11] { - elements: { - IDENT [12] { - name: @index1 - } - IDENT [13] { - name: @index0 - } - } - } } } - CREATE_LIST [14] { + LIST [11] { elements: { - CONSTANT [15] { value: 1 } - IDENT [16] { + CONSTANT [12] { value: 1 } + IDENT [13] { name: @index0 } - CONSTANT [17] { value: 2 } - IDENT [18] { + CONSTANT [14] { value: 2 } + IDENT [15] { name: @index0 } - CONSTANT [19] { value: 5 } - IDENT [20] { + CONSTANT [16] { value: 5 } + IDENT [17] { name: @index0 } - CONSTANT [21] { value: 7 } - IDENT [22] { - name: @index2 + CONSTANT [18] { value: 7 } + LIST [19] { + elements: { + IDENT [20] { + name: @index1 + } + IDENT [21] { + name: @index0 + } + } } - IDENT [23] { + IDENT [22] { name: @index1 } } @@ -734,33 +678,30 @@ Source: msg.single_int64 + msg.single_int64 == 6 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { name: msg }.single_int64 } - CALL [5] { + } + } + CALL [5] { + function: _==_ + args: { + CALL [6] { function: _+_ args: { - IDENT [6] { + IDENT [7] { name: @index0 } - IDENT [7] { + IDENT [8] { name: @index0 } } } - } - } - CALL [8] { - function: _==_ - args: { - IDENT [9] { - name: @index1 - } - CONSTANT [10] { value: 6 } + CONSTANT [9] { value: 6 } } } } @@ -771,69 +712,67 @@ Source: msg.oneof_type.payload.single_int64 + msg.oneof_type.payload.single_int3 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 - }.single_int64 - } - SELECT [9] { - SELECT [10] { - SELECT [11] { - IDENT [12] { - name: @index1 + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg }.oneof_type }.payload }.single_int64 } - SELECT [13] { - IDENT [14] { - name: msg - }.single_int64 + SELECT [7] { + SELECT [8] { + IDENT [9] { + name: msg + }.oneof_type + }.payload } - CALL [15] { + CALL [10] { function: _+_ args: { - CALL [16] { + CALL [11] { function: _+_ args: { - IDENT [17] { - name: @index2 + IDENT [12] { + name: @index0 } - SELECT [18] { - IDENT [19] { + SELECT [13] { + IDENT [14] { name: @index1 }.single_int32 } } } - IDENT [20] { - name: @index2 + IDENT [15] { + name: @index0 } } } - CALL [21] { + SELECT [16] { + SELECT [17] { + SELECT [18] { + IDENT [19] { + name: @index1 + }.oneof_type + }.payload + }.single_int64 + } + CALL [20] { function: _+_ args: { - CALL [22] { + CALL [21] { function: _+_ args: { - IDENT [23] { - name: @index5 + IDENT [22] { + name: @index2 } - IDENT [24] { - name: @index4 + SELECT [23] { + IDENT [24] { + name: msg + }.single_int64 } } } @@ -848,7 +787,7 @@ CALL [1] { function: _==_ args: { IDENT [27] { - name: @index6 + name: @index4 } CONSTANT [28] { value: 31 } } @@ -861,77 +800,62 @@ Source: true || msg.oneof_type.payload.oneof_type.payload.oneof_type.payload.one CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload }.oneof_type } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } SELECT [7] { - IDENT [8] { - name: @index1 + SELECT [8] { + IDENT [9] { + name: @index0 + }.payload }.oneof_type } - SELECT [9] { - IDENT [10] { - name: @index2 - }.payload - } - SELECT [11] { - IDENT [12] { - name: @index3 - }.oneof_type + SELECT [10] { + SELECT [11] { + SELECT [12] { + IDENT [13] { + name: @index1 + }.payload + }.oneof_type + }.payload } - SELECT [13] { - SELECT [14] { - SELECT [15] { - IDENT [16] { - name: @index4 + SELECT [14] { + SELECT [15] { + SELECT [16] { + IDENT [17] { + name: @index1 }.child }.child }.payload } - SELECT [17] { - IDENT [18] { - name: @index5 - }.single_bool - } - SELECT [19] { - SELECT [20] { - SELECT [21] { - IDENT [22] { - name: @index4 - }.payload - }.oneof_type - }.payload - } - CALL [23] { + } + } + CALL [18] { + function: _||_ + args: { + CALL [19] { function: _||_ args: { - CONSTANT [24] { value: true } - SELECT [25] { - IDENT [26] { - name: @index7 + CONSTANT [20] { value: true } + SELECT [21] { + IDENT [22] { + name: @index2 }.single_bool } } } - } - } - CALL [27] { - function: _||_ - args: { - IDENT [28] { - name: @index8 - } - IDENT [29] { - name: @index6 + SELECT [23] { + IDENT [24] { + name: @index3 + }.single_bool } } } @@ -943,60 +867,51 @@ Source: msg.oneof_type.payload.map_int32_int64[1] + msg.oneof_type.payload.map_i CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload }.map_int32_int64 } - CALL [9] { + CALL [7] { function: _[_] args: { - IDENT [10] { - name: @index2 + IDENT [8] { + name: @index0 } - CONSTANT [11] { value: 1 } + CONSTANT [9] { value: 1 } } } - CALL [12] { + } + } + CALL [10] { + function: _==_ + args: { + CALL [11] { function: _+_ args: { - CALL [13] { + CALL [12] { function: _+_ args: { - IDENT [14] { - name: @index3 + IDENT [13] { + name: @index1 } - IDENT [15] { - name: @index3 + IDENT [14] { + name: @index1 } } } - IDENT [16] { - name: @index3 + IDENT [15] { + name: @index1 } } } - } - } - CALL [17] { - function: _==_ - args: { - IDENT [18] { - name: @index4 - } - CONSTANT [19] { value: 15 } + CONSTANT [16] { value: 15 } } } } @@ -1007,69 +922,63 @@ Source: msg.oneof_type.payload.map_int32_int64[0] + msg.oneof_type.payload.map_i CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload }.map_int32_int64 } - CALL [9] { + CALL [7] { function: _+_ args: { - CALL [10] { + CALL [8] { function: _+_ args: { - CALL [11] { + CALL [9] { function: _[_] args: { - IDENT [12] { - name: @index2 + IDENT [10] { + name: @index0 } - CONSTANT [13] { value: 0 } + CONSTANT [11] { value: 0 } } } - CALL [14] { + CALL [12] { function: _[_] args: { - IDENT [15] { - name: @index2 + IDENT [13] { + name: @index0 } - CONSTANT [16] { value: 1 } + CONSTANT [14] { value: 1 } } } } } - CALL [17] { + CALL [15] { function: _[_] args: { - IDENT [18] { - name: @index2 + IDENT [16] { + name: @index0 } - CONSTANT [19] { value: 2 } + CONSTANT [17] { value: 2 } } } } } } } - CALL [20] { + CALL [18] { function: _==_ args: { - IDENT [21] { - name: @index3 + IDENT [19] { + name: @index1 } - CONSTANT [22] { value: 8 } + CONSTANT [20] { value: 8 } } } } @@ -1080,7 +989,7 @@ Source: msg.oneof_type.payload.oneof_type.payload.oneof_type.payload.oneof_type. CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { SELECT [4] { @@ -1100,18 +1009,15 @@ CALL [1] { }.oneof_type }.payload } - SELECT [11] { - SELECT [12] { - IDENT [13] { - name: @index1 - }.oneof_type - }.payload - } } } - SELECT [14] { - IDENT [15] { - name: @index2 + SELECT [11] { + SELECT [12] { + SELECT [13] { + IDENT [14] { + name: @index1 + }.oneof_type + }.payload }.single_int64 } } @@ -1122,40 +1028,37 @@ Source: (msg.single_int64 > 0 ? msg.single_int64 : 0) == 3 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { name: msg }.single_int64 } - CALL [5] { + } + } + CALL [5] { + function: _==_ + args: { + CALL [6] { function: _?_:_ args: { - CALL [6] { + CALL [7] { function: _>_ args: { - IDENT [7] { + IDENT [8] { name: @index0 } - CONSTANT [8] { value: 0 } + CONSTANT [9] { value: 0 } } } - IDENT [9] { + IDENT [10] { name: @index0 } - CONSTANT [10] { value: 0 } + CONSTANT [11] { value: 0 } } } - } - } - CALL [11] { - function: _==_ - args: { - IDENT [12] { - name: @index1 - } - CONSTANT [13] { value: 3 } + CONSTANT [12] { value: 3 } } } } @@ -1166,7 +1069,7 @@ Source: false ? false : (msg.single_int64) + ((msg.single_int64 + 1) * 2) == 11 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { @@ -1196,27 +1099,24 @@ CALL [1] { } } } - CALL [12] { + } + } + CALL [12] { + function: _?_:_ + args: { + CONSTANT [13] { value: false } + CONSTANT [14] { value: false } + CALL [15] { function: _==_ args: { - IDENT [13] { + IDENT [16] { name: @index1 } - CONSTANT [14] { value: 11 } + CONSTANT [17] { value: 11 } } } } } - CALL [15] { - function: _?_:_ - args: { - CONSTANT [16] { value: false } - CONSTANT [17] { value: false } - IDENT [18] { - name: @index2 - } - } - } } } Test case: NESTED_TERNARY @@ -1225,7 +1125,7 @@ Source: (msg.single_int64 > 0 ? (msg.single_int32 > 0 ? msg.single_int64 + msg.s CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { @@ -1297,53 +1197,83 @@ Source: size([[1].exists(i, i > 0)]) + size([[1].exists(j, j > 0)]) + size([[2]. CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } + COMPREHENSION [3] { + iter_var: @it:0:0 + iter_range: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + } + } } - } - CALL [5] { - function: _>_ - args: { - IDENT [6] { - name: @c0:0 + accu_var: @ac:0:0 + accu_init: { + CONSTANT [6] { value: false } + } + loop_condition: { + CALL [7] { + function: @not_strictly_false + args: { + CALL [8] { + function: !_ + args: { + IDENT [9] { + name: @ac:0:0 + } + } + } + } } - CONSTANT [7] { value: 0 } } - } - CALL [8] { - function: _||_ - args: { - IDENT [9] { - name: @x0:0 + loop_step: { + CALL [10] { + function: _||_ + args: { + IDENT [11] { + name: @ac:0:0 + } + CALL [12] { + function: _>_ + args: { + IDENT [13] { + name: @it:0:0 + } + CONSTANT [14] { value: 0 } + } + } + } } - IDENT [10] { - name: @index1 + } + result: { + IDENT [15] { + name: @ac:0:0 } } } - COMPREHENSION [11] { - iter_var: @c0:0 + COMPREHENSION [16] { + iter_var: @it:0:0 iter_range: { - IDENT [12] { - name: @index0 + LIST [17] { + elements: { + CONSTANT [18] { value: 2 } + } } } - accu_var: @x0:0 + accu_var: @ac:0:0 accu_init: { - CONSTANT [13] { value: false } + CONSTANT [19] { value: false } } loop_condition: { - CALL [14] { + CALL [20] { function: @not_strictly_false args: { - CALL [15] { + CALL [21] { function: !_ args: { - IDENT [16] { - name: @x0:0 + IDENT [22] { + name: @ac:0:0 } } } @@ -1351,144 +1281,90 @@ CALL [1] { } } loop_step: { - IDENT [17] { - name: @index2 + CALL [23] { + function: _||_ + args: { + IDENT [24] { + name: @ac:0:0 + } + CALL [25] { + function: _>_ + args: { + IDENT [26] { + name: @it:0:0 + } + CONSTANT [27] { value: 1 } + } + } + } } } result: { - IDENT [18] { - name: @x0:0 - } - } - } - CREATE_LIST [19] { - elements: { - IDENT [20] { - name: @index3 + IDENT [28] { + name: @ac:0:0 } } } - CALL [21] { + CALL [29] { function: size args: { - IDENT [22] { - name: @index4 - } - } - } - CREATE_LIST [23] { - elements: { - CONSTANT [24] { value: 2 } - } - } - CALL [25] { - function: _>_ - args: { - IDENT [26] { - name: @c0:0 - } - CONSTANT [27] { value: 1 } - } - } - CALL [28] { - function: _||_ - args: { - IDENT [29] { - name: @x0:0 - } - IDENT [30] { - name: @index7 - } - } - } - COMPREHENSION [31] { - iter_var: @c0:0 - iter_range: { - IDENT [32] { - name: @index6 - } - } - accu_var: @x0:0 - accu_init: { - CONSTANT [33] { value: false } - } - loop_condition: { - CALL [34] { - function: @not_strictly_false - args: { - CALL [35] { - function: !_ - args: { - IDENT [36] { - name: @x0:0 - } - } + LIST [30] { + elements: { + IDENT [31] { + name: @index0 } } } } - loop_step: { - IDENT [37] { - name: @index8 - } - } - result: { - IDENT [38] { - name: @x0:0 - } - } } - CREATE_LIST [39] { - elements: { - IDENT [40] { - name: @index9 - } - } - } - CALL [41] { + CALL [32] { function: size args: { - IDENT [42] { - name: @index10 + LIST [33] { + elements: { + IDENT [34] { + name: @index1 + } + } } } } - CALL [43] { + CALL [35] { function: _+_ args: { - CALL [44] { + CALL [36] { function: _+_ args: { - CALL [45] { + CALL [37] { function: _+_ args: { - IDENT [46] { - name: @index5 + IDENT [38] { + name: @index2 } - IDENT [47] { - name: @index5 + IDENT [39] { + name: @index2 } } } - IDENT [48] { - name: @index11 + IDENT [40] { + name: @index3 } } } - IDENT [49] { - name: @index11 + IDENT [41] { + name: @index3 } } } } } - CALL [50] { + CALL [42] { function: _==_ args: { - IDENT [51] { - name: @index12 + IDENT [43] { + name: @index4 } - CONSTANT [52] { value: 4 } + CONSTANT [44] { value: 4 } } } } @@ -1499,53 +1375,30 @@ Source: [[1].exists(i, i > 0)] + [[1].exists(j, j > 0)] + [['a'].exists(k, k == CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - } - } - CALL [5] { - function: _>_ - args: { - IDENT [6] { - name: @c0:0 - } - CONSTANT [7] { value: 0 } - } - } - CALL [8] { - function: _||_ - args: { - IDENT [9] { - name: @x0:0 - } - IDENT [10] { - name: @index1 - } - } - } - COMPREHENSION [11] { - iter_var: @c0:0 + COMPREHENSION [3] { + iter_var: @it:0:0 iter_range: { - IDENT [12] { - name: @index0 + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + } } } - accu_var: @x0:0 + accu_var: @ac:0:0 accu_init: { - CONSTANT [13] { value: false } + CONSTANT [6] { value: false } } loop_condition: { - CALL [14] { + CALL [7] { function: @not_strictly_false args: { - CALL [15] { + CALL [8] { function: !_ args: { - IDENT [16] { - name: @x0:0 + IDENT [9] { + name: @ac:0:0 } } } @@ -1553,68 +1406,52 @@ CALL [1] { } } loop_step: { - IDENT [17] { - name: @index2 + CALL [10] { + function: _||_ + args: { + IDENT [11] { + name: @ac:0:0 + } + CALL [12] { + function: _>_ + args: { + IDENT [13] { + name: @it:0:0 + } + CONSTANT [14] { value: 0 } + } + } + } } } result: { - IDENT [18] { - name: @x0:0 - } - } - } - CREATE_LIST [19] { - elements: { - IDENT [20] { - name: @index3 - } - } - } - CREATE_LIST [21] { - elements: { - CONSTANT [22] { value: "a" } - } - } - CALL [23] { - function: _==_ - args: { - IDENT [24] { - name: @c0:1 - } - CONSTANT [25] { value: "a" } - } - } - CALL [26] { - function: _||_ - args: { - IDENT [27] { - name: @x0:1 - } - IDENT [28] { - name: @index6 + IDENT [15] { + name: @ac:0:0 } } } - COMPREHENSION [29] { - iter_var: @c0:1 + COMPREHENSION [16] { + iter_var: @it:0:1 iter_range: { - IDENT [30] { - name: @index5 + LIST [17] { + elements: { + CONSTANT [18] { value: "a" } + } } } - accu_var: @x0:1 + accu_var: @ac:0:1 accu_init: { - CONSTANT [31] { value: false } + CONSTANT [19] { value: false } } loop_condition: { - CALL [32] { + CALL [20] { function: @not_strictly_false args: { - CALL [33] { + CALL [21] { function: !_ args: { - IDENT [34] { - name: @x0:1 + IDENT [22] { + name: @ac:0:1 } } } @@ -1622,565 +1459,2644 @@ CALL [1] { } } loop_step: { - IDENT [35] { - name: @index7 + CALL [23] { + function: _||_ + args: { + IDENT [24] { + name: @ac:0:1 + } + CALL [25] { + function: _==_ + args: { + IDENT [26] { + name: @it:0:1 + } + CONSTANT [27] { value: "a" } + } + } + } } } result: { - IDENT [36] { - name: @x0:1 + IDENT [28] { + name: @ac:0:1 } } } - CREATE_LIST [37] { + LIST [29] { elements: { - IDENT [38] { - name: @index8 + IDENT [30] { + name: @index0 } } } - CREATE_LIST [39] { + LIST [31] { elements: { - CONSTANT [40] { value: true } - CONSTANT [41] { value: true } - CONSTANT [42] { value: true } - CONSTANT [43] { value: true } + IDENT [32] { + name: @index1 + } } } - CALL [44] { + CALL [33] { function: _+_ args: { - CALL [45] { + CALL [34] { function: _+_ args: { - CALL [46] { + CALL [35] { function: _+_ args: { - IDENT [47] { - name: @index4 + IDENT [36] { + name: @index2 } - IDENT [48] { - name: @index4 + IDENT [37] { + name: @index2 } } } - IDENT [49] { - name: @index9 + IDENT [38] { + name: @index3 } } } - IDENT [50] { - name: @index9 + IDENT [39] { + name: @index3 } } } } } - CALL [51] { + CALL [40] { function: _==_ args: { - IDENT [52] { - name: @index11 + IDENT [41] { + name: @index4 } - IDENT [53] { - name: @index10 + LIST [42] { + elements: { + CONSTANT [43] { value: true } + CONSTANT [44] { value: true } + CONSTANT [45] { value: true } + CONSTANT [46] { value: true } + } } } } } } -Test case: NESTED_MACROS -Source: [1,2,3].map(i, [1, 2, 3].map(i, i + 1)) == [[2, 3, 4], [2, 3, 4], [2, 3, 4]] +Test case: MULTIPLE_MACROS_3 +Source: [1].exists(i, i > 0) && [1].exists(j, j > 0) && [1].exists(k, k > 1) && [2].exists(l, l > 1) =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - CONSTANT [5] { value: 2 } - CONSTANT [6] { value: 3 } - } - } - CREATE_LIST [7] { - elements: { - CONSTANT [8] { value: 2 } - CONSTANT [9] { value: 3 } - CONSTANT [10] { value: 4 } - } - } - CREATE_LIST [11] { - elements: { - IDENT [12] { - name: @index1 - } - IDENT [13] { - name: @index1 - } - IDENT [14] { - name: @index1 + COMPREHENSION [3] { + iter_var: @it:0:0 + iter_range: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + } } } - } - CALL [15] { - function: _+_ - args: { - IDENT [16] { - name: @x1:0 - } - CREATE_LIST [17] { - elements: { - CALL [18] { - function: _+_ + accu_var: @ac:0:0 + accu_init: { + CONSTANT [6] { value: false } + } + loop_condition: { + CALL [7] { + function: @not_strictly_false + args: { + CALL [8] { + function: !_ args: { - IDENT [19] { - name: @c1:0 + IDENT [9] { + name: @ac:0:0 } - CONSTANT [20] { value: 1 } } } } } } - } - CREATE_LIST [21] { - elements: { - COMPREHENSION [22] { - iter_var: @c1:0 - iter_range: { - IDENT [23] { - name: @index0 + loop_step: { + CALL [10] { + function: _||_ + args: { + IDENT [11] { + name: @ac:0:0 } - } - accu_var: @x1:0 - accu_init: { - CREATE_LIST [24] { - elements: { + CALL [12] { + function: _>_ + args: { + IDENT [13] { + name: @it:0:0 + } + CONSTANT [14] { value: 0 } } } } - loop_condition: { - CONSTANT [25] { value: true } - } - loop_step: { - IDENT [26] { - name: @index3 - } - } - result: { - IDENT [27] { - name: @x1:0 + } + } + result: { + IDENT [15] { + name: @ac:0:0 + } + } + } + } + } + CALL [16] { + function: _&&_ + args: { + CALL [17] { + function: _&&_ + args: { + IDENT [18] { + name: @index0 + } + IDENT [19] { + name: @index0 + } + } + } + CALL [20] { + function: _&&_ + args: { + COMPREHENSION [21] { + iter_var: @it:0:0 + iter_range: { + LIST [22] { + elements: { + CONSTANT [23] { value: 1 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [24] { value: false } + } + loop_condition: { + CALL [25] { + function: @not_strictly_false + args: { + CALL [26] { + function: !_ + args: { + IDENT [27] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [28] { + function: _||_ + args: { + IDENT [29] { + name: @ac:0:0 + } + CALL [30] { + function: _>_ + args: { + IDENT [31] { + name: @it:0:0 + } + CONSTANT [32] { value: 1 } + } + } + } + } + } + result: { + IDENT [33] { + name: @ac:0:0 + } + } + } + COMPREHENSION [34] { + iter_var: @it:0:0 + iter_range: { + LIST [35] { + elements: { + CONSTANT [36] { value: 2 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [37] { value: false } + } + loop_condition: { + CALL [38] { + function: @not_strictly_false + args: { + CALL [39] { + function: !_ + args: { + IDENT [40] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [41] { + function: _||_ + args: { + IDENT [42] { + name: @ac:0:0 + } + CALL [43] { + function: _>_ + args: { + IDENT [44] { + name: @it:0:0 + } + CONSTANT [45] { value: 1 } + } + } + } + } + } + result: { + IDENT [46] { + name: @ac:0:0 + } + } + } + } + } + } + } + } +} +Test case: MULTIPLE_MACROS_COMP_V2_1 +Source: size([[1].exists(i,x,i > 0 && x >= 0)]) + size([[1].exists(j, y, j > 0 && y >= 0)]) + size([[2].exists(k,z,k > 1 && z >= 0)]) + size([[2].exists(l, w, l > 1 && w >= 0)]) == 4 +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + } + } + LIST [5] { + elements: { + CONSTANT [6] { value: 2 } + } + } + } + } + CALL [7] { + function: _==_ + args: { + CALL [8] { + function: _+_ + args: { + CALL [9] { + function: _+_ + args: { + CALL [10] { + function: _+_ + args: { + CALL [11] { + function: size + args: { + LIST [12] { + elements: { + COMPREHENSION [13] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + IDENT [14] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [15] { value: false } + } + loop_condition: { + CALL [16] { + function: @not_strictly_false + args: { + CALL [17] { + function: !_ + args: { + IDENT [18] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [19] { + function: _||_ + args: { + IDENT [20] { + name: @ac:0:0 + } + CALL [21] { + function: _&&_ + args: { + CALL [22] { + function: _>_ + args: { + IDENT [23] { + name: @it:0:0 + } + CONSTANT [24] { value: 0 } + } + } + CALL [25] { + function: _>=_ + args: { + IDENT [26] { + name: @it2:0:0 + } + CONSTANT [27] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [28] { + name: @ac:0:0 + } + } + } + } + } + } + } + CALL [29] { + function: size + args: { + LIST [30] { + elements: { + COMPREHENSION [31] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + IDENT [32] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [33] { value: false } + } + loop_condition: { + CALL [34] { + function: @not_strictly_false + args: { + CALL [35] { + function: !_ + args: { + IDENT [36] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [37] { + function: _||_ + args: { + IDENT [38] { + name: @ac:0:0 + } + CALL [39] { + function: _&&_ + args: { + CALL [40] { + function: _>_ + args: { + IDENT [41] { + name: @it:0:0 + } + CONSTANT [42] { value: 0 } + } + } + CALL [43] { + function: _>=_ + args: { + IDENT [44] { + name: @it2:0:0 + } + CONSTANT [45] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [46] { + name: @ac:0:0 + } + } + } + } + } + } + } + } + } + CALL [47] { + function: size + args: { + LIST [48] { + elements: { + COMPREHENSION [49] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + IDENT [50] { + name: @index1 + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [51] { value: false } + } + loop_condition: { + CALL [52] { + function: @not_strictly_false + args: { + CALL [53] { + function: !_ + args: { + IDENT [54] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [55] { + function: _||_ + args: { + IDENT [56] { + name: @ac:0:0 + } + CALL [57] { + function: _&&_ + args: { + CALL [58] { + function: _>_ + args: { + IDENT [59] { + name: @it:0:0 + } + CONSTANT [60] { value: 1 } + } + } + CALL [61] { + function: _>=_ + args: { + IDENT [62] { + name: @it2:0:0 + } + CONSTANT [63] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [64] { + name: @ac:0:0 + } + } + } + } + } + } + } + } + } + CALL [65] { + function: size + args: { + LIST [66] { + elements: { + COMPREHENSION [67] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + IDENT [68] { + name: @index1 + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [69] { value: false } + } + loop_condition: { + CALL [70] { + function: @not_strictly_false + args: { + CALL [71] { + function: !_ + args: { + IDENT [72] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [73] { + function: _||_ + args: { + IDENT [74] { + name: @ac:0:0 + } + CALL [75] { + function: _&&_ + args: { + CALL [76] { + function: _>_ + args: { + IDENT [77] { + name: @it:0:0 + } + CONSTANT [78] { value: 1 } + } + } + CALL [79] { + function: _>=_ + args: { + IDENT [80] { + name: @it2:0:0 + } + CONSTANT [81] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [82] { + name: @ac:0:0 + } + } + } + } + } + } + } + } + } + CONSTANT [83] { value: 4 } + } + } + } +} +Test case: MULTIPLE_MACROS_COMP_V2_2 +Source: [1].exists(i, x, i > 0 && x > 0) && [1].exists(j, y, j > 0 && y > 0) && [1].exists(k, z, k > 1 && z > 0) && [2].exists(l, w, l > 1 && w > 0) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + } + } + LIST [5] { + elements: { + CONSTANT [6] { value: 2 } + } + } + } + } + CALL [7] { + function: _&&_ + args: { + CALL [8] { + function: _&&_ + args: { + COMPREHENSION [9] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + IDENT [10] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [11] { value: false } + } + loop_condition: { + CALL [12] { + function: @not_strictly_false + args: { + CALL [13] { + function: !_ + args: { + IDENT [14] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [15] { + function: _||_ + args: { + IDENT [16] { + name: @ac:0:0 + } + CALL [17] { + function: _&&_ + args: { + CALL [18] { + function: _>_ + args: { + IDENT [19] { + name: @it:0:0 + } + CONSTANT [20] { value: 0 } + } + } + CALL [21] { + function: _>_ + args: { + IDENT [22] { + name: @it2:0:0 + } + CONSTANT [23] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [24] { + name: @ac:0:0 + } + } + } + COMPREHENSION [25] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + IDENT [26] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [27] { value: false } + } + loop_condition: { + CALL [28] { + function: @not_strictly_false + args: { + CALL [29] { + function: !_ + args: { + IDENT [30] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [31] { + function: _||_ + args: { + IDENT [32] { + name: @ac:0:0 + } + CALL [33] { + function: _&&_ + args: { + CALL [34] { + function: _>_ + args: { + IDENT [35] { + name: @it:0:0 + } + CONSTANT [36] { value: 0 } + } + } + CALL [37] { + function: _>_ + args: { + IDENT [38] { + name: @it2:0:0 + } + CONSTANT [39] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [40] { + name: @ac:0:0 + } + } + } + } + } + CALL [41] { + function: _&&_ + args: { + COMPREHENSION [42] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + IDENT [43] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [44] { value: false } + } + loop_condition: { + CALL [45] { + function: @not_strictly_false + args: { + CALL [46] { + function: !_ + args: { + IDENT [47] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [48] { + function: _||_ + args: { + IDENT [49] { + name: @ac:0:0 + } + CALL [50] { + function: _&&_ + args: { + CALL [51] { + function: _>_ + args: { + IDENT [52] { + name: @it:0:0 + } + CONSTANT [53] { value: 1 } + } + } + CALL [54] { + function: _>_ + args: { + IDENT [55] { + name: @it2:0:0 + } + CONSTANT [56] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [57] { + name: @ac:0:0 + } + } + } + COMPREHENSION [58] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + IDENT [59] { + name: @index1 + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [60] { value: false } + } + loop_condition: { + CALL [61] { + function: @not_strictly_false + args: { + CALL [62] { + function: !_ + args: { + IDENT [63] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [64] { + function: _||_ + args: { + IDENT [65] { + name: @ac:0:0 + } + CALL [66] { + function: _&&_ + args: { + CALL [67] { + function: _>_ + args: { + IDENT [68] { + name: @it:0:0 + } + CONSTANT [69] { value: 1 } + } + } + CALL [70] { + function: _>_ + args: { + IDENT [71] { + name: @it2:0:0 + } + CONSTANT [72] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [73] { + name: @ac:0:0 + } + } + } + } + } + } + } + } +} +Test case: NESTED_MACROS +Source: [1,2,3].map(i, [1, 2, 3].map(i, i + 1)) == [[2, 3, 4], [2, 3, 4], [2, 3, 4]] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + CONSTANT [5] { value: 2 } + CONSTANT [6] { value: 3 } + } + } + LIST [7] { + elements: { + CONSTANT [8] { value: 2 } + CONSTANT [9] { value: 3 } + CONSTANT [10] { value: 4 } + } + } + LIST [11] { + elements: { + IDENT [12] { + name: @index1 + } + IDENT [13] { + name: @index1 + } + IDENT [14] { + name: @index1 + } + } + } + } + } + CALL [15] { + function: _==_ + args: { + COMPREHENSION [16] { + iter_var: @it:1:0 + iter_range: { + IDENT [17] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [18] { + elements: { + } + } + } + loop_condition: { + CONSTANT [19] { value: true } + } + loop_step: { + CALL [20] { + function: _+_ + args: { + IDENT [21] { + name: @ac:1:0 + } + LIST [22] { + elements: { + COMPREHENSION [23] { + iter_var: @it:0:0 + iter_range: { + IDENT [24] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [25] { + elements: { + } + } + } + loop_condition: { + CONSTANT [26] { value: true } + } + loop_step: { + CALL [27] { + function: _+_ + args: { + IDENT [28] { + name: @ac:0:0 + } + LIST [29] { + elements: { + CALL [30] { + function: _+_ + args: { + IDENT [31] { + name: @it:0:0 + } + CONSTANT [32] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [33] { + name: @ac:0:0 + } + } + } + } + } + } + } + } + result: { + IDENT [34] { + name: @ac:1:0 + } + } + } + IDENT [35] { + name: @index2 + } + } + } + } +} +Test case: NESTED_MACROS_2 +Source: [1, 2].map(y, [1, 2, 3].filter(x, x == y)) == [[1], [2]] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + } + } + LIST [6] { + elements: { + CONSTANT [7] { value: 2 } + } + } + } + } + LIST [8] { + elements: { + CONSTANT [9] { value: 1 } + CONSTANT [10] { value: 2 } + } + } + LIST [11] { + elements: { + CONSTANT [12] { value: 1 } + CONSTANT [13] { value: 2 } + CONSTANT [14] { value: 3 } + } + } + } + } + CALL [15] { + function: _==_ + args: { + COMPREHENSION [16] { + iter_var: @it:1:0 + iter_range: { + IDENT [17] { + name: @index1 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [18] { + elements: { + } + } + } + loop_condition: { + CONSTANT [19] { value: true } + } + loop_step: { + CALL [20] { + function: _+_ + args: { + IDENT [21] { + name: @ac:1:0 + } + LIST [22] { + elements: { + COMPREHENSION [23] { + iter_var: @it:0:0 + iter_range: { + IDENT [24] { + name: @index2 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [25] { + elements: { + } + } + } + loop_condition: { + CONSTANT [26] { value: true } + } + loop_step: { + CALL [27] { + function: _?_:_ + args: { + CALL [28] { + function: _==_ + args: { + IDENT [29] { + name: @it:0:0 + } + IDENT [30] { + name: @it:1:0 + } + } + } + CALL [31] { + function: _+_ + args: { + IDENT [32] { + name: @ac:0:0 + } + LIST [33] { + elements: { + IDENT [34] { + name: @it:0:0 + } + } + } + } + } + IDENT [35] { + name: @ac:0:0 + } + } + } + } + result: { + IDENT [36] { + name: @ac:0:0 + } + } + } + } + } + } + } + } + result: { + IDENT [37] { + name: @ac:1:0 + } + } + } + IDENT [38] { + name: @index0 + } + } + } + } +} +Test case: NESTED_MACROS_3 +Source: [1,2,3].map(i, i * 2) + [1,2,3].map(i, i * 2).map(i, i * 2) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + CONSTANT [5] { value: 2 } + CONSTANT [6] { value: 3 } + } + } + } + } + CALL [7] { + function: _+_ + args: { + COMPREHENSION [8] { + iter_var: @it:0:0 + iter_range: { + IDENT [9] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [10] { + elements: { + } + } + } + loop_condition: { + CONSTANT [11] { value: true } + } + loop_step: { + CALL [12] { + function: _+_ + args: { + IDENT [13] { + name: @ac:0:0 + } + LIST [14] { + elements: { + CALL [15] { + function: _*_ + args: { + IDENT [16] { + name: @it:0:0 + } + CONSTANT [17] { value: 2 } + } + } + } + } + } + } + } + result: { + IDENT [18] { + name: @ac:0:0 + } + } + } + COMPREHENSION [19] { + iter_var: @it:1:0 + iter_range: { + COMPREHENSION [20] { + iter_var: @it:0:0 + iter_range: { + IDENT [21] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [22] { + elements: { + } + } + } + loop_condition: { + CONSTANT [23] { value: true } + } + loop_step: { + CALL [24] { + function: _+_ + args: { + IDENT [25] { + name: @ac:0:0 + } + LIST [26] { + elements: { + CALL [27] { + function: _*_ + args: { + IDENT [28] { + name: @it:0:0 + } + CONSTANT [29] { value: 2 } + } + } + } + } + } + } + } + result: { + IDENT [30] { + name: @ac:0:0 + } + } + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [31] { + elements: { + } + } + } + loop_condition: { + CONSTANT [32] { value: true } + } + loop_step: { + CALL [33] { + function: _+_ + args: { + IDENT [34] { + name: @ac:1:0 + } + LIST [35] { + elements: { + CALL [36] { + function: _*_ + args: { + IDENT [37] { + name: @it:1:0 + } + CONSTANT [38] { value: 2 } + } + } + } + } + } + } + } + result: { + IDENT [39] { + name: @ac:1:0 + } + } + } + } + } + } +} +Test case: NESTED_MACROS_COMP_V2_1 +Source: [1,2,3].transformList(i, v, [1, 2, 3].transformList(i, v, i + v + 1)) == [[2, 4, 6], [2, 4, 6], [2, 4, 6]] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + CONSTANT [5] { value: 2 } + CONSTANT [6] { value: 3 } + } + } + LIST [7] { + elements: { + CONSTANT [8] { value: 2 } + CONSTANT [9] { value: 4 } + CONSTANT [10] { value: 6 } + } + } + LIST [11] { + elements: { + IDENT [12] { + name: @index1 + } + IDENT [13] { + name: @index1 + } + IDENT [14] { + name: @index1 + } + } + } + } + } + CALL [15] { + function: _==_ + args: { + COMPREHENSION [16] { + iter_var: @it:1:0 + iter_var2: @it2:1:0 + iter_range: { + IDENT [17] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [18] { + elements: { + } + } + } + loop_condition: { + CONSTANT [19] { value: true } + } + loop_step: { + CALL [20] { + function: _+_ + args: { + IDENT [21] { + name: @ac:1:0 + } + LIST [22] { + elements: { + COMPREHENSION [23] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + IDENT [24] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [25] { + elements: { + } + } + } + loop_condition: { + CONSTANT [26] { value: true } + } + loop_step: { + CALL [27] { + function: _+_ + args: { + IDENT [28] { + name: @ac:0:0 + } + LIST [29] { + elements: { + CALL [30] { + function: _+_ + args: { + CALL [31] { + function: _+_ + args: { + IDENT [32] { + name: @it:0:0 + } + IDENT [33] { + name: @it2:0:0 + } + } + } + CONSTANT [34] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [35] { + name: @ac:0:0 + } + } + } + } + } + } + } + } + result: { + IDENT [36] { + name: @ac:1:0 + } + } + } + IDENT [37] { + name: @index2 + } + } + } + } +} +Test case: NESTED_MACROS_COMP_V2_2 +Source: [1, 2].transformList(i, y, [1, 2, 3].filter(x, x == y && i < y)) == [[1], [2]] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + } + } + LIST [6] { + elements: { + CONSTANT [7] { value: 2 } + } + } + } + } + LIST [8] { + elements: { + CONSTANT [9] { value: 1 } + CONSTANT [10] { value: 2 } + } + } + LIST [11] { + elements: { + CONSTANT [12] { value: 1 } + CONSTANT [13] { value: 2 } + CONSTANT [14] { value: 3 } + } + } + } + } + CALL [15] { + function: _==_ + args: { + COMPREHENSION [16] { + iter_var: @it:1:0 + iter_var2: @it2:1:0 + iter_range: { + IDENT [17] { + name: @index1 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [18] { + elements: { + } + } + } + loop_condition: { + CONSTANT [19] { value: true } + } + loop_step: { + CALL [20] { + function: _+_ + args: { + IDENT [21] { + name: @ac:1:0 + } + LIST [22] { + elements: { + COMPREHENSION [23] { + iter_var: @it:0:0 + iter_range: { + IDENT [24] { + name: @index2 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [25] { + elements: { + } + } + } + loop_condition: { + CONSTANT [26] { value: true } + } + loop_step: { + CALL [27] { + function: _?_:_ + args: { + CALL [28] { + function: _&&_ + args: { + CALL [29] { + function: _==_ + args: { + IDENT [30] { + name: @it:0:0 + } + IDENT [31] { + name: @it2:1:0 + } + } + } + CALL [32] { + function: _<_ + args: { + IDENT [33] { + name: @it:1:0 + } + IDENT [34] { + name: @it2:1:0 + } + } + } + } + } + CALL [35] { + function: _+_ + args: { + IDENT [36] { + name: @ac:0:0 + } + LIST [37] { + elements: { + IDENT [38] { + name: @it:0:0 + } + } + } + } + } + IDENT [39] { + name: @ac:0:0 + } + } + } + } + result: { + IDENT [40] { + name: @ac:0:0 + } + } + } + } + } + } + } + } + result: { + IDENT [41] { + name: @ac:1:0 + } + } + } + IDENT [42] { + name: @index0 + } + } + } + } +} +Test case: ADJACENT_NESTED_MACROS +Source: [1,2,3].map(i, [1, 2, 3].map(i, i + 1)) == [1,2,3].map(j, [1, 2, 3].map(j, j + 1)) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + CONSTANT [5] { value: 2 } + CONSTANT [6] { value: 3 } + } + } + } + } + CALL [7] { + function: _==_ + args: { + COMPREHENSION [8] { + iter_var: @it:1:0 + iter_range: { + IDENT [9] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [10] { + elements: { + } + } + } + loop_condition: { + CONSTANT [11] { value: true } + } + loop_step: { + CALL [12] { + function: _+_ + args: { + IDENT [13] { + name: @ac:1:0 + } + LIST [14] { + elements: { + COMPREHENSION [15] { + iter_var: @it:0:0 + iter_range: { + IDENT [16] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [17] { + elements: { + } + } + } + loop_condition: { + CONSTANT [18] { value: true } + } + loop_step: { + CALL [19] { + function: _+_ + args: { + IDENT [20] { + name: @ac:0:0 + } + LIST [21] { + elements: { + CALL [22] { + function: _+_ + args: { + IDENT [23] { + name: @it:0:0 + } + CONSTANT [24] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [25] { + name: @ac:0:0 + } + } + } + } + } + } + } + } + result: { + IDENT [26] { + name: @ac:1:0 + } + } + } + COMPREHENSION [27] { + iter_var: @it:1:0 + iter_range: { + IDENT [28] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [29] { + elements: { + } + } + } + loop_condition: { + CONSTANT [30] { value: true } + } + loop_step: { + CALL [31] { + function: _+_ + args: { + IDENT [32] { + name: @ac:1:0 + } + LIST [33] { + elements: { + COMPREHENSION [34] { + iter_var: @it:0:0 + iter_range: { + IDENT [35] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [36] { + elements: { + } + } + } + loop_condition: { + CONSTANT [37] { value: true } + } + loop_step: { + CALL [38] { + function: _+_ + args: { + IDENT [39] { + name: @ac:0:0 + } + LIST [40] { + elements: { + CALL [41] { + function: _+_ + args: { + IDENT [42] { + name: @it:0:0 + } + CONSTANT [43] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [44] { + name: @ac:0:0 + } + } + } + } + } + } + } + } + result: { + IDENT [45] { + name: @ac:1:0 + } + } + } + } + } + } +} +Test case: ADJACENT_NESTED_MACROS_COMP_V2 +Source: [1,2,3].transformMap(i, x, [1, 2, 3].transformMap(i, x, i + x + 1)) == [1,2,3].transformMap(j, y, [1, 2, 3].transformMap(j, y, j + y + 1)) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + CONSTANT [5] { value: 2 } + CONSTANT [6] { value: 3 } + } + } + } + } + CALL [7] { + function: _==_ + args: { + COMPREHENSION [8] { + iter_var: @it:1:0 + iter_var2: @it2:1:0 + iter_range: { + IDENT [9] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + MAP [10] { + + } + } + loop_condition: { + CONSTANT [11] { value: true } + } + loop_step: { + CALL [12] { + function: cel.@mapInsert + args: { + IDENT [13] { + name: @ac:1:0 + } + IDENT [14] { + name: @it:1:0 + } + COMPREHENSION [15] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + IDENT [16] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + MAP [17] { + + } + } + loop_condition: { + CONSTANT [18] { value: true } + } + loop_step: { + CALL [19] { + function: cel.@mapInsert + args: { + IDENT [20] { + name: @ac:0:0 + } + IDENT [21] { + name: @it:0:0 + } + CALL [22] { + function: _+_ + args: { + CALL [23] { + function: _+_ + args: { + IDENT [24] { + name: @it:0:0 + } + IDENT [25] { + name: @it2:0:0 + } + } + } + CONSTANT [26] { value: 1 } + } + } + } + } + } + result: { + IDENT [27] { + name: @ac:0:0 + } + } + } + } + } + } + result: { + IDENT [28] { + name: @ac:1:0 + } + } + } + COMPREHENSION [29] { + iter_var: @it:1:0 + iter_var2: @it2:1:0 + iter_range: { + IDENT [30] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + MAP [31] { + + } + } + loop_condition: { + CONSTANT [32] { value: true } + } + loop_step: { + CALL [33] { + function: cel.@mapInsert + args: { + IDENT [34] { + name: @ac:1:0 + } + IDENT [35] { + name: @it:1:0 + } + COMPREHENSION [36] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + IDENT [37] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + MAP [38] { + + } + } + loop_condition: { + CONSTANT [39] { value: true } + } + loop_step: { + CALL [40] { + function: cel.@mapInsert + args: { + IDENT [41] { + name: @ac:0:0 + } + IDENT [42] { + name: @it:0:0 + } + CALL [43] { + function: _+_ + args: { + CALL [44] { + function: _+_ + args: { + IDENT [45] { + name: @it:0:0 + } + IDENT [46] { + name: @it2:0:0 + } + } + } + CONSTANT [47] { value: 1 } + } + } + } + } + } + result: { + IDENT [48] { + name: @ac:0:0 + } + } + } + } + } + } + result: { + IDENT [49] { + name: @ac:1:0 + } + } + } + } + } + } +} +Test case: INCLUSION_LIST +Source: 1 in [1,2,3] && 2 in [1,2,3] && 3 in [3, [1,2,3]] && 1 in [1,2,3] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + CALL [3] { + function: @in + args: { + CONSTANT [4] { value: 1 } + LIST [5] { + elements: { + CONSTANT [6] { value: 1 } + CONSTANT [7] { value: 2 } + CONSTANT [8] { value: 3 } + } + } + } + } + LIST [9] { + elements: { + CONSTANT [10] { value: 1 } + CONSTANT [11] { value: 2 } + CONSTANT [12] { value: 3 } + } + } + CALL [13] { + function: _&&_ + args: { + CALL [14] { + function: @in + args: { + CONSTANT [15] { value: 3 } + LIST [16] { + elements: { + CONSTANT [17] { value: 3 } + IDENT [18] { + name: @index1 + } + } + } + } + } + IDENT [19] { + name: @index0 + } + } + } + } + } + CALL [20] { + function: _&&_ + args: { + CALL [21] { + function: _&&_ + args: { + IDENT [22] { + name: @index0 + } + CALL [23] { + function: @in + args: { + CONSTANT [24] { value: 2 } + IDENT [25] { + name: @index1 } } } } } - COMPREHENSION [28] { - iter_var: @c0:0 + IDENT [26] { + name: @index2 + } + } + } + } +} +Test case: INCLUSION_MAP +Source: 2 in {'a': 1, 2: {true: false}, 3: {true: false}} +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + MAP [3] { + MAP_ENTRY [4] { + key: { + CONSTANT [5] { value: true } + } + value: { + CONSTANT [6] { value: false } + } + } + } + } + } + CALL [7] { + function: @in + args: { + CONSTANT [8] { value: 2 } + MAP [9] { + MAP_ENTRY [10] { + key: { + CONSTANT [11] { value: "a" } + } + value: { + CONSTANT [12] { value: 1 } + } + } + MAP_ENTRY [13] { + key: { + CONSTANT [14] { value: 2 } + } + value: { + IDENT [15] { + name: @index0 + } + } + } + MAP_ENTRY [16] { + key: { + CONSTANT [17] { value: 3 } + } + value: { + IDENT [18] { + name: @index0 + } + } + } + } + } + } + } +} +Test case: MACRO_ITER_VAR_NOT_REFERENCED +Source: [1,2].map(i, [1, 2].map(i, [3,4])) == [[[3, 4], [3, 4]], [[3, 4], [3, 4]]] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + LIST [4] { + elements: { + CONSTANT [5] { value: 3 } + CONSTANT [6] { value: 4 } + } + } + LIST [7] { + elements: { + CONSTANT [8] { value: 3 } + CONSTANT [9] { value: 4 } + } + } + } + } + LIST [10] { + elements: { + CONSTANT [11] { value: 1 } + CONSTANT [12] { value: 2 } + } + } + LIST [13] { + elements: { + LIST [14] { + elements: { + CONSTANT [15] { value: 3 } + CONSTANT [16] { value: 4 } + } + } + } + } + COMPREHENSION [17] { + iter_var: @it:0:0 iter_range: { - IDENT [29] { - name: @index0 + IDENT [18] { + name: @index1 } } - accu_var: @x0:0 + accu_var: @ac:0:0 accu_init: { - CREATE_LIST [30] { + LIST [19] { elements: { } } } loop_condition: { - CONSTANT [31] { value: true } + CONSTANT [20] { value: true } } loop_step: { - CALL [32] { + CALL [21] { function: _+_ args: { - IDENT [33] { - name: @x0:0 + IDENT [22] { + name: @ac:0:0 } - IDENT [34] { - name: @index4 + IDENT [23] { + name: @index2 } } } } result: { - IDENT [35] { - name: @x0:0 + IDENT [24] { + name: @ac:0:0 } } } } } - CALL [36] { + CALL [25] { function: _==_ args: { - IDENT [37] { - name: @index5 + COMPREHENSION [26] { + iter_var: @it:1:0 + iter_range: { + IDENT [27] { + name: @index1 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [28] { + elements: { + } + } + } + loop_condition: { + CONSTANT [29] { value: true } + } + loop_step: { + CALL [30] { + function: _+_ + args: { + IDENT [31] { + name: @ac:1:0 + } + LIST [32] { + elements: { + IDENT [33] { + name: @index3 + } + } + } + } + } + } + result: { + IDENT [34] { + name: @ac:1:0 + } + } } - IDENT [38] { - name: @index2 + LIST [35] { + elements: { + IDENT [36] { + name: @index0 + } + IDENT [37] { + name: @index0 + } + } } } } } } -Test case: NESTED_MACROS_2 -Source: [1, 2].map(y, [1, 2, 3].filter(x, x == y)) == [[1], [2]] +Test case: MACRO_ITER_VAR2_NOT_REFERENCED +Source: [1,2].transformList(i, v, [1, 2].transformList(i, v, [3,4])) == [[[3, 4], [3, 4]], [[3, 4], [3, 4]]] =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { + LIST [3] { elements: { - CREATE_LIST [4] { + LIST [4] { elements: { - CONSTANT [5] { value: 1 } + CONSTANT [5] { value: 3 } + CONSTANT [6] { value: 4 } } } - CREATE_LIST [6] { + LIST [7] { elements: { - CONSTANT [7] { value: 2 } + CONSTANT [8] { value: 3 } + CONSTANT [9] { value: 4 } } } } } - CALL [8] { - function: _?_:_ - args: { - CALL [9] { - function: _==_ - args: { - IDENT [10] { - name: @c1:0 - } - IDENT [11] { - name: @c0:0 - } - } - } - CALL [12] { - function: _+_ - args: { - IDENT [13] { - name: @x1:0 - } - CREATE_LIST [14] { - elements: { - IDENT [15] { - name: @c1:0 - } - } - } - } - } - IDENT [16] { - name: @x1:0 - } + LIST [10] { + elements: { + CONSTANT [11] { value: 1 } + CONSTANT [12] { value: 2 } } } - CREATE_LIST [17] { + LIST [13] { elements: { - COMPREHENSION [18] { - iter_var: @c1:0 - iter_range: { - CREATE_LIST [19] { - elements: { - CONSTANT [20] { value: 1 } - CONSTANT [21] { value: 2 } - CONSTANT [22] { value: 3 } - } - } - } - accu_var: @x1:0 - accu_init: { - CREATE_LIST [23] { - elements: { - } - } - } - loop_condition: { - CONSTANT [24] { value: true } - } - loop_step: { - IDENT [25] { - name: @index1 - } - } - result: { - IDENT [26] { - name: @x1:0 - } + LIST [14] { + elements: { + CONSTANT [15] { value: 3 } + CONSTANT [16] { value: 4 } } } } } - COMPREHENSION [27] { - iter_var: @c0:0 + COMPREHENSION [17] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 iter_range: { - CREATE_LIST [28] { - elements: { - CONSTANT [29] { value: 1 } - CONSTANT [30] { value: 2 } - } + IDENT [18] { + name: @index1 } } - accu_var: @x0:0 + accu_var: @ac:0:0 accu_init: { - CREATE_LIST [31] { + LIST [19] { elements: { } } } loop_condition: { - CONSTANT [32] { value: true } + CONSTANT [20] { value: true } } loop_step: { - CALL [33] { + CALL [21] { function: _+_ args: { - IDENT [34] { - name: @x0:0 + IDENT [22] { + name: @ac:0:0 } - IDENT [35] { + IDENT [23] { name: @index2 } } } } result: { - IDENT [36] { - name: @x0:0 + IDENT [24] { + name: @ac:0:0 } } } } } - CALL [37] { + CALL [25] { function: _==_ args: { - IDENT [38] { - name: @index3 - } - IDENT [39] { - name: @index0 - } - } - } - } -} -Test case: INCLUSION_LIST -Source: 1 in [1,2,3] && 2 in [1,2,3] && 3 in [3, [1,2,3]] && 1 in [1,2,3] -=====> -CALL [1] { - function: cel.@block - args: { - CREATE_LIST [2] { - elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - CONSTANT [5] { value: 2 } - CONSTANT [6] { value: 3 } + COMPREHENSION [26] { + iter_var: @it:1:0 + iter_var2: @it2:1:0 + iter_range: { + IDENT [27] { + name: @index1 + } } - } - CALL [7] { - function: @in - args: { - CONSTANT [8] { value: 1 } - IDENT [9] { - name: @index0 + accu_var: @ac:1:0 + accu_init: { + LIST [28] { + elements: { + } } } - } - CALL [10] { - function: _&&_ - args: { - CALL [11] { - function: @in + loop_condition: { + CONSTANT [29] { value: true } + } + loop_step: { + CALL [30] { + function: _+_ args: { - CONSTANT [12] { value: 3 } - CREATE_LIST [13] { + IDENT [31] { + name: @ac:1:0 + } + LIST [32] { elements: { - CONSTANT [14] { value: 3 } - IDENT [15] { - name: @index0 + IDENT [33] { + name: @index3 } } } } } - IDENT [16] { - name: @index1 + } + result: { + IDENT [34] { + name: @ac:1:0 } } } - CALL [17] { - function: _&&_ - args: { - IDENT [18] { - name: @index1 - } - CALL [19] { - function: @in - args: { - CONSTANT [20] { value: 2 } - IDENT [21] { - name: @index0 - } - } + LIST [35] { + elements: { + IDENT [36] { + name: @index0 + } + IDENT [37] { + name: @index0 } } } } } - CALL [22] { - function: _&&_ - args: { - IDENT [23] { - name: @index3 - } - IDENT [24] { - name: @index2 - } - } - } } } -Test case: INCLUSION_MAP -Source: 2 in {'a': 1, 2: {true: false}, 3: {true: false}} +Test case: MACRO_SHADOWED_VARIABLE +Source: [x - 1 > 3 ? x - 1 : 5].exists(x, x - 1 > 3) || x - 1 > 3 =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_MAP [3] { - MAP_ENTRY [4] { - key: { - CONSTANT [5] { value: true } - } - value: { - CONSTANT [6] { value: false } + CALL [3] { + function: _>_ + args: { + CALL [4] { + function: _-_ + args: { + IDENT [5] { + name: x + } + CONSTANT [6] { value: 1 } + } } + CONSTANT [7] { value: 3 } } } - CREATE_MAP [7] { - MAP_ENTRY [8] { - key: { - CONSTANT [9] { value: "a" } - } - value: { - CONSTANT [10] { value: 1 } + LIST [8] { + elements: { + CALL [9] { + function: _?_:_ + args: { + IDENT [10] { + name: @index0 + } + CALL [11] { + function: _-_ + args: { + IDENT [12] { + name: x + } + CONSTANT [13] { value: 1 } + } + } + CONSTANT [14] { value: 5 } + } } } - MAP_ENTRY [11] { - key: { - CONSTANT [12] { value: 2 } + } + } + } + CALL [15] { + function: _||_ + args: { + COMPREHENSION [16] { + iter_var: @it:0:0 + iter_range: { + IDENT [17] { + name: @index1 } - value: { - IDENT [13] { - name: @index0 + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [18] { value: false } + } + loop_condition: { + CALL [19] { + function: @not_strictly_false + args: { + CALL [20] { + function: !_ + args: { + IDENT [21] { + name: @ac:0:0 + } + } + } } } } - MAP_ENTRY [14] { - key: { - CONSTANT [15] { value: 3 } - } - value: { - IDENT [16] { - name: @index0 + loop_step: { + CALL [22] { + function: _||_ + args: { + IDENT [23] { + name: @ac:0:0 + } + CALL [24] { + function: _>_ + args: { + CALL [25] { + function: _-_ + args: { + IDENT [26] { + name: @it:0:0 + } + CONSTANT [27] { value: 1 } + } + } + CONSTANT [28] { value: 3 } + } + } } } } + result: { + IDENT [29] { + name: @ac:0:0 + } + } } - } - } - CALL [17] { - function: @in - args: { - CONSTANT [18] { value: 2 } - IDENT [19] { - name: @index1 + IDENT [30] { + name: @index0 } } } } } -Test case: MACRO_ITER_VAR_NOT_REFERENCED -Source: [1,2].map(i, [1, 2].map(i, [3,4])) == [[[3, 4], [3, 4]], [[3, 4], [3, 4]]] +Test case: MACRO_SHADOWED_VARIABLE_2 +Source: ["foo", "bar"].map(x, [x + x, x + x]).map(x, [x + x, x + x]) =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - CONSTANT [5] { value: 2 } - } - } - CREATE_LIST [6] { - elements: { - CONSTANT [7] { value: 3 } - CONSTANT [8] { value: 4 } - } - } - CREATE_LIST [9] { - elements: { - IDENT [10] { - name: @index1 - } - IDENT [11] { - name: @index1 - } - } - } - CREATE_LIST [12] { + LIST [3] { elements: { - IDENT [13] { - name: @index2 - } - IDENT [14] { - name: @index2 - } + CONSTANT [4] { value: "foo" } + CONSTANT [5] { value: "bar" } } } - COMPREHENSION [15] { - iter_var: @c1:0 + } + } + COMPREHENSION [6] { + iter_var: @it:1:0 + iter_range: { + COMPREHENSION [7] { + iter_var: @it:0:0 iter_range: { - IDENT [16] { + IDENT [8] { name: @index0 } } - accu_var: @x1:0 + accu_var: @ac:0:0 accu_init: { - CREATE_LIST [17] { + LIST [9] { elements: { } } } loop_condition: { - CONSTANT [18] { value: true } + CONSTANT [10] { value: true } } loop_step: { - CALL [19] { + CALL [11] { function: _+_ args: { - IDENT [20] { - name: @x1:0 + IDENT [12] { + name: @ac:0:0 } - CREATE_LIST [21] { + LIST [13] { elements: { - IDENT [22] { - name: @index1 + LIST [14] { + elements: { + CALL [15] { + function: _+_ + args: { + IDENT [16] { + name: @it:0:0 + } + IDENT [17] { + name: @it:0:0 + } + } + } + CALL [18] { + function: _+_ + args: { + IDENT [19] { + name: @it:0:0 + } + IDENT [20] { + name: @it:0:0 + } + } + } + } } } } @@ -2188,312 +4104,346 @@ CALL [1] { } } result: { - IDENT [23] { - name: @x1:0 + IDENT [21] { + name: @ac:0:0 } } } - COMPREHENSION [24] { - iter_var: @c0:0 - iter_range: { + } + accu_var: @ac:1:0 + accu_init: { + LIST [22] { + elements: { + } + } + } + loop_condition: { + CONSTANT [23] { value: true } + } + loop_step: { + CALL [24] { + function: _+_ + args: { IDENT [25] { - name: @index0 + name: @ac:1:0 } - } - accu_var: @x0:0 - accu_init: { - CREATE_LIST [26] { + LIST [26] { elements: { - } - } - } - loop_condition: { - CONSTANT [27] { value: true } - } - loop_step: { - CALL [28] { - function: _+_ - args: { - IDENT [29] { - name: @x0:0 - } - CREATE_LIST [30] { + LIST [27] { elements: { - IDENT [31] { - name: @index4 + CALL [28] { + function: _+_ + args: { + IDENT [29] { + name: @it:1:0 + } + IDENT [30] { + name: @it:1:0 + } + } + } + CALL [31] { + function: _+_ + args: { + IDENT [32] { + name: @it:1:0 + } + IDENT [33] { + name: @it:1:0 + } + } } } } } } } - result: { - IDENT [32] { - name: @x0:0 - } - } } } - } - CALL [33] { - function: _==_ - args: { + result: { IDENT [34] { - name: @index5 - } - IDENT [35] { - name: @index3 + name: @ac:1:0 } } } } } -Test case: MACRO_SHADOWED_VARIABLE -Source: [x - 1 > 3 ? x - 1 : 5].exists(x, x - 1 > 3) || x - 1 > 3 +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 =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { - function: _-_ - args: { - IDENT [4] { - name: x - } - CONSTANT [5] { value: 1 } - } - } - CALL [6] { function: _>_ args: { - IDENT [7] { - name: @index0 - } - CONSTANT [8] { value: 3 } - } - } - CALL [9] { - function: _||_ - args: { - IDENT [10] { - name: @x0:0 - } - CALL [11] { - function: _>_ + CALL [4] { + function: _-_ args: { - CALL [12] { + CALL [5] { function: _-_ args: { - IDENT [13] { - name: @c0:0 + IDENT [6] { + name: x + } + IDENT [7] { + name: y } - CONSTANT [14] { value: 1 } } } - CONSTANT [15] { value: 3 } + CONSTANT [8] { value: 1 } } } + CONSTANT [9] { value: 3 } } } - COMPREHENSION [16] { - iter_var: @c0:0 - iter_range: { - CREATE_LIST [17] { - elements: { - CALL [18] { - function: _?_:_ + CALL [10] { + function: _?_:_ + args: { + IDENT [11] { + name: @index0 + } + CALL [12] { + function: _-_ + args: { + CALL [13] { + function: _-_ args: { - IDENT [19] { - name: @index1 + IDENT [14] { + name: x } - IDENT [20] { - name: @index0 + IDENT [15] { + name: y } - CONSTANT [21] { value: 5 } } } + CONSTANT [16] { value: 1 } } } + CONSTANT [17] { value: 5 } + } + } + LIST [18] { + elements: { + IDENT [19] { + name: @index1 + } + } + } + } + } + CALL [20] { + function: _||_ + args: { + COMPREHENSION [21] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + IDENT [22] { + name: @index2 + } } - accu_var: @x0:0 + accu_var: @ac:0:0 accu_init: { - CONSTANT [22] { value: false } + CONSTANT [23] { value: false } } loop_condition: { - CALL [23] { + CALL [24] { function: @not_strictly_false args: { - CALL [24] { + CALL [25] { function: !_ args: { - IDENT [25] { - name: @x0:0 + IDENT [26] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [27] { + function: _||_ + args: { + IDENT [28] { + name: @ac:0:0 + } + CALL [29] { + function: _>_ + args: { + CALL [30] { + function: _-_ + args: { + CALL [31] { + function: _-_ + args: { + IDENT [32] { + name: @it:0:0 + } + IDENT [33] { + name: @it2:0:0 + } + } + } + CONSTANT [34] { value: 1 } + } } + CONSTANT [35] { value: 3 } } } } } } - loop_step: { - IDENT [26] { - name: @index2 - } - } result: { - IDENT [27] { - name: @x0:0 + IDENT [36] { + name: @ac:0:0 } } } - } - } - CALL [28] { - function: _||_ - args: { - IDENT [29] { - name: @index3 - } - IDENT [30] { - name: @index1 + IDENT [37] { + name: @index0 } } } } } -Test case: MACRO_SHADOWED_VARIABLE_2 -Source: ["foo", "bar"].map(x, [x + x, x + x]).map(x, [x + x, x + x]) +Test case: MACRO_SHADOWED_VARIABLE_COMP_V2_2 +Source: ["foo", "bar"].transformMap(x, y, [x + x, y + y]).transformMap(x, y, [x + x, y + y]) =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CALL [3] { - function: _+_ - args: { - IDENT [4] { - name: @c1:0 - } - IDENT [5] { - name: @c1:0 - } + LIST [3] { + elements: { + CONSTANT [4] { value: "foo" } + CONSTANT [5] { value: "bar" } } } - CALL [6] { - function: _+_ - args: { - IDENT [7] { - name: @c0:0 - } + } + } + COMPREHENSION [6] { + iter_var: @it:1:0 + iter_var2: @it2:1:0 + iter_range: { + COMPREHENSION [7] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { IDENT [8] { - name: @c0:0 + name: @index0 } } - } - CALL [9] { - function: _+_ - args: { - IDENT [10] { - name: @x0:0 - } - CREATE_LIST [11] { - elements: { - CREATE_LIST [12] { - elements: { - IDENT [13] { - name: @index1 - } - IDENT [14] { - name: @index1 - } - } - } - } + accu_var: @ac:0:0 + accu_init: { + MAP [9] { + } } - } - CALL [15] { - function: _+_ - args: { - IDENT [16] { - name: @x1:0 - } - CREATE_LIST [17] { - elements: { - CREATE_LIST [18] { + loop_condition: { + CONSTANT [10] { value: true } + } + loop_step: { + CALL [11] { + function: cel.@mapInsert + args: { + IDENT [12] { + name: @ac:0:0 + } + IDENT [13] { + name: @it:0:0 + } + LIST [14] { elements: { - IDENT [19] { - name: @index0 + CALL [15] { + function: _+_ + args: { + IDENT [16] { + name: @it:0:0 + } + IDENT [17] { + name: @it:0:0 + } + } } - IDENT [20] { - name: @index0 + CALL [18] { + function: _+_ + args: { + IDENT [19] { + name: @it2:0:0 + } + IDENT [20] { + name: @it2:0:0 + } + } } } } } } } - } - COMPREHENSION [21] { - iter_var: @c1:0 - iter_range: { - CREATE_LIST [22] { - elements: { - CONSTANT [23] { value: "foo" } - CONSTANT [24] { value: "bar" } - } - } - } - accu_var: @x1:0 - accu_init: { - CREATE_LIST [25] { - elements: { - } - } - } - loop_condition: { - CONSTANT [26] { value: true } - } - loop_step: { - IDENT [27] { - name: @index3 - } - } result: { - IDENT [28] { - name: @x1:0 + IDENT [21] { + name: @ac:0:0 } } } } - } - COMPREHENSION [29] { - iter_var: @c0:0 - iter_range: { - IDENT [30] { - name: @index4 - } - } - accu_var: @x0:0 + accu_var: @ac:1:0 accu_init: { - CREATE_LIST [31] { - elements: { - } + MAP [22] { + } } loop_condition: { - CONSTANT [32] { value: true } + CONSTANT [23] { value: true } } loop_step: { - IDENT [33] { - name: @index2 + CALL [24] { + function: cel.@mapInsert + args: { + IDENT [25] { + name: @ac:1:0 + } + IDENT [26] { + name: @it:1:0 + } + LIST [27] { + elements: { + CALL [28] { + function: _+_ + args: { + IDENT [29] { + name: @it:1:0 + } + IDENT [30] { + name: @it:1:0 + } + } + } + CALL [31] { + function: _+_ + args: { + IDENT [32] { + name: @it2:1:0 + } + IDENT [33] { + name: @it2:1:0 + } + } + } + } + } + } } } result: { IDENT [34] { - name: @x0:0 + name: @ac:1:0 } } } @@ -2505,9 +4455,9 @@ Source: has({'a': true}.a) && {'a':true}['a'] CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_MAP [3] { + MAP [3] { MAP_ENTRY [4] { key: { CONSTANT [5] { value: "a" } @@ -2517,27 +4467,59 @@ CALL [1] { } } } - CALL [7] { + } + } + CALL [7] { + function: _&&_ + args: { + SELECT [8] { + IDENT [9] { + name: @index0 + }.a~presence_test + } + CALL [10] { function: _[_] args: { - IDENT [8] { + IDENT [11] { name: @index0 } - CONSTANT [9] { value: "a" } + CONSTANT [12] { value: "a" } } } } } - CALL [10] { + } +} +Test case: PRESENCE_TEST_2 +Source: has({'a': true}.a) && has({'a': true}.a) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + SELECT [3] { + MAP [4] { + MAP_ENTRY [5] { + key: { + CONSTANT [6] { value: "a" } + } + value: { + CONSTANT [7] { value: true } + } + } + }.a~presence_test + } + } + } + CALL [8] { function: _&&_ args: { - SELECT [11] { - IDENT [12] { - name: @index0 - }.a~presence_test + IDENT [9] { + name: @index0 } - IDENT [13] { - name: @index1 + IDENT [10] { + name: @index0 } } } @@ -2549,7 +4531,7 @@ Source: (has(msg.oneof_type.payload) ? msg.oneof_type.payload.single_int64 : 0) CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { @@ -2593,54 +4575,50 @@ Source: (has(msg.oneof_type.payload) ? msg.oneof_type.payload.single_int64 : msg CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload }.single_int64 } - CALL [9] { + CALL [7] { function: _?_:_ args: { - SELECT [10] { - IDENT [11] { - name: @index0 + SELECT [8] { + SELECT [9] { + IDENT [10] { + name: msg + }.oneof_type }.payload~presence_test } - IDENT [12] { - name: @index2 + IDENT [11] { + name: @index0 } - CALL [13] { + CALL [12] { function: _*_ args: { - IDENT [14] { - name: @index2 + IDENT [13] { + name: @index0 } - CONSTANT [15] { value: 0 } + CONSTANT [14] { value: 0 } } } } } } } - CALL [16] { + CALL [15] { function: _==_ args: { - IDENT [17] { - name: @index3 + IDENT [16] { + name: @index1 } - CONSTANT [18] { value: 10 } + CONSTANT [17] { value: 10 } } } } @@ -2651,53 +4629,51 @@ Source: (has(msg.oneof_type.payload.single_int64) ? msg.oneof_type.payload.singl CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.single_int64 } SELECT [7] { - IDENT [8] { - name: @index1 - }.single_int64 + SELECT [8] { + SELECT [9] { + IDENT [10] { + name: msg + }.oneof_type + }.payload + }.single_int64~presence_test } - CALL [9] { + } + } + CALL [11] { + function: _==_ + args: { + CALL [12] { function: _?_:_ args: { - SELECT [10] { - IDENT [11] { - name: @index1 - }.single_int64~presence_test + IDENT [13] { + name: @index1 } - IDENT [12] { - name: @index2 + IDENT [14] { + name: @index0 } - CALL [13] { + CALL [15] { function: _*_ args: { - IDENT [14] { - name: @index2 + IDENT [16] { + name: @index0 } - CONSTANT [15] { value: 0 } + CONSTANT [17] { value: 0 } } } } } - } - } - CALL [16] { - function: _==_ - args: { - IDENT [17] { - name: @index3 - } CONSTANT [18] { value: 10 } } } @@ -2709,92 +4685,95 @@ Source: (has(msg.oneof_type) && has(msg.oneof_type.payload) && has(msg.oneof_typ CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.map_string_string } - SELECT [5] { - IDENT [6] { - name: @index0 + SELECT [7] { + SELECT [8] { + IDENT [9] { + name: msg + }.oneof_type }.payload } - SELECT [7] { - IDENT [8] { - name: @index1 - }.map_string_string + CALL [10] { + function: _&&_ + args: { + SELECT [11] { + IDENT [12] { + name: msg + }.oneof_type~presence_test + } + SELECT [13] { + SELECT [14] { + IDENT [15] { + name: msg + }.oneof_type + }.payload~presence_test + } + } } - CALL [9] { + CALL [16] { function: _?_:_ args: { - CALL [10] { + CALL [17] { function: _&&_ args: { - SELECT [11] { - IDENT [12] { + SELECT [18] { + IDENT [19] { name: @index1 }.map_string_string~presence_test } - SELECT [13] { - IDENT [14] { - name: @index2 + SELECT [20] { + IDENT [21] { + name: @index0 }.key~presence_test } } } - CALL [15] { + CALL [22] { function: _==_ args: { - SELECT [16] { - IDENT [17] { - name: @index2 + SELECT [23] { + IDENT [24] { + name: @index0 }.key } - CONSTANT [18] { value: "A" } + CONSTANT [25] { value: "A" } } } - CONSTANT [19] { value: false } + CONSTANT [26] { value: false } } } - CALL [20] { + } + } + CALL [27] { + function: _?_:_ + args: { + CALL [28] { function: _&&_ args: { - CALL [21] { - function: _&&_ - args: { - SELECT [22] { - IDENT [23] { - name: msg - }.oneof_type~presence_test - } - SELECT [24] { - IDENT [25] { - name: @index0 - }.payload~presence_test - } - } + IDENT [29] { + name: @index2 } - SELECT [26] { - IDENT [27] { + SELECT [30] { + IDENT [31] { name: @index1 }.single_int64~presence_test } } } - } - } - CALL [28] { - function: _?_:_ - args: { - IDENT [29] { - name: @index4 - } - IDENT [30] { + IDENT [32] { name: @index3 } - CONSTANT [31] { value: false } + CONSTANT [33] { value: false } } } } @@ -2805,46 +4784,51 @@ Source: [10, ?optional.none(), [?optional.none(), ?opt_x], [?optional.none(), ?o CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CALL [3] { - function: optional.none - args: { - } - } - CREATE_LIST [4] { + LIST [3] { elements: { - IDENT [5] { - name: @index0 + CALL [4] { + function: optional.none + args: { + } } - IDENT [6] { + IDENT [5] { name: opt_x } } optional_indices: [0, 1] } - CREATE_LIST [7] { + LIST [6] { elements: { - CONSTANT [8] { value: 5 } + CONSTANT [7] { value: 5 } } } - CREATE_LIST [9] { + } + } + CALL [8] { + function: _==_ + args: { + LIST [9] { elements: { CONSTANT [10] { value: 10 } - IDENT [11] { - name: @index2 + CALL [11] { + function: optional.none + args: { + } } IDENT [12] { - name: @index2 + name: @index0 + } + IDENT [13] { + name: @index0 } } + optional_indices: [0] } - CREATE_LIST [13] { + LIST [14] { elements: { - CONSTANT [14] { value: 10 } - IDENT [15] { - name: @index0 - } + CONSTANT [15] { value: 10 } IDENT [16] { name: @index1 } @@ -2852,18 +4836,6 @@ CALL [1] { name: @index1 } } - optional_indices: [0] - } - } - } - CALL [18] { - function: _==_ - args: { - IDENT [19] { - name: @index4 - } - IDENT [20] { - name: @index3 } } } @@ -2875,56 +4847,47 @@ Source: {?'hello': optional.of('hello')}['hello'] + {?'hello': optional.of('hell CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { - function: optional.of - args: { - CONSTANT [4] { value: "hello" } - } - } - CREATE_MAP [5] { - MAP_ENTRY [6] { - key: { - CONSTANT [7] { value: "hello" } - } - optional_entry: true - value: { - IDENT [8] { - name: @index0 - } - } - } - } - CALL [9] { function: _[_] args: { - IDENT [10] { - name: @index1 + MAP [4] { + MAP_ENTRY [5] { + key: { + CONSTANT [6] { value: "hello" } + } + optional_entry: true + value: { + CALL [7] { + function: optional.of + args: { + CONSTANT [8] { value: "hello" } + } + } + } + } } - CONSTANT [11] { value: "hello" } + CONSTANT [9] { value: "hello" } } } - CALL [12] { + } + } + CALL [10] { + function: _==_ + args: { + CALL [11] { function: _+_ args: { - IDENT [13] { - name: @index2 + IDENT [12] { + name: @index0 } - IDENT [14] { - name: @index2 + IDENT [13] { + name: @index0 } } } - } - } - CALL [15] { - function: _==_ - args: { - IDENT [16] { - name: @index3 - } - CONSTANT [17] { value: "hellohello" } + CONSTANT [14] { value: "hellohello" } } } } @@ -2935,9 +4898,9 @@ Source: {?'key': optional.of('test')}[?'bogus'].or({'key': 'test'}[?'bogus']).or CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_MAP [3] { + MAP [3] { MAP_ENTRY [4] { key: { CONSTANT [5] { value: "key" } @@ -2948,77 +4911,71 @@ CALL [1] { } } CALL [7] { - function: _[_] - args: { - IDENT [8] { - name: @index0 - } - CONSTANT [9] { value: "key" } - } - } - CALL [10] { - function: _[?_] - args: { - IDENT [11] { - name: @index0 - } - CONSTANT [12] { value: "bogus" } - } - } - CALL [13] { function: _[?_] args: { - CREATE_MAP [14] { - MAP_ENTRY [15] { + MAP [8] { + MAP_ENTRY [9] { key: { - CONSTANT [16] { value: "key" } + CONSTANT [10] { value: "key" } } optional_entry: true value: { - CALL [17] { + CALL [11] { function: optional.of args: { - CONSTANT [18] { value: "test" } + CONSTANT [12] { value: "test" } } } } } } - CONSTANT [19] { value: "bogus" } + CONSTANT [13] { value: "bogus" } } } - CALL [20] { + CALL [14] { function: orValue target: { - CALL [21] { + CALL [15] { function: or target: { - IDENT [22] { - name: @index3 + IDENT [16] { + name: @index1 } } args: { - IDENT [23] { - name: @index2 + CALL [17] { + function: _[?_] + args: { + IDENT [18] { + name: @index0 + } + CONSTANT [19] { value: "bogus" } + } } } } } args: { - IDENT [24] { - name: @index1 + CALL [20] { + function: _[_] + args: { + IDENT [21] { + name: @index0 + } + CONSTANT [22] { value: "key" } + } } } } } } - CALL [25] { + CALL [23] { function: _==_ args: { - IDENT [26] { - name: @index4 + IDENT [24] { + name: @index2 } - CONSTANT [27] { value: "test" } + CONSTANT [25] { value: "test" } } } } @@ -3029,67 +4986,58 @@ Source: TestAllTypes{?single_int64: optional.ofNonZeroValue(1), ?single_int32: o CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CALL [3] { - function: optional.ofNonZeroValue - args: { - CONSTANT [4] { value: 1 } - } - } - CALL [5] { - function: optional.of - args: { - CONSTANT [6] { value: 4 } - } - } - CREATE_STRUCT [7] { - name: TestAllTypes + STRUCT [3] { + name: cel.expr.conformance.proto3.TestAllTypes entries: { - ENTRY [8] { + ENTRY [4] { field_key: single_int64 optional_entry: true value: { - IDENT [9] { - name: @index0 + CALL [5] { + function: optional.ofNonZeroValue + args: { + CONSTANT [6] { value: 1 } + } } } } - ENTRY [10] { + ENTRY [7] { field_key: single_int32 optional_entry: true value: { - IDENT [11] { - name: @index1 + CALL [8] { + function: optional.of + args: { + CONSTANT [9] { value: 4 } + } } } } } } - CALL [12] { + } + } + CALL [10] { + function: _==_ + args: { + CALL [11] { function: _+_ args: { - SELECT [13] { - IDENT [14] { - name: @index2 + SELECT [12] { + IDENT [13] { + name: @index0 }.single_int32 } - SELECT [15] { - IDENT [16] { - name: @index2 + SELECT [14] { + IDENT [15] { + name: @index0 }.single_int64 } } } - } - } - CALL [17] { - function: _==_ - args: { - IDENT [18] { - name: @index3 - } - CONSTANT [19] { value: 5 } + CONSTANT [16] { value: 5 } } } } @@ -3098,65 +5046,56 @@ Test case: CALL Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('h' + 'e' + 'l' + 'l' + 'o') =====> CALL [1] { - function: cel.@block - args: { - CREATE_LIST [2] { - elements: { - CALL [3] { - function: _+_ - args: { - CONSTANT [4] { value: "h" } - CONSTANT [5] { value: "e" } - } - } - CALL [6] { - function: _+_ - args: { - IDENT [7] { - name: @index0 - } - CONSTANT [8] { value: "l" } - } - } - CALL [9] { - function: _+_ - args: { - IDENT [10] { - name: @index1 - } - CONSTANT [11] { value: "l" } - } - } - CALL [12] { + function: cel.@block + args: { + LIST [2] { + elements: { + CALL [3] { function: _+_ args: { - IDENT [13] { - name: @index2 + CALL [4] { + function: _+_ + args: { + CALL [5] { + function: _+_ + args: { + CONSTANT [6] { value: "h" } + CONSTANT [7] { value: "e" } + } + } + CONSTANT [8] { value: "l" } + } } - CONSTANT [14] { value: "o" } + CONSTANT [9] { value: "l" } } } - CALL [15] { + CALL [10] { function: _+_ args: { - IDENT [16] { - name: @index3 + IDENT [11] { + name: @index0 } - CONSTANT [17] { value: " world" } + CONSTANT [12] { value: "o" } } } } } - CALL [18] { + CALL [13] { function: matches target: { - IDENT [19] { - name: @index4 + CALL [14] { + function: _+_ + args: { + IDENT [15] { + name: @index1 + } + CONSTANT [16] { value: " world" } + } } } args: { - IDENT [20] { - name: @index3 + IDENT [17] { + name: @index1 } } } @@ -3168,7 +5107,7 @@ Source: 'hello world'.matches('h' + 'e' + 'l' + 'l' + 'o') CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { function: _+_ @@ -3189,25 +5128,22 @@ CALL [1] { CONSTANT [9] { value: "l" } } } - CALL [10] { - function: _+_ - args: { - IDENT [11] { - name: @index0 - } - CONSTANT [12] { value: "o" } - } - } } } - CALL [13] { + CALL [10] { function: matches target: { - CONSTANT [14] { value: "hello world" } + CONSTANT [11] { value: "hello world" } } args: { - IDENT [15] { - name: @index1 + CALL [12] { + function: _+_ + args: { + IDENT [13] { + name: @index0 + } + CONSTANT [14] { value: "o" } + } } } } @@ -3219,7 +5155,7 @@ Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('hello') CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { function: _+_ @@ -3240,32 +5176,29 @@ CALL [1] { CONSTANT [9] { value: "l" } } } - CALL [10] { + } + } + CALL [10] { + function: matches + target: { + CALL [11] { function: _+_ args: { - CALL [11] { + CALL [12] { function: _+_ args: { - IDENT [12] { + IDENT [13] { name: @index0 } - CONSTANT [13] { value: "o" } + CONSTANT [14] { value: "o" } } } - CONSTANT [14] { value: " world" } + CONSTANT [15] { value: " world" } } } } - } - CALL [15] { - function: matches - target: { - IDENT [16] { - name: @index1 - } - } args: { - CONSTANT [17] { value: "hello" } + CONSTANT [16] { value: "hello" } } } } @@ -3276,7 +5209,7 @@ Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('w' + 'o' + 'r' + 'l' + CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { function: _+_ @@ -3287,11 +5220,11 @@ CALL [1] { CALL [5] { function: _+_ args: { - CONSTANT [6] { value: "w" } - CONSTANT [7] { value: "o" } + CONSTANT [6] { value: "h" } + CONSTANT [7] { value: "e" } } } - CONSTANT [8] { value: "r" } + CONSTANT [8] { value: "l" } } } CONSTANT [9] { value: "l" } @@ -3300,58 +5233,52 @@ CALL [1] { CALL [10] { function: _+_ args: { - IDENT [11] { - name: @index0 - } - CONSTANT [12] { value: "d" } - } - } - CALL [13] { - function: _+_ - args: { - CALL [14] { + CALL [11] { function: _+_ args: { - CALL [15] { + CALL [12] { function: _+_ args: { - CONSTANT [16] { value: "h" } - CONSTANT [17] { value: "e" } + CONSTANT [13] { value: "w" } + CONSTANT [14] { value: "o" } } } - CONSTANT [18] { value: "l" } + CONSTANT [15] { value: "r" } } } - CONSTANT [19] { value: "l" } + CONSTANT [16] { value: "l" } } } - CALL [20] { + } + } + CALL [17] { + function: matches + target: { + CALL [18] { function: _+_ args: { - CALL [21] { + CALL [19] { function: _+_ args: { - IDENT [22] { - name: @index2 + IDENT [20] { + name: @index0 } - CONSTANT [23] { value: "o" } + CONSTANT [21] { value: "o" } } } - CONSTANT [24] { value: " world" } + CONSTANT [22] { value: " world" } } } } - } - CALL [25] { - function: matches - target: { - IDENT [26] { - name: @index3 - } - } args: { - IDENT [27] { - name: @index1 + CALL [23] { + function: _+_ + args: { + IDENT [24] { + name: @index1 + } + CONSTANT [25] { value: "d" } + } } } } @@ -3363,77 +5290,69 @@ Source: non_pure_custom_func(msg.oneof_type.payload.single_int64) + non_pure_cus CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 - }.single_int64 - } - SELECT [9] { - IDENT [10] { - name: msg + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload }.single_int64 } - SELECT [11] { - IDENT [12] { - name: @index1 - }.single_int32 - } } } - CALL [13] { + CALL [7] { function: _+_ args: { - CALL [14] { + CALL [8] { function: _+_ args: { - CALL [15] { + CALL [9] { function: _+_ args: { - CALL [16] { + CALL [10] { function: non_pure_custom_func args: { - IDENT [17] { - name: @index2 + IDENT [11] { + name: @index0 } } } - CALL [18] { + CALL [12] { function: non_pure_custom_func args: { - IDENT [19] { - name: @index4 + SELECT [13] { + SELECT [14] { + SELECT [15] { + IDENT [16] { + name: msg + }.oneof_type + }.payload + }.single_int32 } } } } } - CALL [20] { + CALL [17] { function: non_pure_custom_func args: { - IDENT [21] { - name: @index2 + IDENT [18] { + name: @index0 } } } } } - CALL [22] { + CALL [19] { function: non_pure_custom_func args: { - IDENT [23] { - name: @index3 + SELECT [20] { + IDENT [21] { + name: msg + }.single_int64 } } } @@ -3447,82 +5366,77 @@ Source: pure_custom_func(msg.oneof_type.payload.single_int64) + pure_custom_func CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload }.single_int64 } - CALL [9] { + CALL [7] { function: pure_custom_func args: { - IDENT [10] { - name: @index2 + IDENT [8] { + name: @index0 } } } - CALL [11] { - function: pure_custom_func - args: { - SELECT [12] { - IDENT [13] { + SELECT [9] { + SELECT [10] { + SELECT [11] { + IDENT [12] { name: msg - }.single_int64 - } - } + }.oneof_type + }.payload + }.single_int32 } - CALL [14] { + CALL [13] { function: _+_ args: { - IDENT [15] { - name: @index3 - } - CALL [16] { - function: pure_custom_func + CALL [14] { + function: _+_ args: { - SELECT [17] { - IDENT [18] { - name: @index1 - }.single_int32 + IDENT [15] { + name: @index1 + } + CALL [16] { + function: pure_custom_func + args: { + IDENT [17] { + name: @index2 + } + } } } } - } - } - CALL [19] { - function: _+_ - args: { - IDENT [20] { - name: @index5 - } - IDENT [21] { - name: @index3 + IDENT [18] { + name: @index1 } } } } } - CALL [22] { + CALL [19] { function: _+_ args: { - IDENT [23] { - name: @index6 + IDENT [20] { + name: @index3 } - IDENT [24] { - name: @index4 + CALL [21] { + function: pure_custom_func + args: { + SELECT [22] { + IDENT [23] { + name: msg + }.single_int64 + } + } } } } } -} +} \ No newline at end of file diff --git a/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_4.baseline b/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_4.baseline index 71a440692..fe139eea1 100644 --- a/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_4.baseline +++ b/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_4.baseline @@ -4,22 +4,24 @@ Source: size([1,2]) + size([1,2]) + 1 == 5 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - CONSTANT [5] { value: 2 } - } - } - CALL [6] { + CALL [3] { function: size args: { - IDENT [7] { - name: @index0 + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + CONSTANT [6] { value: 2 } + } } } } + } + } + CALL [7] { + function: _==_ + args: { CALL [8] { function: _+_ args: { @@ -27,25 +29,17 @@ CALL [1] { function: _+_ args: { IDENT [10] { - name: @index1 + name: @index0 } IDENT [11] { - name: @index1 + name: @index0 } } } CONSTANT [12] { value: 1 } } } - } - } - CALL [13] { - function: _==_ - args: { - IDENT [14] { - name: @index2 - } - CONSTANT [15] { value: 5 } + CONSTANT [13] { value: 5 } } } } @@ -56,22 +50,24 @@ Source: 2 + size([1,2]) + size([1,2]) + 1 == 7 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - CONSTANT [5] { value: 2 } - } - } - CALL [6] { + CALL [3] { function: size args: { - IDENT [7] { - name: @index0 + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + CONSTANT [6] { value: 2 } + } } } } + } + } + CALL [7] { + function: _==_ + args: { CALL [8] { function: _+_ args: { @@ -83,27 +79,19 @@ CALL [1] { args: { CONSTANT [11] { value: 2 } IDENT [12] { - name: @index1 + name: @index0 } } } IDENT [13] { - name: @index1 + name: @index0 } } } CONSTANT [14] { value: 1 } } } - } - } - CALL [15] { - function: _==_ - args: { - IDENT [16] { - name: @index2 - } - CONSTANT [17] { value: 7 } + CONSTANT [15] { value: 7 } } } } @@ -114,71 +102,62 @@ Source: size([0]) + size([0]) + size([1,2]) + size([1,2]) == 6 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 0 } - } - } - CALL [5] { + CALL [3] { function: size args: { - IDENT [6] { - name: @index0 + LIST [4] { + elements: { + CONSTANT [5] { value: 0 } + } } } } - CREATE_LIST [7] { - elements: { - CONSTANT [8] { value: 1 } - CONSTANT [9] { value: 2 } - } - } - CALL [10] { + CALL [6] { function: size args: { - IDENT [11] { - name: @index2 + LIST [7] { + elements: { + CONSTANT [8] { value: 1 } + CONSTANT [9] { value: 2 } + } } } } - CALL [12] { + } + } + CALL [10] { + function: _==_ + args: { + CALL [11] { function: _+_ args: { - CALL [13] { + CALL [12] { function: _+_ args: { - CALL [14] { + CALL [13] { function: _+_ args: { - IDENT [15] { - name: @index1 + IDENT [14] { + name: @index0 } - IDENT [16] { - name: @index1 + IDENT [15] { + name: @index0 } } } - IDENT [17] { - name: @index3 + IDENT [16] { + name: @index1 } } } - IDENT [18] { - name: @index3 + IDENT [17] { + name: @index1 } } } - } - } - CALL [19] { - function: _==_ - args: { - IDENT [20] { - name: @index4 - } - CONSTANT [21] { value: 6 } + CONSTANT [18] { value: 6 } } } } @@ -189,111 +168,99 @@ Source: 5 + size([0]) + size([0]) + size([1,2]) + size([1,2]) + size([1,2,3]) + CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 0 } - } - } - CALL [5] { + CALL [3] { function: size args: { - IDENT [6] { - name: @index0 + LIST [4] { + elements: { + CONSTANT [5] { value: 0 } + } } } } - CREATE_LIST [7] { - elements: { - CONSTANT [8] { value: 1 } - CONSTANT [9] { value: 2 } - } - } - CALL [10] { + CALL [6] { function: size args: { - IDENT [11] { - name: @index2 + LIST [7] { + elements: { + CONSTANT [8] { value: 1 } + CONSTANT [9] { value: 2 } + } } } } - CREATE_LIST [12] { - elements: { - CONSTANT [13] { value: 1 } - CONSTANT [14] { value: 2 } - CONSTANT [15] { value: 3 } - } - } - CALL [16] { + CALL [10] { function: size args: { - IDENT [17] { - name: @index4 + LIST [11] { + elements: { + CONSTANT [12] { value: 1 } + CONSTANT [13] { value: 2 } + CONSTANT [14] { value: 3 } + } } } } - CALL [18] { + CALL [15] { function: _+_ args: { - CALL [19] { + CALL [16] { function: _+_ args: { - CALL [20] { + CALL [17] { function: _+_ args: { - CALL [21] { + CALL [18] { function: _+_ args: { - CONSTANT [22] { value: 5 } - IDENT [23] { - name: @index1 + CONSTANT [19] { value: 5 } + IDENT [20] { + name: @index0 } } } - IDENT [24] { - name: @index1 + IDENT [21] { + name: @index0 } } } - IDENT [25] { - name: @index3 + IDENT [22] { + name: @index1 } } } - IDENT [26] { - name: @index3 + IDENT [23] { + name: @index1 } } } - CALL [27] { + } + } + CALL [24] { + function: _==_ + args: { + CALL [25] { function: _+_ args: { - CALL [28] { + CALL [26] { function: _+_ args: { - IDENT [29] { - name: @index6 + IDENT [27] { + name: @index3 } - IDENT [30] { - name: @index5 + IDENT [28] { + name: @index2 } } } - IDENT [31] { - name: @index5 + IDENT [29] { + name: @index2 } } } - } - } - CALL [32] { - function: _==_ - args: { - IDENT [33] { - name: @index7 - } - CONSTANT [34] { value: 17 } + CONSTANT [30] { value: 17 } } } } @@ -304,153 +271,103 @@ Source: timestamp(int(timestamp(1000000000))).getFullYear() + timestamp(int(time CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { - function: timestamp - args: { - CONSTANT [4] { value: 1000000000 } - } - } - CALL [5] { - function: int - args: { - IDENT [6] { - name: @index0 + function: getFullYear + target: { + CALL [4] { + function: timestamp + args: { + CALL [5] { + function: int + args: { + CALL [6] { + function: timestamp + args: { + CONSTANT [7] { value: 1000000000 } + } + } + } + } + } } } - } - CALL [7] { - function: timestamp args: { - IDENT [8] { - name: @index1 - } } } - CALL [9] { + CALL [8] { function: getFullYear target: { - IDENT [10] { - name: @index2 + CALL [9] { + function: timestamp + args: { + CALL [10] { + function: int + args: { + CALL [11] { + function: timestamp + args: { + CONSTANT [12] { value: 200 } + } + } + } + } + } } } args: { } } - CALL [11] { - function: timestamp - args: { - CONSTANT [12] { value: 50 } - } - } CALL [13] { - function: int - args: { - IDENT [14] { - name: @index4 - } - } - } - CALL [15] { function: timestamp args: { - IDENT [16] { - name: @index5 + CALL [14] { + function: int + args: { + CALL [15] { + function: timestamp + args: { + CONSTANT [16] { value: 50 } + } + } + } } } } CALL [17] { function: timestamp args: { - CONSTANT [18] { value: 200 } - } - } - CALL [19] { - function: int - args: { - IDENT [20] { - name: @index7 + CALL [18] { + function: int + args: { + CALL [19] { + function: timestamp + args: { + CONSTANT [20] { value: 75 } + } + } + } } } } CALL [21] { - function: timestamp - args: { - IDENT [22] { - name: @index8 - } - } - } - CALL [23] { - function: getFullYear - target: { - IDENT [24] { - name: @index9 - } - } - args: { - } - } - CALL [25] { - function: timestamp + function: _+_ args: { - CONSTANT [26] { value: 75 } - } - } - CALL [27] { - function: int - args: { - IDENT [28] { - name: @index11 - } - } - } - CALL [29] { - function: timestamp - args: { - IDENT [30] { - name: @index12 - } - } - } - CALL [31] { - function: getMinutes - target: { - IDENT [32] { - name: @index13 - } - } - args: { - } - } - CALL [33] { - function: getSeconds - target: { - IDENT [34] { - name: @index6 - } - } - args: { - } - } - CALL [35] { - function: _+_ - args: { - CALL [36] { + CALL [22] { function: _+_ args: { - CALL [37] { + CALL [23] { function: _+_ args: { - IDENT [38] { - name: @index3 + IDENT [24] { + name: @index0 } - CALL [39] { + CALL [25] { function: getFullYear target: { - IDENT [40] { - name: @index13 + IDENT [26] { + name: @index3 } } args: { @@ -458,11 +375,11 @@ CALL [1] { } } } - CALL [41] { + CALL [27] { function: getFullYear target: { - IDENT [42] { - name: @index6 + IDENT [28] { + name: @index2 } } args: { @@ -470,66 +387,77 @@ CALL [1] { } } } - IDENT [43] { - name: @index3 + IDENT [29] { + name: @index0 } } } - CALL [44] { + CALL [30] { function: _+_ args: { - CALL [45] { + CALL [31] { function: _+_ args: { - CALL [46] { + CALL [32] { function: _+_ args: { - CALL [47] { - function: _+_ - args: { - IDENT [48] { - name: @index16 - } - IDENT [49] { - name: @index15 + IDENT [33] { + name: @index4 + } + CALL [34] { + function: getSeconds + target: { + IDENT [35] { + name: @index2 } } - } - IDENT [50] { - name: @index10 + args: { + } } } } - IDENT [51] { - name: @index10 + IDENT [36] { + name: @index1 } } } - IDENT [52] { - name: @index14 + IDENT [37] { + name: @index1 } } } - CALL [53] { + } + } + CALL [38] { + function: _==_ + args: { + CALL [39] { function: _+_ args: { - IDENT [54] { - name: @index17 + CALL [40] { + function: _+_ + args: { + IDENT [41] { + name: @index5 + } + CALL [42] { + function: getMinutes + target: { + IDENT [43] { + name: @index3 + } + } + args: { + } + } + } } - IDENT [55] { - name: @index3 + IDENT [44] { + name: @index0 } } } - } - } - CALL [56] { - function: _==_ - args: { - IDENT [57] { - name: @index18 - } - CONSTANT [58] { value: 13934 } + CONSTANT [45] { value: 13934 } } } } @@ -540,55 +468,49 @@ Source: {"a": 2}["a"] + {"a": 2}["a"] * {"a": 2}["a"] == 6 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_MAP [3] { - MAP_ENTRY [4] { - key: { - CONSTANT [5] { value: "a" } - } - value: { - CONSTANT [6] { value: 2 } - } - } - } - CALL [7] { + CALL [3] { function: _[_] args: { - IDENT [8] { - name: @index0 + MAP [4] { + MAP_ENTRY [5] { + key: { + CONSTANT [6] { value: "a" } + } + value: { + CONSTANT [7] { value: 2 } + } + } } - CONSTANT [9] { value: "a" } + CONSTANT [8] { value: "a" } } } + } + } + CALL [9] { + function: _==_ + args: { CALL [10] { function: _+_ args: { IDENT [11] { - name: @index1 + name: @index0 } CALL [12] { function: _*_ args: { IDENT [13] { - name: @index1 + name: @index0 } IDENT [14] { - name: @index1 + name: @index0 } } } } } - } - } - CALL [15] { - function: _==_ - args: { - IDENT [16] { - name: @index2 - } - CONSTANT [17] { value: 6 } + CONSTANT [15] { value: 6 } } } } @@ -599,56 +521,53 @@ Source: {'a': {'b': 1}, 'c': {'b': 1}, 'd': {'e': {'b': 1}}, 'e': {'e': {'b': 1} CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_MAP [3] { + MAP [3] { MAP_ENTRY [4] { key: { - CONSTANT [5] { value: "b" } + CONSTANT [5] { value: "e" } } value: { - CONSTANT [6] { value: 1 } + MAP [6] { + MAP_ENTRY [7] { + key: { + CONSTANT [8] { value: "b" } + } + value: { + CONSTANT [9] { value: 1 } + } + } + } } } } - CREATE_MAP [7] { - MAP_ENTRY [8] { + MAP [10] { + MAP_ENTRY [11] { key: { - CONSTANT [9] { value: "e" } + CONSTANT [12] { value: "b" } } value: { - IDENT [10] { - name: @index0 - } + CONSTANT [13] { value: 1 } } } } } } - CREATE_MAP [11] { - MAP_ENTRY [12] { - key: { - CONSTANT [13] { value: "a" } - } - value: { - IDENT [14] { - name: @index0 - } - } - } + MAP [14] { MAP_ENTRY [15] { key: { - CONSTANT [16] { value: "c" } + CONSTANT [16] { value: "a" } } value: { IDENT [17] { - name: @index0 + name: @index1 } } } MAP_ENTRY [18] { key: { - CONSTANT [19] { value: "d" } + CONSTANT [19] { value: "c" } } value: { IDENT [20] { @@ -658,11 +577,21 @@ CALL [1] { } MAP_ENTRY [21] { key: { - CONSTANT [22] { value: "e" } + CONSTANT [22] { value: "d" } } value: { IDENT [23] { - name: @index1 + name: @index0 + } + } + } + MAP_ENTRY [24] { + key: { + CONSTANT [25] { value: "e" } + } + value: { + IDENT [26] { + name: @index0 } } } @@ -675,9 +604,9 @@ Source: [1, [1,2,3,4], 2, [1,2,3,4], 5, [1,2,3,4], 7, [[1,2], [1,2,3,4]], [1,2]] CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { + LIST [3] { elements: { CONSTANT [4] { value: 1 } CONSTANT [5] { value: 2 } @@ -685,43 +614,40 @@ CALL [1] { CONSTANT [7] { value: 4 } } } - CREATE_LIST [8] { + LIST [8] { elements: { CONSTANT [9] { value: 1 } CONSTANT [10] { value: 2 } } } - CREATE_LIST [11] { - elements: { - IDENT [12] { - name: @index1 - } - IDENT [13] { - name: @index0 - } - } - } } } - CREATE_LIST [14] { + LIST [11] { elements: { - CONSTANT [15] { value: 1 } - IDENT [16] { + CONSTANT [12] { value: 1 } + IDENT [13] { name: @index0 } - CONSTANT [17] { value: 2 } - IDENT [18] { + CONSTANT [14] { value: 2 } + IDENT [15] { name: @index0 } - CONSTANT [19] { value: 5 } - IDENT [20] { + CONSTANT [16] { value: 5 } + IDENT [17] { name: @index0 } - CONSTANT [21] { value: 7 } - IDENT [22] { - name: @index2 + CONSTANT [18] { value: 7 } + LIST [19] { + elements: { + IDENT [20] { + name: @index1 + } + IDENT [21] { + name: @index0 + } + } } - IDENT [23] { + IDENT [22] { name: @index1 } } @@ -734,33 +660,30 @@ Source: msg.single_int64 + msg.single_int64 == 6 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { name: msg }.single_int64 } - CALL [5] { + } + } + CALL [5] { + function: _==_ + args: { + CALL [6] { function: _+_ args: { - IDENT [6] { + IDENT [7] { name: @index0 } - IDENT [7] { + IDENT [8] { name: @index0 } } } - } - } - CALL [8] { - function: _==_ - args: { - IDENT [9] { - name: @index1 - } - CONSTANT [10] { value: 6 } + CONSTANT [9] { value: 6 } } } } @@ -771,71 +694,69 @@ Source: msg.oneof_type.payload.single_int64 + msg.oneof_type.payload.single_int3 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 - }.single_int64 - } - SELECT [9] { - SELECT [10] { - SELECT [11] { - IDENT [12] { - name: @index1 + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg }.oneof_type }.payload }.single_int64 } - CALL [13] { + SELECT [7] { + SELECT [8] { + IDENT [9] { + name: msg + }.oneof_type + }.payload + } + CALL [10] { function: _+_ args: { - CALL [14] { + CALL [11] { function: _+_ args: { - CALL [15] { + CALL [12] { function: _+_ args: { - IDENT [16] { - name: @index2 + IDENT [13] { + name: @index0 } - SELECT [17] { - IDENT [18] { + SELECT [14] { + IDENT [15] { name: @index1 }.single_int32 } } } - IDENT [19] { - name: @index2 + IDENT [16] { + name: @index0 } } } - SELECT [20] { - IDENT [21] { + SELECT [17] { + IDENT [18] { name: msg }.single_int64 } } } - CALL [22] { + CALL [19] { function: _+_ args: { - IDENT [23] { - name: @index4 + IDENT [20] { + name: @index2 } - IDENT [24] { - name: @index3 + SELECT [21] { + SELECT [22] { + SELECT [23] { + IDENT [24] { + name: @index1 + }.oneof_type + }.payload + }.single_int64 } } } @@ -845,7 +766,7 @@ CALL [1] { function: _==_ args: { IDENT [26] { - name: @index5 + name: @index3 } CONSTANT [27] { value: 31 } } @@ -858,74 +779,62 @@ Source: true || msg.oneof_type.payload.oneof_type.payload.oneof_type.payload.one CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 - }.oneof_type - } - SELECT [9] { - IDENT [10] { - name: @index2 + SELECT [4] { + SELECT [5] { + SELECT [6] { + IDENT [7] { + name: msg + }.oneof_type + }.payload + }.oneof_type }.payload } - SELECT [11] { - IDENT [12] { - name: @index3 + SELECT [8] { + IDENT [9] { + name: @index0 }.oneof_type } - SELECT [13] { - SELECT [14] { - SELECT [15] { - SELECT [16] { - IDENT [17] { - name: @index4 - }.child - }.child - }.payload - }.single_bool - } - SELECT [18] { - SELECT [19] { - SELECT [20] { - SELECT [21] { - IDENT [22] { - name: @index4 + SELECT [10] { + SELECT [11] { + SELECT [12] { + SELECT [13] { + IDENT [14] { + name: @index1 }.payload }.oneof_type }.payload }.single_bool } - CALL [23] { - function: _||_ - args: { - CONSTANT [24] { value: true } - IDENT [25] { - name: @index6 - } - } + SELECT [15] { + SELECT [16] { + SELECT [17] { + SELECT [18] { + IDENT [19] { + name: @index1 + }.child + }.child + }.payload + }.single_bool } } } - CALL [26] { + CALL [20] { function: _||_ args: { - IDENT [27] { - name: @index7 + CALL [21] { + function: _||_ + args: { + CONSTANT [22] { value: true } + IDENT [23] { + name: @index2 + } + } } - IDENT [28] { - name: @index5 + IDENT [24] { + name: @index3 } } } @@ -937,60 +846,48 @@ Source: msg.oneof_type.payload.map_int32_int64[1] + msg.oneof_type.payload.map_i CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 - }.map_int32_int64 - } - CALL [9] { + CALL [3] { function: _[_] args: { - IDENT [10] { - name: @index2 + SELECT [4] { + SELECT [5] { + SELECT [6] { + IDENT [7] { + name: msg + }.oneof_type + }.payload + }.map_int32_int64 } - CONSTANT [11] { value: 1 } + CONSTANT [8] { value: 1 } } } - CALL [12] { + } + } + CALL [9] { + function: _==_ + args: { + CALL [10] { function: _+_ args: { - CALL [13] { + CALL [11] { function: _+_ args: { - IDENT [14] { - name: @index3 + IDENT [12] { + name: @index0 } - IDENT [15] { - name: @index3 + IDENT [13] { + name: @index0 } } } - IDENT [16] { - name: @index3 + IDENT [14] { + name: @index0 } } } - } - } - CALL [17] { - function: _==_ - args: { - IDENT [18] { - name: @index4 - } - CONSTANT [19] { value: 15 } + CONSTANT [15] { value: 15 } } } } @@ -1001,69 +898,60 @@ Source: msg.oneof_type.payload.map_int32_int64[0] + msg.oneof_type.payload.map_i CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload }.map_int32_int64 } - CALL [9] { + } + } + CALL [7] { + function: _==_ + args: { + CALL [8] { function: _+_ args: { - CALL [10] { + CALL [9] { function: _+_ args: { - CALL [11] { + CALL [10] { function: _[_] args: { - IDENT [12] { - name: @index2 + IDENT [11] { + name: @index0 } - CONSTANT [13] { value: 0 } + CONSTANT [12] { value: 0 } } } - CALL [14] { + CALL [13] { function: _[_] args: { - IDENT [15] { - name: @index2 + IDENT [14] { + name: @index0 } - CONSTANT [16] { value: 1 } + CONSTANT [15] { value: 1 } } } } } - CALL [17] { + CALL [16] { function: _[_] args: { - IDENT [18] { - name: @index2 + IDENT [17] { + name: @index0 } - CONSTANT [19] { value: 2 } + CONSTANT [18] { value: 2 } } } } } - } - } - CALL [20] { - function: _==_ - args: { - IDENT [21] { - name: @index3 - } - CONSTANT [22] { value: 8 } + CONSTANT [19] { value: 8 } } } } @@ -1074,7 +962,7 @@ Source: msg.oneof_type.payload.oneof_type.payload.oneof_type.payload.oneof_type. CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { SELECT [4] { @@ -1113,40 +1001,37 @@ Source: (msg.single_int64 > 0 ? msg.single_int64 : 0) == 3 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { name: msg }.single_int64 } - CALL [5] { + } + } + CALL [5] { + function: _==_ + args: { + CALL [6] { function: _?_:_ args: { - CALL [6] { + CALL [7] { function: _>_ args: { - IDENT [7] { + IDENT [8] { name: @index0 } - CONSTANT [8] { value: 0 } + CONSTANT [9] { value: 0 } } } - IDENT [9] { + IDENT [10] { name: @index0 } - CONSTANT [10] { value: 0 } + CONSTANT [11] { value: 0 } } } - } - } - CALL [11] { - function: _==_ - args: { - IDENT [12] { - name: @index1 - } - CONSTANT [13] { value: 3 } + CONSTANT [12] { value: 3 } } } } @@ -1157,7 +1042,7 @@ Source: false ? false : (msg.single_int64) + ((msg.single_int64 + 1) * 2) == 11 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { @@ -1213,7 +1098,7 @@ Source: (msg.single_int64 > 0 ? (msg.single_int32 > 0 ? msg.single_int64 + msg.s CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { @@ -1225,56 +1110,53 @@ CALL [1] { name: msg }.single_int32 } - CALL [7] { + } + } + CALL [7] { + function: _==_ + args: { + CALL [8] { function: _?_:_ args: { - CALL [8] { + CALL [9] { function: _>_ args: { - IDENT [9] { + IDENT [10] { name: @index0 } - CONSTANT [10] { value: 0 } + CONSTANT [11] { value: 0 } } } - CALL [11] { + CALL [12] { function: _?_:_ args: { - CALL [12] { + CALL [13] { function: _>_ args: { - IDENT [13] { + IDENT [14] { name: @index1 } - CONSTANT [14] { value: 0 } + CONSTANT [15] { value: 0 } } } - CALL [15] { + CALL [16] { function: _+_ args: { - IDENT [16] { + IDENT [17] { name: @index0 } - IDENT [17] { + IDENT [18] { name: @index1 } } } - CONSTANT [18] { value: 0 } + CONSTANT [19] { value: 0 } } } - CONSTANT [19] { value: 0 } + CONSTANT [20] { value: 0 } } } - } - } - CALL [20] { - function: _==_ - args: { - IDENT [21] { - name: @index2 - } - CONSTANT [22] { value: 8 } + CONSTANT [21] { value: 8 } } } } @@ -1285,53 +1167,83 @@ Source: size([[1].exists(i, i > 0)]) + size([[1].exists(j, j > 0)]) + size([[2]. CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } + COMPREHENSION [3] { + iter_var: @it:0:0 + iter_range: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + } + } } - } - CALL [5] { - function: _>_ - args: { - IDENT [6] { - name: @c0:0 + accu_var: @ac:0:0 + accu_init: { + CONSTANT [6] { value: false } + } + loop_condition: { + CALL [7] { + function: @not_strictly_false + args: { + CALL [8] { + function: !_ + args: { + IDENT [9] { + name: @ac:0:0 + } + } + } + } } - CONSTANT [7] { value: 0 } } - } - CALL [8] { - function: _||_ - args: { - IDENT [9] { - name: @x0:0 + loop_step: { + CALL [10] { + function: _||_ + args: { + IDENT [11] { + name: @ac:0:0 + } + CALL [12] { + function: _>_ + args: { + IDENT [13] { + name: @it:0:0 + } + CONSTANT [14] { value: 0 } + } + } + } } - IDENT [10] { - name: @index1 + } + result: { + IDENT [15] { + name: @ac:0:0 } } } - COMPREHENSION [11] { - iter_var: @c0:0 + COMPREHENSION [16] { + iter_var: @it:0:0 iter_range: { - IDENT [12] { - name: @index0 + LIST [17] { + elements: { + CONSTANT [18] { value: 2 } + } } } - accu_var: @x0:0 + accu_var: @ac:0:0 accu_init: { - CONSTANT [13] { value: false } + CONSTANT [19] { value: false } } loop_condition: { - CALL [14] { + CALL [20] { function: @not_strictly_false args: { - CALL [15] { + CALL [21] { function: !_ args: { - IDENT [16] { - name: @x0:0 + IDENT [22] { + name: @ac:0:0 } } } @@ -1339,144 +1251,87 @@ CALL [1] { } } loop_step: { - IDENT [17] { - name: @index2 + CALL [23] { + function: _||_ + args: { + IDENT [24] { + name: @ac:0:0 + } + CALL [25] { + function: _>_ + args: { + IDENT [26] { + name: @it:0:0 + } + CONSTANT [27] { value: 1 } + } + } + } } } result: { - IDENT [18] { - name: @x0:0 + IDENT [28] { + name: @ac:0:0 } } } - CREATE_LIST [19] { - elements: { - IDENT [20] { - name: @index3 + CALL [29] { + function: size + args: { + LIST [30] { + elements: { + IDENT [31] { + name: @index0 + } + } } } } - CALL [21] { + CALL [32] { function: size args: { - IDENT [22] { - name: @index4 - } - } - } - CREATE_LIST [23] { - elements: { - CONSTANT [24] { value: 2 } - } - } - CALL [25] { - function: _>_ - args: { - IDENT [26] { - name: @c0:0 - } - CONSTANT [27] { value: 1 } - } - } - CALL [28] { - function: _||_ - args: { - IDENT [29] { - name: @x0:0 - } - IDENT [30] { - name: @index7 - } - } - } - COMPREHENSION [31] { - iter_var: @c0:0 - iter_range: { - IDENT [32] { - name: @index6 - } - } - accu_var: @x0:0 - accu_init: { - CONSTANT [33] { value: false } - } - loop_condition: { - CALL [34] { - function: @not_strictly_false - args: { - CALL [35] { - function: !_ - args: { - IDENT [36] { - name: @x0:0 - } - } + LIST [33] { + elements: { + IDENT [34] { + name: @index1 } } } } - loop_step: { - IDENT [37] { - name: @index8 - } - } - result: { - IDENT [38] { - name: @x0:0 - } - } - } - CREATE_LIST [39] { - elements: { - IDENT [40] { - name: @index9 - } - } - } - CALL [41] { - function: size - args: { - IDENT [42] { - name: @index10 - } - } } - CALL [43] { + } + } + CALL [35] { + function: _==_ + args: { + CALL [36] { function: _+_ args: { - CALL [44] { + CALL [37] { function: _+_ args: { - CALL [45] { + CALL [38] { function: _+_ args: { - IDENT [46] { - name: @index5 + IDENT [39] { + name: @index2 } - IDENT [47] { - name: @index5 + IDENT [40] { + name: @index2 } } } - IDENT [48] { - name: @index11 + IDENT [41] { + name: @index3 } } } - IDENT [49] { - name: @index11 + IDENT [42] { + name: @index3 } } } - } - } - CALL [50] { - function: _==_ - args: { - IDENT [51] { - name: @index12 - } - CONSTANT [52] { value: 4 } + CONSTANT [43] { value: 4 } } } } @@ -1487,53 +1342,30 @@ Source: [[1].exists(i, i > 0)] + [[1].exists(j, j > 0)] + [['a'].exists(k, k == CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - } - } - CALL [5] { - function: _>_ - args: { - IDENT [6] { - name: @c0:0 - } - CONSTANT [7] { value: 0 } - } - } - CALL [8] { - function: _||_ - args: { - IDENT [9] { - name: @x0:0 - } - IDENT [10] { - name: @index1 - } - } - } - COMPREHENSION [11] { - iter_var: @c0:0 + COMPREHENSION [3] { + iter_var: @it:0:0 iter_range: { - IDENT [12] { - name: @index0 + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + } } } - accu_var: @x0:0 + accu_var: @ac:0:0 accu_init: { - CONSTANT [13] { value: false } + CONSTANT [6] { value: false } } loop_condition: { - CALL [14] { + CALL [7] { function: @not_strictly_false args: { - CALL [15] { + CALL [8] { function: !_ args: { - IDENT [16] { - name: @x0:0 + IDENT [9] { + name: @ac:0:0 } } } @@ -1541,68 +1373,52 @@ CALL [1] { } } loop_step: { - IDENT [17] { - name: @index2 + CALL [10] { + function: _||_ + args: { + IDENT [11] { + name: @ac:0:0 + } + CALL [12] { + function: _>_ + args: { + IDENT [13] { + name: @it:0:0 + } + CONSTANT [14] { value: 0 } + } + } + } } } result: { - IDENT [18] { - name: @x0:0 - } - } - } - CREATE_LIST [19] { - elements: { - IDENT [20] { - name: @index3 - } - } - } - CREATE_LIST [21] { - elements: { - CONSTANT [22] { value: "a" } - } - } - CALL [23] { - function: _==_ - args: { - IDENT [24] { - name: @c0:1 - } - CONSTANT [25] { value: "a" } - } - } - CALL [26] { - function: _||_ - args: { - IDENT [27] { - name: @x0:1 - } - IDENT [28] { - name: @index6 + IDENT [15] { + name: @ac:0:0 } } } - COMPREHENSION [29] { - iter_var: @c0:1 + COMPREHENSION [16] { + iter_var: @it:0:1 iter_range: { - IDENT [30] { - name: @index5 + LIST [17] { + elements: { + CONSTANT [18] { value: "a" } + } } } - accu_var: @x0:1 + accu_var: @ac:0:1 accu_init: { - CONSTANT [31] { value: false } + CONSTANT [19] { value: false } } loop_condition: { - CALL [32] { + CALL [20] { function: @not_strictly_false args: { - CALL [33] { + CALL [21] { function: !_ args: { - IDENT [34] { - name: @x0:1 + IDENT [22] { + name: @ac:0:1 } } } @@ -1610,325 +1426,412 @@ CALL [1] { } } loop_step: { - IDENT [35] { - name: @index7 + CALL [23] { + function: _||_ + args: { + IDENT [24] { + name: @ac:0:1 + } + CALL [25] { + function: _==_ + args: { + IDENT [26] { + name: @it:0:1 + } + CONSTANT [27] { value: "a" } + } + } + } } } result: { - IDENT [36] { - name: @x0:1 + IDENT [28] { + name: @ac:0:1 } } } - CREATE_LIST [37] { + LIST [29] { elements: { - IDENT [38] { - name: @index8 + IDENT [30] { + name: @index0 } } } - CREATE_LIST [39] { + LIST [31] { elements: { - CONSTANT [40] { value: true } - CONSTANT [41] { value: true } - CONSTANT [42] { value: true } - CONSTANT [43] { value: true } + IDENT [32] { + name: @index1 + } } } - CALL [44] { + } + } + CALL [33] { + function: _==_ + args: { + CALL [34] { function: _+_ args: { - CALL [45] { + CALL [35] { function: _+_ args: { - CALL [46] { + CALL [36] { function: _+_ args: { - IDENT [47] { - name: @index4 + IDENT [37] { + name: @index2 } - IDENT [48] { - name: @index4 + IDENT [38] { + name: @index2 } } } - IDENT [49] { - name: @index9 + IDENT [39] { + name: @index3 } } } - IDENT [50] { - name: @index9 + IDENT [40] { + name: @index3 } } } - } - } - CALL [51] { - function: _==_ - args: { - IDENT [52] { - name: @index11 - } - IDENT [53] { - name: @index10 + LIST [41] { + elements: { + CONSTANT [42] { value: true } + CONSTANT [43] { value: true } + CONSTANT [44] { value: true } + CONSTANT [45] { value: true } + } } } } } } -Test case: NESTED_MACROS -Source: [1,2,3].map(i, [1, 2, 3].map(i, i + 1)) == [[2, 3, 4], [2, 3, 4], [2, 3, 4]] +Test case: MULTIPLE_MACROS_3 +Source: [1].exists(i, i > 0) && [1].exists(j, j > 0) && [1].exists(k, k > 1) && [2].exists(l, l > 1) =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - CONSTANT [5] { value: 2 } - CONSTANT [6] { value: 3 } - } - } - CREATE_LIST [7] { - elements: { - CONSTANT [8] { value: 2 } - CONSTANT [9] { value: 3 } - CONSTANT [10] { value: 4 } - } - } - CREATE_LIST [11] { - elements: { - IDENT [12] { - name: @index1 - } - IDENT [13] { - name: @index1 - } - IDENT [14] { - name: @index1 - } - } - } - COMPREHENSION [15] { - iter_var: @c1:0 + COMPREHENSION [3] { + iter_var: @it:0:0 iter_range: { - IDENT [16] { - name: @index0 - } - } - accu_var: @x1:0 - accu_init: { - CREATE_LIST [17] { + LIST [4] { elements: { + CONSTANT [5] { value: 1 } } } } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [6] { value: false } + } loop_condition: { - CONSTANT [18] { value: true } + CALL [7] { + function: @not_strictly_false + args: { + CALL [8] { + function: !_ + args: { + IDENT [9] { + name: @ac:0:0 + } + } + } + } + } } loop_step: { - CALL [19] { - function: _+_ + CALL [10] { + function: _||_ args: { - IDENT [20] { - name: @x1:0 + IDENT [11] { + name: @ac:0:0 } - CREATE_LIST [21] { - elements: { - CALL [22] { - function: _+_ - args: { - IDENT [23] { - name: @c1:0 - } - CONSTANT [24] { value: 1 } - } + CALL [12] { + function: _>_ + args: { + IDENT [13] { + name: @it:0:0 } + CONSTANT [14] { value: 0 } } } } } } result: { - IDENT [25] { - name: @x1:0 + IDENT [15] { + name: @ac:0:0 } } } - COMPREHENSION [26] { - iter_var: @c0:0 - iter_range: { - IDENT [27] { + } + } + CALL [16] { + function: _&&_ + args: { + CALL [17] { + function: _&&_ + args: { + IDENT [18] { name: @index0 } - } - accu_var: @x0:0 - accu_init: { - CREATE_LIST [28] { - elements: { - } + IDENT [19] { + name: @index0 } } - loop_condition: { - CONSTANT [29] { value: true } - } - loop_step: { - CALL [30] { - function: _+_ - args: { - IDENT [31] { - name: @x0:0 - } - CREATE_LIST [32] { + } + CALL [20] { + function: _&&_ + args: { + COMPREHENSION [21] { + iter_var: @it:0:0 + iter_range: { + LIST [22] { elements: { - IDENT [33] { - name: @index3 + CONSTANT [23] { value: 1 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [24] { value: false } + } + loop_condition: { + CALL [25] { + function: @not_strictly_false + args: { + CALL [26] { + function: !_ + args: { + IDENT [27] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [28] { + function: _||_ + args: { + IDENT [29] { + name: @ac:0:0 + } + CALL [30] { + function: _>_ + args: { + IDENT [31] { + name: @it:0:0 + } + CONSTANT [32] { value: 1 } + } } } } } + result: { + IDENT [33] { + name: @ac:0:0 + } + } } - } - result: { - IDENT [34] { - name: @x0:0 + COMPREHENSION [34] { + iter_var: @it:0:0 + iter_range: { + LIST [35] { + elements: { + CONSTANT [36] { value: 2 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [37] { value: false } + } + loop_condition: { + CALL [38] { + function: @not_strictly_false + args: { + CALL [39] { + function: !_ + args: { + IDENT [40] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [41] { + function: _||_ + args: { + IDENT [42] { + name: @ac:0:0 + } + CALL [43] { + function: _>_ + args: { + IDENT [44] { + name: @it:0:0 + } + CONSTANT [45] { value: 1 } + } + } + } + } + } + result: { + IDENT [46] { + name: @ac:0:0 + } + } } } } } } - CALL [35] { - function: _==_ - args: { - IDENT [36] { - name: @index4 - } - IDENT [37] { - name: @index2 - } - } - } } } -Test case: NESTED_MACROS_2 -Source: [1, 2].map(y, [1, 2, 3].filter(x, x == y)) == [[1], [2]] +Test case: MULTIPLE_MACROS_COMP_V2_1 +Source: size([[1].exists(i,x,i > 0 && x >= 0)]) + size([[1].exists(j, y, j > 0 && y >= 0)]) + size([[2].exists(k,z,k > 1 && z >= 0)]) + size([[2].exists(l, w, l > 1 && w >= 0)]) == 4 =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CREATE_LIST [4] { - elements: { - CONSTANT [5] { value: 1 } - } - } - CREATE_LIST [6] { - elements: { - CONSTANT [7] { value: 2 } - } - } - } - } - COMPREHENSION [8] { - iter_var: @c1:0 + COMPREHENSION [3] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 iter_range: { - CREATE_LIST [9] { + LIST [4] { elements: { - CONSTANT [10] { value: 1 } - CONSTANT [11] { value: 2 } - CONSTANT [12] { value: 3 } + CONSTANT [5] { value: 1 } } } } - accu_var: @x1:0 + accu_var: @ac:0:0 accu_init: { - CREATE_LIST [13] { - elements: { - } - } + CONSTANT [6] { value: false } } loop_condition: { - CONSTANT [14] { value: true } - } - loop_step: { - CALL [15] { - function: _?_:_ + CALL [7] { + function: @not_strictly_false args: { - CALL [16] { - function: _==_ + CALL [8] { + function: !_ args: { - IDENT [17] { - name: @c1:0 - } - IDENT [18] { - name: @c0:0 + IDENT [9] { + name: @ac:0:0 } } } - CALL [19] { - function: _+_ + } + } + } + loop_step: { + CALL [10] { + function: _||_ + args: { + IDENT [11] { + name: @ac:0:0 + } + CALL [12] { + function: _&&_ args: { - IDENT [20] { - name: @x1:0 + CALL [13] { + function: _>_ + args: { + IDENT [14] { + name: @it:0:0 + } + CONSTANT [15] { value: 0 } + } } - CREATE_LIST [21] { - elements: { - IDENT [22] { - name: @c1:0 + CALL [16] { + function: _>=_ + args: { + IDENT [17] { + name: @it2:0:0 } + CONSTANT [18] { value: 0 } } } } } - IDENT [23] { - name: @x1:0 - } } } } result: { - IDENT [24] { - name: @x1:0 + IDENT [19] { + name: @ac:0:0 } } } - COMPREHENSION [25] { - iter_var: @c0:0 + COMPREHENSION [20] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 iter_range: { - CREATE_LIST [26] { + LIST [21] { elements: { - CONSTANT [27] { value: 1 } - CONSTANT [28] { value: 2 } + CONSTANT [22] { value: 2 } } } } - accu_var: @x0:0 + accu_var: @ac:0:0 accu_init: { - CREATE_LIST [29] { - elements: { - } - } + CONSTANT [23] { value: false } } loop_condition: { - CONSTANT [30] { value: true } + CALL [24] { + function: @not_strictly_false + args: { + CALL [25] { + function: !_ + args: { + IDENT [26] { + name: @ac:0:0 + } + } + } + } + } } loop_step: { - CALL [31] { - function: _+_ + CALL [27] { + function: _||_ args: { - IDENT [32] { - name: @x0:0 + IDENT [28] { + name: @ac:0:0 } - CREATE_LIST [33] { - elements: { - IDENT [34] { - name: @index1 + CALL [29] { + function: _&&_ + args: { + CALL [30] { + function: _>_ + args: { + IDENT [31] { + name: @it:0:0 + } + CONSTANT [32] { value: 1 } + } + } + CALL [33] { + function: _>=_ + args: { + IDENT [34] { + name: @it2:0:0 + } + CONSTANT [35] { value: 0 } + } } } } @@ -1936,359 +1839,1977 @@ CALL [1] { } } result: { - IDENT [35] { - name: @x0:0 + IDENT [36] { + name: @ac:0:0 + } + } + } + CALL [37] { + function: size + args: { + LIST [38] { + elements: { + IDENT [39] { + name: @index0 + } + } + } + } + } + CALL [40] { + function: size + args: { + LIST [41] { + elements: { + IDENT [42] { + name: @index1 + } + } } } } } } - CALL [36] { + CALL [43] { function: _==_ args: { - IDENT [37] { - name: @index2 - } - IDENT [38] { - name: @index0 + CALL [44] { + function: _+_ + args: { + CALL [45] { + function: _+_ + args: { + CALL [46] { + function: _+_ + args: { + IDENT [47] { + name: @index2 + } + IDENT [48] { + name: @index2 + } + } + } + IDENT [49] { + name: @index3 + } + } + } + IDENT [50] { + name: @index3 + } + } } + CONSTANT [51] { value: 4 } } } } } -Test case: INCLUSION_LIST -Source: 1 in [1,2,3] && 2 in [1,2,3] && 3 in [3, [1,2,3]] && 1 in [1,2,3] +Test case: MULTIPLE_MACROS_COMP_V2_2 +Source: [1].exists(i, x, i > 0 && x > 0) && [1].exists(j, y, j > 0 && y > 0) && [1].exists(k, z, k > 1 && z > 0) && [2].exists(l, w, l > 1 && w > 0) =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - CONSTANT [5] { value: 2 } - CONSTANT [6] { value: 3 } - } - } - CALL [7] { - function: @in - args: { - CONSTANT [8] { value: 1 } - IDENT [9] { - name: @index0 + COMPREHENSION [3] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + } } } - } - CALL [10] { - function: _&&_ - args: { - CALL [11] { - function: @in + accu_var: @ac:0:0 + accu_init: { + CONSTANT [6] { value: false } + } + loop_condition: { + CALL [7] { + function: @not_strictly_false args: { - CONSTANT [12] { value: 3 } - CREATE_LIST [13] { - elements: { - CONSTANT [14] { value: 3 } - IDENT [15] { - name: @index0 + CALL [8] { + function: !_ + args: { + IDENT [9] { + name: @ac:0:0 } } } } } - IDENT [16] { - name: @index1 - } } - } - CALL [17] { - function: _&&_ - args: { - IDENT [18] { - name: @index1 - } - CALL [19] { - function: @in + loop_step: { + CALL [10] { + function: _||_ args: { - CONSTANT [20] { value: 2 } - IDENT [21] { - name: @index0 + IDENT [11] { + name: @ac:0:0 + } + CALL [12] { + function: _&&_ + args: { + CALL [13] { + function: _>_ + args: { + IDENT [14] { + name: @it:0:0 + } + CONSTANT [15] { value: 0 } + } + } + CALL [16] { + function: _>_ + args: { + IDENT [17] { + name: @it2:0:0 + } + CONSTANT [18] { value: 0 } + } + } + } } } } } + result: { + IDENT [19] { + name: @ac:0:0 + } + } } } } - CALL [22] { + CALL [20] { function: _&&_ args: { - IDENT [23] { - name: @index3 - } - IDENT [24] { - name: @index2 - } - } - } - } -} -Test case: INCLUSION_MAP -Source: 2 in {'a': 1, 2: {true: false}, 3: {true: false}} -=====> -CALL [1] { - function: cel.@block - args: { - CREATE_LIST [2] { - elements: { - CREATE_MAP [3] { - MAP_ENTRY [4] { - key: { - CONSTANT [5] { value: true } + CALL [21] { + function: _&&_ + args: { + IDENT [22] { + name: @index0 } - value: { - CONSTANT [6] { value: false } + IDENT [23] { + name: @index0 } } } - CREATE_MAP [7] { - MAP_ENTRY [8] { - key: { - CONSTANT [9] { value: "a" } - } - value: { - CONSTANT [10] { value: 1 } - } - } - MAP_ENTRY [11] { - key: { - CONSTANT [12] { value: 2 } - } - value: { - IDENT [13] { - name: @index0 + CALL [24] { + function: _&&_ + args: { + COMPREHENSION [25] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + LIST [26] { + elements: { + CONSTANT [27] { value: 1 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [28] { value: false } + } + loop_condition: { + CALL [29] { + function: @not_strictly_false + args: { + CALL [30] { + function: !_ + args: { + IDENT [31] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [32] { + function: _||_ + args: { + IDENT [33] { + name: @ac:0:0 + } + CALL [34] { + function: _&&_ + args: { + CALL [35] { + function: _>_ + args: { + IDENT [36] { + name: @it:0:0 + } + CONSTANT [37] { value: 1 } + } + } + CALL [38] { + function: _>_ + args: { + IDENT [39] { + name: @it2:0:0 + } + CONSTANT [40] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [41] { + name: @ac:0:0 + } } } - } - MAP_ENTRY [14] { - key: { - CONSTANT [15] { value: 3 } - } - value: { - IDENT [16] { - name: @index0 + COMPREHENSION [42] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + LIST [43] { + elements: { + CONSTANT [44] { value: 2 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [45] { value: false } + } + loop_condition: { + CALL [46] { + function: @not_strictly_false + args: { + CALL [47] { + function: !_ + args: { + IDENT [48] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [49] { + function: _||_ + args: { + IDENT [50] { + name: @ac:0:0 + } + CALL [51] { + function: _&&_ + args: { + CALL [52] { + function: _>_ + args: { + IDENT [53] { + name: @it:0:0 + } + CONSTANT [54] { value: 1 } + } + } + CALL [55] { + function: _>_ + args: { + IDENT [56] { + name: @it2:0:0 + } + CONSTANT [57] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [58] { + name: @ac:0:0 + } } } } } } } - CALL [17] { - function: @in - args: { - CONSTANT [18] { value: 2 } - IDENT [19] { - name: @index1 - } - } - } } } -Test case: MACRO_ITER_VAR_NOT_REFERENCED -Source: [1,2].map(i, [1, 2].map(i, [3,4])) == [[[3, 4], [3, 4]], [[3, 4], [3, 4]]] +Test case: NESTED_MACROS +Source: [1,2,3].map(i, [1, 2, 3].map(i, i + 1)) == [[2, 3, 4], [2, 3, 4], [2, 3, 4]] =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { + LIST [3] { elements: { CONSTANT [4] { value: 1 } CONSTANT [5] { value: 2 } + CONSTANT [6] { value: 3 } } } - CREATE_LIST [6] { + LIST [7] { elements: { - CONSTANT [7] { value: 3 } - CONSTANT [8] { value: 4 } + CONSTANT [8] { value: 2 } + CONSTANT [9] { value: 3 } + CONSTANT [10] { value: 4 } } } - CREATE_LIST [9] { - elements: { - IDENT [10] { - name: @index1 - } - IDENT [11] { - name: @index1 + COMPREHENSION [11] { + iter_var: @it:0:0 + iter_range: { + IDENT [12] { + name: @index0 } } - } - CREATE_LIST [12] { - elements: { - IDENT [13] { - name: @index2 - } - IDENT [14] { - name: @index2 + accu_var: @ac:0:0 + accu_init: { + LIST [13] { + elements: { + } } } - } - CREATE_LIST [15] { - elements: { - COMPREHENSION [16] { - iter_var: @c1:0 - iter_range: { - IDENT [17] { - name: @index0 + loop_condition: { + CONSTANT [14] { value: true } + } + loop_step: { + CALL [15] { + function: _+_ + args: { + IDENT [16] { + name: @ac:0:0 } - } - accu_var: @x1:0 - accu_init: { - CREATE_LIST [18] { + LIST [17] { elements: { - } - } - } - loop_condition: { - CONSTANT [19] { value: true } - } - loop_step: { - CALL [20] { - function: _+_ - args: { - IDENT [21] { - name: @x1:0 - } - CREATE_LIST [22] { - elements: { - IDENT [23] { - name: @index1 + CALL [18] { + function: _+_ + args: { + IDENT [19] { + name: @it:0:0 } + CONSTANT [20] { value: 1 } } } } } } - result: { - IDENT [24] { - name: @x1:0 - } - } + } + } + result: { + IDENT [21] { + name: @ac:0:0 } } } - COMPREHENSION [25] { - iter_var: @c0:0 + } + } + CALL [22] { + function: _==_ + args: { + COMPREHENSION [23] { + iter_var: @it:1:0 iter_range: { - IDENT [26] { + IDENT [24] { name: @index0 } } - accu_var: @x0:0 + accu_var: @ac:1:0 accu_init: { - CREATE_LIST [27] { + LIST [25] { elements: { } } } loop_condition: { - CONSTANT [28] { value: true } + CONSTANT [26] { value: true } } loop_step: { - CALL [29] { + CALL [27] { function: _+_ args: { - IDENT [30] { - name: @x0:0 + IDENT [28] { + name: @ac:1:0 } - IDENT [31] { - name: @index4 + LIST [29] { + elements: { + IDENT [30] { + name: @index2 + } + } + } + } + } + } + result: { + IDENT [31] { + name: @ac:1:0 + } + } + } + LIST [32] { + elements: { + IDENT [33] { + name: @index1 + } + IDENT [34] { + name: @index1 + } + IDENT [35] { + name: @index1 + } + } + } + } + } + } +} +Test case: NESTED_MACROS_2 +Source: [1, 2].map(y, [1, 2, 3].filter(x, x == y)) == [[1], [2]] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + } + } + LIST [6] { + elements: { + CONSTANT [7] { value: 2 } + } + } + } + } + LIST [8] { + elements: { + CONSTANT [9] { value: 1 } + CONSTANT [10] { value: 2 } + } + } + LIST [11] { + elements: { + CONSTANT [12] { value: 1 } + CONSTANT [13] { value: 2 } + CONSTANT [14] { value: 3 } + } + } + } + } + CALL [15] { + function: _==_ + args: { + COMPREHENSION [16] { + iter_var: @it:1:0 + iter_range: { + IDENT [17] { + name: @index1 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [18] { + elements: { + } + } + } + loop_condition: { + CONSTANT [19] { value: true } + } + loop_step: { + CALL [20] { + function: _+_ + args: { + IDENT [21] { + name: @ac:1:0 + } + LIST [22] { + elements: { + COMPREHENSION [23] { + iter_var: @it:0:0 + iter_range: { + IDENT [24] { + name: @index2 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [25] { + elements: { + } + } + } + loop_condition: { + CONSTANT [26] { value: true } + } + loop_step: { + CALL [27] { + function: _?_:_ + args: { + CALL [28] { + function: _==_ + args: { + IDENT [29] { + name: @it:0:0 + } + IDENT [30] { + name: @it:1:0 + } + } + } + CALL [31] { + function: _+_ + args: { + IDENT [32] { + name: @ac:0:0 + } + LIST [33] { + elements: { + IDENT [34] { + name: @it:0:0 + } + } + } + } + } + IDENT [35] { + name: @ac:0:0 + } + } + } + } + result: { + IDENT [36] { + name: @ac:0:0 + } + } + } + } + } + } + } + } + result: { + IDENT [37] { + name: @ac:1:0 + } + } + } + IDENT [38] { + name: @index0 + } + } + } + } +} +Test case: NESTED_MACROS_3 +Source: [1,2,3].map(i, i * 2) + [1,2,3].map(i, i * 2).map(i, i * 2) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + COMPREHENSION [3] { + iter_var: @it:0:0 + iter_range: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + CONSTANT [6] { value: 2 } + CONSTANT [7] { value: 3 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [8] { + elements: { + } + } + } + loop_condition: { + CONSTANT [9] { value: true } + } + loop_step: { + CALL [10] { + function: _+_ + args: { + IDENT [11] { + name: @ac:0:0 + } + LIST [12] { + elements: { + CALL [13] { + function: _*_ + args: { + IDENT [14] { + name: @it:0:0 + } + CONSTANT [15] { value: 2 } + } + } + } + } + } + } + } + result: { + IDENT [16] { + name: @ac:0:0 + } + } + } + } + } + CALL [17] { + function: _+_ + args: { + IDENT [18] { + name: @index0 + } + COMPREHENSION [19] { + iter_var: @it:1:0 + iter_range: { + IDENT [20] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [21] { + elements: { + } + } + } + loop_condition: { + CONSTANT [22] { value: true } + } + loop_step: { + CALL [23] { + function: _+_ + args: { + IDENT [24] { + name: @ac:1:0 + } + LIST [25] { + elements: { + CALL [26] { + function: _*_ + args: { + IDENT [27] { + name: @it:1:0 + } + CONSTANT [28] { value: 2 } + } + } + } + } + } + } + } + result: { + IDENT [29] { + name: @ac:1:0 + } + } + } + } + } + } +} +Test case: NESTED_MACROS_COMP_V2_1 +Source: [1,2,3].transformList(i, v, [1, 2, 3].transformList(i, v, i + v + 1)) == [[2, 4, 6], [2, 4, 6], [2, 4, 6]] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + CONSTANT [5] { value: 2 } + CONSTANT [6] { value: 3 } + } + } + LIST [7] { + elements: { + CONSTANT [8] { value: 2 } + CONSTANT [9] { value: 4 } + CONSTANT [10] { value: 6 } + } + } + LIST [11] { + elements: { + IDENT [12] { + name: @index1 + } + IDENT [13] { + name: @index1 + } + IDENT [14] { + name: @index1 + } + } + } + } + } + CALL [15] { + function: _==_ + args: { + COMPREHENSION [16] { + iter_var: @it:1:0 + iter_var2: @it2:1:0 + iter_range: { + IDENT [17] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [18] { + elements: { + } + } + } + loop_condition: { + CONSTANT [19] { value: true } + } + loop_step: { + CALL [20] { + function: _+_ + args: { + IDENT [21] { + name: @ac:1:0 + } + LIST [22] { + elements: { + COMPREHENSION [23] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + IDENT [24] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [25] { + elements: { + } + } + } + loop_condition: { + CONSTANT [26] { value: true } + } + loop_step: { + CALL [27] { + function: _+_ + args: { + IDENT [28] { + name: @ac:0:0 + } + LIST [29] { + elements: { + CALL [30] { + function: _+_ + args: { + CALL [31] { + function: _+_ + args: { + IDENT [32] { + name: @it:0:0 + } + IDENT [33] { + name: @it2:0:0 + } + } + } + CONSTANT [34] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [35] { + name: @ac:0:0 + } + } + } + } + } + } + } + } + result: { + IDENT [36] { + name: @ac:1:0 + } + } + } + IDENT [37] { + name: @index2 + } + } + } + } +} +Test case: NESTED_MACROS_COMP_V2_2 +Source: [1, 2].transformList(i, y, [1, 2, 3].filter(x, x == y && i < y)) == [[1], [2]] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + } + } + LIST [6] { + elements: { + CONSTANT [7] { value: 2 } + } + } + } + } + LIST [8] { + elements: { + CONSTANT [9] { value: 1 } + CONSTANT [10] { value: 2 } + } + } + LIST [11] { + elements: { + CONSTANT [12] { value: 1 } + CONSTANT [13] { value: 2 } + CONSTANT [14] { value: 3 } + } + } + } + } + CALL [15] { + function: _==_ + args: { + COMPREHENSION [16] { + iter_var: @it:1:0 + iter_var2: @it2:1:0 + iter_range: { + IDENT [17] { + name: @index1 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [18] { + elements: { + } + } + } + loop_condition: { + CONSTANT [19] { value: true } + } + loop_step: { + CALL [20] { + function: _+_ + args: { + IDENT [21] { + name: @ac:1:0 + } + LIST [22] { + elements: { + COMPREHENSION [23] { + iter_var: @it:0:0 + iter_range: { + IDENT [24] { + name: @index2 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [25] { + elements: { + } + } + } + loop_condition: { + CONSTANT [26] { value: true } + } + loop_step: { + CALL [27] { + function: _?_:_ + args: { + CALL [28] { + function: _&&_ + args: { + CALL [29] { + function: _==_ + args: { + IDENT [30] { + name: @it:0:0 + } + IDENT [31] { + name: @it2:1:0 + } + } + } + CALL [32] { + function: _<_ + args: { + IDENT [33] { + name: @it:1:0 + } + IDENT [34] { + name: @it2:1:0 + } + } + } + } + } + CALL [35] { + function: _+_ + args: { + IDENT [36] { + name: @ac:0:0 + } + LIST [37] { + elements: { + IDENT [38] { + name: @it:0:0 + } + } + } + } + } + IDENT [39] { + name: @ac:0:0 + } + } + } + } + result: { + IDENT [40] { + name: @ac:0:0 + } + } + } + } + } + } + } + } + result: { + IDENT [41] { + name: @ac:1:0 + } + } + } + IDENT [42] { + name: @index0 + } + } + } + } +} +Test case: ADJACENT_NESTED_MACROS +Source: [1,2,3].map(i, [1, 2, 3].map(i, i + 1)) == [1,2,3].map(j, [1, 2, 3].map(j, j + 1)) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + COMPREHENSION [3] { + iter_var: @it:0:0 + iter_range: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + CONSTANT [6] { value: 2 } + CONSTANT [7] { value: 3 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [8] { + elements: { + } + } + } + loop_condition: { + CONSTANT [9] { value: true } + } + loop_step: { + CALL [10] { + function: _+_ + args: { + IDENT [11] { + name: @ac:0:0 + } + LIST [12] { + elements: { + CALL [13] { + function: _+_ + args: { + IDENT [14] { + name: @it:0:0 + } + CONSTANT [15] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [16] { + name: @ac:0:0 + } + } + } + COMPREHENSION [17] { + iter_var: @it:1:0 + iter_range: { + LIST [18] { + elements: { + CONSTANT [19] { value: 1 } + CONSTANT [20] { value: 2 } + CONSTANT [21] { value: 3 } + } + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [22] { + elements: { + } + } + } + loop_condition: { + CONSTANT [23] { value: true } + } + loop_step: { + CALL [24] { + function: _+_ + args: { + IDENT [25] { + name: @ac:1:0 + } + LIST [26] { + elements: { + IDENT [27] { + name: @index0 + } + } + } + } + } + } + result: { + IDENT [28] { + name: @ac:1:0 + } + } + } + } + } + CALL [29] { + function: _==_ + args: { + IDENT [30] { + name: @index1 + } + IDENT [31] { + name: @index1 + } + } + } + } +} +Test case: ADJACENT_NESTED_MACROS_COMP_V2 +Source: [1,2,3].transformMap(i, x, [1, 2, 3].transformMap(i, x, i + x + 1)) == [1,2,3].transformMap(j, y, [1, 2, 3].transformMap(j, y, j + y + 1)) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + COMPREHENSION [3] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + CONSTANT [6] { value: 2 } + CONSTANT [7] { value: 3 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + MAP [8] { + + } + } + loop_condition: { + CONSTANT [9] { value: true } + } + loop_step: { + CALL [10] { + function: cel.@mapInsert + args: { + IDENT [11] { + name: @ac:0:0 + } + IDENT [12] { + name: @it:0:0 + } + CALL [13] { + function: _+_ + args: { + CALL [14] { + function: _+_ + args: { + IDENT [15] { + name: @it:0:0 + } + IDENT [16] { + name: @it2:0:0 + } + } + } + CONSTANT [17] { value: 1 } + } + } + } + } + } + result: { + IDENT [18] { + name: @ac:0:0 + } + } + } + COMPREHENSION [19] { + iter_var: @it:1:0 + iter_var2: @it2:1:0 + iter_range: { + LIST [20] { + elements: { + CONSTANT [21] { value: 1 } + CONSTANT [22] { value: 2 } + CONSTANT [23] { value: 3 } + } + } + } + accu_var: @ac:1:0 + accu_init: { + MAP [24] { + + } + } + loop_condition: { + CONSTANT [25] { value: true } + } + loop_step: { + CALL [26] { + function: cel.@mapInsert + args: { + IDENT [27] { + name: @ac:1:0 + } + IDENT [28] { + name: @it:1:0 + } + IDENT [29] { + name: @index0 + } + } + } + } + result: { + IDENT [30] { + name: @ac:1:0 + } + } + } + } + } + CALL [31] { + function: _==_ + args: { + IDENT [32] { + name: @index1 + } + IDENT [33] { + name: @index1 + } + } + } + } +} +Test case: INCLUSION_LIST +Source: 1 in [1,2,3] && 2 in [1,2,3] && 3 in [3, [1,2,3]] && 1 in [1,2,3] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + CALL [3] { + function: @in + args: { + CONSTANT [4] { value: 1 } + LIST [5] { + elements: { + CONSTANT [6] { value: 1 } + CONSTANT [7] { value: 2 } + CONSTANT [8] { value: 3 } + } + } + } + } + LIST [9] { + elements: { + CONSTANT [10] { value: 1 } + CONSTANT [11] { value: 2 } + CONSTANT [12] { value: 3 } + } + } + } + } + CALL [13] { + function: _&&_ + args: { + CALL [14] { + function: _&&_ + args: { + IDENT [15] { + name: @index0 + } + CALL [16] { + function: @in + args: { + CONSTANT [17] { value: 2 } + IDENT [18] { + name: @index1 + } + } + } + } + } + CALL [19] { + function: _&&_ + args: { + CALL [20] { + function: @in + args: { + CONSTANT [21] { value: 3 } + LIST [22] { + elements: { + CONSTANT [23] { value: 3 } + IDENT [24] { + name: @index1 + } + } + } + } + } + IDENT [25] { + name: @index0 + } + } + } + } + } + } +} +Test case: INCLUSION_MAP +Source: 2 in {'a': 1, 2: {true: false}, 3: {true: false}} +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + MAP [3] { + MAP_ENTRY [4] { + key: { + CONSTANT [5] { value: true } + } + value: { + CONSTANT [6] { value: false } + } + } + } + } + } + CALL [7] { + function: @in + args: { + CONSTANT [8] { value: 2 } + MAP [9] { + MAP_ENTRY [10] { + key: { + CONSTANT [11] { value: "a" } + } + value: { + CONSTANT [12] { value: 1 } + } + } + MAP_ENTRY [13] { + key: { + CONSTANT [14] { value: 2 } + } + value: { + IDENT [15] { + name: @index0 + } + } + } + MAP_ENTRY [16] { + key: { + CONSTANT [17] { value: 3 } + } + value: { + IDENT [18] { + name: @index0 + } + } + } + } + } + } + } +} +Test case: MACRO_ITER_VAR_NOT_REFERENCED +Source: [1,2].map(i, [1, 2].map(i, [3,4])) == [[[3, 4], [3, 4]], [[3, 4], [3, 4]]] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + LIST [4] { + elements: { + CONSTANT [5] { value: 3 } + CONSTANT [6] { value: 4 } + } + } + LIST [7] { + elements: { + CONSTANT [8] { value: 3 } + CONSTANT [9] { value: 4 } + } + } + } + } + LIST [10] { + elements: { + CONSTANT [11] { value: 1 } + CONSTANT [12] { value: 2 } + } + } + COMPREHENSION [13] { + iter_var: @it:0:0 + iter_range: { + IDENT [14] { + name: @index1 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [15] { + elements: { + } + } + } + loop_condition: { + CONSTANT [16] { value: true } + } + loop_step: { + CALL [17] { + function: _+_ + args: { + IDENT [18] { + name: @ac:0:0 + } + LIST [19] { + elements: { + LIST [20] { + elements: { + CONSTANT [21] { value: 3 } + CONSTANT [22] { value: 4 } + } + } + } + } + } + } + } + result: { + IDENT [23] { + name: @ac:0:0 + } + } + } + } + } + CALL [24] { + function: _==_ + args: { + COMPREHENSION [25] { + iter_var: @it:1:0 + iter_range: { + IDENT [26] { + name: @index1 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [27] { + elements: { + } + } + } + loop_condition: { + CONSTANT [28] { value: true } + } + loop_step: { + CALL [29] { + function: _+_ + args: { + IDENT [30] { + name: @ac:1:0 + } + LIST [31] { + elements: { + IDENT [32] { + name: @index2 + } + } + } + } + } + } + result: { + IDENT [33] { + name: @ac:1:0 + } + } + } + LIST [34] { + elements: { + IDENT [35] { + name: @index0 + } + IDENT [36] { + name: @index0 + } + } + } + } + } + } +} +Test case: MACRO_ITER_VAR2_NOT_REFERENCED +Source: [1,2].transformList(i, v, [1, 2].transformList(i, v, [3,4])) == [[[3, 4], [3, 4]], [[3, 4], [3, 4]]] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + LIST [4] { + elements: { + CONSTANT [5] { value: 3 } + CONSTANT [6] { value: 4 } + } + } + LIST [7] { + elements: { + CONSTANT [8] { value: 3 } + CONSTANT [9] { value: 4 } + } + } + } + } + LIST [10] { + elements: { + CONSTANT [11] { value: 1 } + CONSTANT [12] { value: 2 } + } + } + COMPREHENSION [13] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + IDENT [14] { + name: @index1 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [15] { + elements: { + } + } + } + loop_condition: { + CONSTANT [16] { value: true } + } + loop_step: { + CALL [17] { + function: _+_ + args: { + IDENT [18] { + name: @ac:0:0 + } + LIST [19] { + elements: { + LIST [20] { + elements: { + CONSTANT [21] { value: 3 } + CONSTANT [22] { value: 4 } + } + } + } + } + } + } + } + result: { + IDENT [23] { + name: @ac:0:0 + } + } + } + } + } + CALL [24] { + function: _==_ + args: { + COMPREHENSION [25] { + iter_var: @it:1:0 + iter_var2: @it2:1:0 + iter_range: { + IDENT [26] { + name: @index1 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [27] { + elements: { + } + } + } + loop_condition: { + CONSTANT [28] { value: true } + } + loop_step: { + CALL [29] { + function: _+_ + args: { + IDENT [30] { + name: @ac:1:0 + } + LIST [31] { + elements: { + IDENT [32] { + name: @index2 + } + } + } + } + } + } + result: { + IDENT [33] { + name: @ac:1:0 + } + } + } + LIST [34] { + elements: { + IDENT [35] { + name: @index0 + } + IDENT [36] { + name: @index0 + } + } + } + } + } + } +} +Test case: MACRO_SHADOWED_VARIABLE +Source: [x - 1 > 3 ? x - 1 : 5].exists(x, x - 1 > 3) || x - 1 > 3 +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + CALL [3] { + function: _>_ + args: { + CALL [4] { + function: _-_ + args: { + IDENT [5] { + name: x + } + CONSTANT [6] { value: 1 } + } + } + CONSTANT [7] { value: 3 } + } + } + } + } + CALL [8] { + function: _||_ + args: { + COMPREHENSION [9] { + iter_var: @it:0:0 + iter_range: { + LIST [10] { + elements: { + CALL [11] { + function: _?_:_ + args: { + IDENT [12] { + name: @index0 + } + CALL [13] { + function: _-_ + args: { + IDENT [14] { + name: x + } + CONSTANT [15] { value: 1 } + } + } + CONSTANT [16] { value: 5 } + } + } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [17] { value: false } + } + loop_condition: { + CALL [18] { + function: @not_strictly_false + args: { + CALL [19] { + function: !_ + args: { + IDENT [20] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [21] { + function: _||_ + args: { + IDENT [22] { + name: @ac:0:0 + } + CALL [23] { + function: _>_ + args: { + CALL [24] { + function: _-_ + args: { + IDENT [25] { + name: @it:0:0 + } + CONSTANT [26] { value: 1 } + } + } + CONSTANT [27] { value: 3 } + } + } + } + } + } + result: { + IDENT [28] { + name: @ac:0:0 + } + } + } + IDENT [29] { + name: @index0 + } + } + } + } +} +Test case: MACRO_SHADOWED_VARIABLE_2 +Source: ["foo", "bar"].map(x, [x + x, x + x]).map(x, [x + x, x + x]) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + CONSTANT [4] { value: "foo" } + CONSTANT [5] { value: "bar" } + } + } + } + } + COMPREHENSION [6] { + iter_var: @it:1:0 + iter_range: { + COMPREHENSION [7] { + iter_var: @it:0:0 + iter_range: { + IDENT [8] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [9] { + elements: { + } + } + } + loop_condition: { + CONSTANT [10] { value: true } + } + loop_step: { + CALL [11] { + function: _+_ + args: { + IDENT [12] { + name: @ac:0:0 + } + LIST [13] { + elements: { + LIST [14] { + elements: { + CALL [15] { + function: _+_ + args: { + IDENT [16] { + name: @it:0:0 + } + IDENT [17] { + name: @it:0:0 + } + } + } + CALL [18] { + function: _+_ + args: { + IDENT [19] { + name: @it:0:0 + } + IDENT [20] { + name: @it:0:0 + } + } + } + } + } + } + } + } + } + } + result: { + IDENT [21] { + name: @ac:0:0 + } + } + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [22] { + elements: { + } + } + } + loop_condition: { + CONSTANT [23] { value: true } + } + loop_step: { + CALL [24] { + function: _+_ + args: { + IDENT [25] { + name: @ac:1:0 + } + LIST [26] { + elements: { + LIST [27] { + elements: { + CALL [28] { + function: _+_ + args: { + IDENT [29] { + name: @it:1:0 + } + IDENT [30] { + name: @it:1:0 + } + } + } + CALL [31] { + function: _+_ + args: { + IDENT [32] { + name: @it:1:0 + } + IDENT [33] { + name: @it:1:0 + } + } + } + } } } } } - result: { - IDENT [32] { - name: @x0:0 - } - } } } - } - CALL [33] { - function: _==_ - args: { + result: { IDENT [34] { - name: @index5 - } - IDENT [35] { - name: @index3 + name: @ac:1:0 } } } } } -Test case: MACRO_SHADOWED_VARIABLE -Source: [x - 1 > 3 ? x - 1 : 5].exists(x, x - 1 > 3) || x - 1 > 3 +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 =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { - function: _-_ - args: { - IDENT [4] { - name: x - } - CONSTANT [5] { value: 1 } - } - } - CALL [6] { function: _>_ args: { - IDENT [7] { - name: @index0 + CALL [4] { + function: _-_ + args: { + CALL [5] { + function: _-_ + args: { + IDENT [6] { + name: x + } + IDENT [7] { + name: y + } + } + } + CONSTANT [8] { value: 1 } + } } - CONSTANT [8] { value: 3 } + CONSTANT [9] { value: 3 } } } - COMPREHENSION [9] { - iter_var: @c0:0 - iter_range: { - CREATE_LIST [10] { - elements: { - CALL [11] { - function: _?_:_ + LIST [10] { + elements: { + CALL [11] { + function: _?_:_ + args: { + IDENT [12] { + name: @index0 + } + CALL [13] { + function: _-_ args: { - IDENT [12] { - name: @index1 - } - IDENT [13] { - name: @index0 + CALL [14] { + function: _-_ + args: { + IDENT [15] { + name: x + } + IDENT [16] { + name: y + } + } } - CONSTANT [14] { value: 5 } + CONSTANT [17] { value: 1 } } } + CONSTANT [18] { value: 5 } } } } - accu_var: @x0:0 + } + } + } + CALL [19] { + function: _||_ + args: { + COMPREHENSION [20] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + IDENT [21] { + name: @index1 + } + } + accu_var: @ac:0:0 accu_init: { - CONSTANT [15] { value: false } + CONSTANT [22] { value: false } } loop_condition: { - CALL [16] { + CALL [23] { function: @not_strictly_false args: { - CALL [17] { + CALL [24] { function: !_ args: { - IDENT [18] { - name: @x0:0 + IDENT [25] { + name: @ac:0:0 } } } @@ -2296,139 +3817,110 @@ CALL [1] { } } loop_step: { - CALL [19] { + CALL [26] { function: _||_ args: { - IDENT [20] { - name: @x0:0 + IDENT [27] { + name: @ac:0:0 } - CALL [21] { + CALL [28] { function: _>_ args: { - CALL [22] { + CALL [29] { function: _-_ args: { - IDENT [23] { - name: @c0:0 + CALL [30] { + function: _-_ + args: { + IDENT [31] { + name: @it:0:0 + } + IDENT [32] { + name: @it2:0:0 + } + } } - CONSTANT [24] { value: 1 } + CONSTANT [33] { value: 1 } } } - CONSTANT [25] { value: 3 } + CONSTANT [34] { value: 3 } } } } } } result: { - IDENT [26] { - name: @x0:0 + IDENT [35] { + name: @ac:0:0 } } } - } - } - CALL [27] { - function: _||_ - args: { - IDENT [28] { - name: @index2 - } - IDENT [29] { - name: @index1 + IDENT [36] { + name: @index0 } } } } } -Test case: MACRO_SHADOWED_VARIABLE_2 -Source: ["foo", "bar"].map(x, [x + x, x + x]).map(x, [x + x, x + x]) +Test case: MACRO_SHADOWED_VARIABLE_COMP_V2_2 +Source: ["foo", "bar"].transformMap(x, y, [x + x, y + y]).transformMap(x, y, [x + x, y + y]) =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CALL [3] { - function: _+_ - args: { - IDENT [4] { - name: @c1:0 - } - IDENT [5] { - name: @c1:0 - } - } - } - CALL [6] { - function: _+_ - args: { - IDENT [7] { - name: @c0:0 - } - IDENT [8] { - name: @c0:0 - } - } - } - CALL [9] { - function: _+_ - args: { - IDENT [10] { - name: @x0:0 - } - CREATE_LIST [11] { - elements: { - CREATE_LIST [12] { - elements: { - IDENT [13] { - name: @index1 - } - IDENT [14] { - name: @index1 - } - } - } - } - } - } - } - COMPREHENSION [15] { - iter_var: @c1:0 + COMPREHENSION [3] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 iter_range: { - CREATE_LIST [16] { + LIST [4] { elements: { - CONSTANT [17] { value: "foo" } - CONSTANT [18] { value: "bar" } + CONSTANT [5] { value: "foo" } + CONSTANT [6] { value: "bar" } } } } - accu_var: @x1:0 + accu_var: @ac:0:0 accu_init: { - CREATE_LIST [19] { - elements: { - } + MAP [7] { + } } loop_condition: { - CONSTANT [20] { value: true } + CONSTANT [8] { value: true } } loop_step: { - CALL [21] { - function: _+_ + CALL [9] { + function: cel.@mapInsert args: { - IDENT [22] { - name: @x1:0 + IDENT [10] { + name: @ac:0:0 + } + IDENT [11] { + name: @it:0:0 } - CREATE_LIST [23] { + LIST [12] { elements: { - CREATE_LIST [24] { - elements: { - IDENT [25] { - name: @index0 + CALL [13] { + function: _+_ + args: { + IDENT [14] { + name: @it:0:0 } - IDENT [26] { - name: @index0 + IDENT [15] { + name: @it:0:0 + } + } + } + CALL [16] { + function: _+_ + args: { + IDENT [17] { + name: @it2:0:0 + } + IDENT [18] { + name: @it2:0:0 } } } @@ -2438,38 +3930,72 @@ CALL [1] { } } result: { - IDENT [27] { - name: @x1:0 + IDENT [19] { + name: @ac:0:0 } } } } } - COMPREHENSION [28] { - iter_var: @c0:0 + COMPREHENSION [20] { + iter_var: @it:1:0 + iter_var2: @it2:1:0 iter_range: { - IDENT [29] { - name: @index3 + IDENT [21] { + name: @index0 } } - accu_var: @x0:0 + accu_var: @ac:1:0 accu_init: { - CREATE_LIST [30] { - elements: { - } + MAP [22] { + } } loop_condition: { - CONSTANT [31] { value: true } + CONSTANT [23] { value: true } } loop_step: { - IDENT [32] { - name: @index2 + CALL [24] { + function: cel.@mapInsert + args: { + IDENT [25] { + name: @ac:1:0 + } + IDENT [26] { + name: @it:1:0 + } + LIST [27] { + elements: { + CALL [28] { + function: _+_ + args: { + IDENT [29] { + name: @it:1:0 + } + IDENT [30] { + name: @it:1:0 + } + } + } + CALL [31] { + function: _+_ + args: { + IDENT [32] { + name: @it2:1:0 + } + IDENT [33] { + name: @it2:1:0 + } + } + } + } + } + } } } result: { - IDENT [33] { - name: @x0:0 + IDENT [34] { + name: @ac:1:0 } } } @@ -2481,9 +4007,9 @@ Source: has({'a': true}.a) && {'a':true}['a'] CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_MAP [3] { + MAP [3] { MAP_ENTRY [4] { key: { CONSTANT [5] { value: "a" } @@ -2493,27 +4019,59 @@ CALL [1] { } } } - CALL [7] { + } + } + CALL [7] { + function: _&&_ + args: { + SELECT [8] { + IDENT [9] { + name: @index0 + }.a~presence_test + } + CALL [10] { function: _[_] args: { - IDENT [8] { + IDENT [11] { name: @index0 } - CONSTANT [9] { value: "a" } + CONSTANT [12] { value: "a" } } } } } - CALL [10] { + } +} +Test case: PRESENCE_TEST_2 +Source: has({'a': true}.a) && has({'a': true}.a) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + SELECT [3] { + MAP [4] { + MAP_ENTRY [5] { + key: { + CONSTANT [6] { value: "a" } + } + value: { + CONSTANT [7] { value: true } + } + } + }.a~presence_test + } + } + } + CALL [8] { function: _&&_ args: { - SELECT [11] { - IDENT [12] { - name: @index0 - }.a~presence_test + IDENT [9] { + name: @index0 } - IDENT [13] { - name: @index1 + IDENT [10] { + name: @index0 } } } @@ -2525,40 +4083,37 @@ Source: (has(msg.oneof_type.payload) ? msg.oneof_type.payload.single_int64 : 0) CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { name: msg }.oneof_type } - CALL [5] { + } + } + CALL [5] { + function: _==_ + args: { + CALL [6] { function: _?_:_ args: { - SELECT [6] { - IDENT [7] { + SELECT [7] { + IDENT [8] { name: @index0 }.payload~presence_test } - SELECT [8] { - SELECT [9] { - IDENT [10] { + SELECT [9] { + SELECT [10] { + IDENT [11] { name: @index0 }.payload }.single_int64 } - CONSTANT [11] { value: 0 } + CONSTANT [12] { value: 0 } } } - } - } - CALL [12] { - function: _==_ - args: { - IDENT [13] { - name: @index1 - } - CONSTANT [14] { value: 10 } + CONSTANT [13] { value: 10 } } } } @@ -2569,54 +4124,47 @@ Source: (has(msg.oneof_type.payload) ? msg.oneof_type.payload.single_int64 : msg CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload }.single_int64 } - CALL [9] { + } + } + CALL [7] { + function: _==_ + args: { + CALL [8] { function: _?_:_ args: { - SELECT [10] { - IDENT [11] { - name: @index0 + SELECT [9] { + SELECT [10] { + IDENT [11] { + name: msg + }.oneof_type }.payload~presence_test } IDENT [12] { - name: @index2 + name: @index0 } CALL [13] { function: _*_ args: { IDENT [14] { - name: @index2 + name: @index0 } CONSTANT [15] { value: 0 } } } } } - } - } - CALL [16] { - function: _==_ - args: { - IDENT [17] { - name: @index3 - } - CONSTANT [18] { value: 10 } + CONSTANT [16] { value: 10 } } } } @@ -2627,39 +4175,37 @@ Source: (has(msg.oneof_type.payload.single_int64) ? msg.oneof_type.payload.singl CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload }.single_int64 } - CALL [9] { + CALL [7] { function: _?_:_ args: { - SELECT [10] { - IDENT [11] { - name: @index1 + SELECT [8] { + SELECT [9] { + SELECT [10] { + IDENT [11] { + name: msg + }.oneof_type + }.payload }.single_int64~presence_test } IDENT [12] { - name: @index2 + name: @index0 } CALL [13] { function: _*_ args: { IDENT [14] { - name: @index2 + name: @index0 } CONSTANT [15] { value: 0 } } @@ -2672,7 +4218,7 @@ CALL [1] { function: _==_ args: { IDENT [17] { - name: @index3 + name: @index1 } CONSTANT [18] { value: 10 } } @@ -2685,75 +4231,46 @@ Source: (has(msg.oneof_type) && has(msg.oneof_type.payload) && has(msg.oneof_typ CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload }.map_string_string } - CALL [9] { - function: _?_:_ - args: { - CALL [10] { - function: _&&_ - args: { - SELECT [11] { - IDENT [12] { - name: @index1 - }.map_string_string~presence_test - } - SELECT [13] { - IDENT [14] { - name: @index2 - }.key~presence_test - } - } - } - CALL [15] { - function: _==_ - args: { - SELECT [16] { - IDENT [17] { - name: @index2 - }.key - } - CONSTANT [18] { value: "A" } - } - } - CONSTANT [19] { value: false } - } + SELECT [7] { + SELECT [8] { + IDENT [9] { + name: msg + }.oneof_type + }.payload } - CALL [20] { + CALL [10] { function: _&&_ args: { - CALL [21] { + CALL [11] { function: _&&_ args: { - SELECT [22] { - IDENT [23] { + SELECT [12] { + IDENT [13] { name: msg }.oneof_type~presence_test } - SELECT [24] { - IDENT [25] { - name: @index0 + SELECT [14] { + SELECT [15] { + IDENT [16] { + name: msg + }.oneof_type }.payload~presence_test } } } - SELECT [26] { - IDENT [27] { + SELECT [17] { + IDENT [18] { name: @index1 }.single_int64~presence_test } @@ -2761,16 +4278,45 @@ CALL [1] { } } } - CALL [28] { + CALL [19] { function: _?_:_ args: { - IDENT [29] { - name: @index4 + IDENT [20] { + name: @index2 } - IDENT [30] { - name: @index3 + CALL [21] { + function: _?_:_ + args: { + CALL [22] { + function: _&&_ + args: { + SELECT [23] { + IDENT [24] { + name: @index1 + }.map_string_string~presence_test + } + SELECT [25] { + IDENT [26] { + name: @index0 + }.key~presence_test + } + } + } + CALL [27] { + function: _==_ + args: { + SELECT [28] { + IDENT [29] { + name: @index0 + }.key + } + CONSTANT [30] { value: "A" } + } + } + CONSTANT [31] { value: false } + } } - CONSTANT [31] { value: false } + CONSTANT [32] { value: false } } } } @@ -2781,46 +4327,51 @@ Source: [10, ?optional.none(), [?optional.none(), ?opt_x], [?optional.none(), ?o CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CALL [3] { - function: optional.none - args: { - } - } - CREATE_LIST [4] { + LIST [3] { elements: { - IDENT [5] { - name: @index0 + CALL [4] { + function: optional.none + args: { + } } - IDENT [6] { + IDENT [5] { name: opt_x } } optional_indices: [0, 1] } - CREATE_LIST [7] { + LIST [6] { elements: { - CONSTANT [8] { value: 5 } + CONSTANT [7] { value: 5 } } } - CREATE_LIST [9] { + } + } + CALL [8] { + function: _==_ + args: { + LIST [9] { elements: { CONSTANT [10] { value: 10 } - IDENT [11] { - name: @index2 + CALL [11] { + function: optional.none + args: { + } } IDENT [12] { - name: @index2 + name: @index0 + } + IDENT [13] { + name: @index0 } } + optional_indices: [0] } - CREATE_LIST [13] { + LIST [14] { elements: { - CONSTANT [14] { value: 10 } - IDENT [15] { - name: @index0 - } + CONSTANT [15] { value: 10 } IDENT [16] { name: @index1 } @@ -2828,18 +4379,6 @@ CALL [1] { name: @index1 } } - optional_indices: [0] - } - } - } - CALL [18] { - function: _==_ - args: { - IDENT [19] { - name: @index4 - } - IDENT [20] { - name: @index3 } } } @@ -2851,56 +4390,47 @@ Source: {?'hello': optional.of('hello')}['hello'] + {?'hello': optional.of('hell CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { - function: optional.of - args: { - CONSTANT [4] { value: "hello" } - } - } - CREATE_MAP [5] { - MAP_ENTRY [6] { - key: { - CONSTANT [7] { value: "hello" } - } - optional_entry: true - value: { - IDENT [8] { - name: @index0 - } - } - } - } - CALL [9] { function: _[_] args: { - IDENT [10] { - name: @index1 + MAP [4] { + MAP_ENTRY [5] { + key: { + CONSTANT [6] { value: "hello" } + } + optional_entry: true + value: { + CALL [7] { + function: optional.of + args: { + CONSTANT [8] { value: "hello" } + } + } + } + } } - CONSTANT [11] { value: "hello" } + CONSTANT [9] { value: "hello" } } } - CALL [12] { + } + } + CALL [10] { + function: _==_ + args: { + CALL [11] { function: _+_ args: { - IDENT [13] { - name: @index2 + IDENT [12] { + name: @index0 } - IDENT [14] { - name: @index2 + IDENT [13] { + name: @index0 } } } - } - } - CALL [15] { - function: _==_ - args: { - IDENT [16] { - name: @index3 - } - CONSTANT [17] { value: "hellohello" } + CONSTANT [14] { value: "hellohello" } } } } @@ -2911,9 +4441,9 @@ Source: {?'key': optional.of('test')}[?'bogus'].or({'key': 'test'}[?'bogus']).or CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_MAP [3] { + MAP [3] { MAP_ENTRY [4] { key: { CONSTANT [5] { value: "key" } @@ -2924,145 +4454,130 @@ CALL [1] { } } CALL [7] { - function: _[_] - args: { - IDENT [8] { - name: @index0 - } - CONSTANT [9] { value: "key" } - } - } - CALL [10] { function: or target: { - CALL [11] { + CALL [8] { function: _[?_] args: { - CREATE_MAP [12] { - MAP_ENTRY [13] { + MAP [9] { + MAP_ENTRY [10] { key: { - CONSTANT [14] { value: "key" } + CONSTANT [11] { value: "key" } } optional_entry: true value: { - CALL [15] { + CALL [12] { function: optional.of args: { - CONSTANT [16] { value: "test" } + CONSTANT [13] { value: "test" } } } } } } - CONSTANT [17] { value: "bogus" } + CONSTANT [14] { value: "bogus" } } } } args: { - CALL [18] { + CALL [15] { function: _[?_] args: { - IDENT [19] { + IDENT [16] { name: @index0 } - CONSTANT [20] { value: "bogus" } + CONSTANT [17] { value: "bogus" } } } } } - CALL [21] { + } + } + CALL [18] { + function: _==_ + args: { + CALL [19] { function: orValue target: { - IDENT [22] { - name: @index2 + IDENT [20] { + name: @index1 } } args: { - IDENT [23] { - name: @index1 + CALL [21] { + function: _[_] + args: { + IDENT [22] { + name: @index0 + } + CONSTANT [23] { value: "key" } + } } } } - } - } - CALL [24] { - function: _==_ - args: { - IDENT [25] { - name: @index3 - } - CONSTANT [26] { value: "test" } + CONSTANT [24] { value: "test" } } } } } Test case: OPTIONAL_MESSAGE -Source: TestAllTypes{?single_int64: optional.ofNonZeroValue(1), ?single_int32: optional.of(4)}.single_int32 + TestAllTypes{?single_int64: optional.ofNonZeroValue(1), ?single_int32: optional.of(4)}.single_int64 == 5 -=====> -CALL [1] { - function: cel.@block - args: { - CREATE_LIST [2] { - elements: { - CALL [3] { - function: optional.ofNonZeroValue - args: { - CONSTANT [4] { value: 1 } - } - } - CALL [5] { - function: optional.of - args: { - CONSTANT [6] { value: 4 } - } - } - CREATE_STRUCT [7] { - name: TestAllTypes +Source: TestAllTypes{?single_int64: optional.ofNonZeroValue(1), ?single_int32: optional.of(4)}.single_int32 + TestAllTypes{?single_int64: optional.ofNonZeroValue(1), ?single_int32: optional.of(4)}.single_int64 == 5 +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + STRUCT [3] { + name: cel.expr.conformance.proto3.TestAllTypes entries: { - ENTRY [8] { + ENTRY [4] { field_key: single_int64 optional_entry: true value: { - IDENT [9] { - name: @index0 + CALL [5] { + function: optional.ofNonZeroValue + args: { + CONSTANT [6] { value: 1 } + } } } } - ENTRY [10] { + ENTRY [7] { field_key: single_int32 optional_entry: true value: { - IDENT [11] { - name: @index1 + CALL [8] { + function: optional.of + args: { + CONSTANT [9] { value: 4 } + } } } } } } - CALL [12] { + } + } + CALL [10] { + function: _==_ + args: { + CALL [11] { function: _+_ args: { - SELECT [13] { - IDENT [14] { - name: @index2 + SELECT [12] { + IDENT [13] { + name: @index0 }.single_int32 } - SELECT [15] { - IDENT [16] { - name: @index2 + SELECT [14] { + IDENT [15] { + name: @index0 }.single_int64 } } } - } - } - CALL [17] { - function: _==_ - args: { - IDENT [18] { - name: @index3 - } - CONSTANT [19] { value: 5 } + CONSTANT [16] { value: 5 } } } } @@ -3073,63 +4588,51 @@ Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('h' + 'e' + 'l' + 'l' + CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { function: _+_ args: { - CONSTANT [4] { value: "h" } - CONSTANT [5] { value: "e" } - } - } - CALL [6] { - function: _+_ - args: { - IDENT [7] { - name: @index0 - } - CONSTANT [8] { value: "l" } - } - } - CALL [9] { - function: _+_ - args: { - IDENT [10] { - name: @index1 - } - CONSTANT [11] { value: "l" } - } - } - CALL [12] { - function: _+_ - args: { - IDENT [13] { - name: @index2 - } - CONSTANT [14] { value: "o" } - } - } - CALL [15] { - function: _+_ - args: { - IDENT [16] { - name: @index3 + CALL [4] { + function: _+_ + args: { + CALL [5] { + function: _+_ + args: { + CALL [6] { + function: _+_ + args: { + CONSTANT [7] { value: "h" } + CONSTANT [8] { value: "e" } + } + } + CONSTANT [9] { value: "l" } + } + } + CONSTANT [10] { value: "l" } + } } - CONSTANT [17] { value: " world" } + CONSTANT [11] { value: "o" } } } } } - CALL [18] { + CALL [12] { function: matches target: { - IDENT [19] { - name: @index4 + CALL [13] { + function: _+_ + args: { + IDENT [14] { + name: @index0 + } + CONSTANT [15] { value: " world" } + } } } args: { - IDENT [20] { - name: @index3 + IDENT [16] { + name: @index0 } } } @@ -3141,7 +4644,7 @@ Source: 'hello world'.matches('h' + 'e' + 'l' + 'l' + 'o') CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { function: _+_ @@ -3189,7 +4692,7 @@ Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('hello') CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { function: _+_ @@ -3216,26 +4719,23 @@ CALL [1] { CONSTANT [11] { value: "o" } } } - CALL [12] { + } + } + CALL [12] { + function: matches + target: { + CALL [13] { function: _+_ args: { - IDENT [13] { + IDENT [14] { name: @index0 } - CONSTANT [14] { value: " world" } + CONSTANT [15] { value: " world" } } } } - } - CALL [15] { - function: matches - target: { - IDENT [16] { - name: @index1 - } - } args: { - CONSTANT [17] { value: "hello" } + CONSTANT [16] { value: "hello" } } } } @@ -3246,7 +4746,7 @@ Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('w' + 'o' + 'r' + 'l' + CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { function: _+_ @@ -3260,17 +4760,17 @@ CALL [1] { CALL [6] { function: _+_ args: { - CONSTANT [7] { value: "w" } - CONSTANT [8] { value: "o" } + CONSTANT [7] { value: "h" } + CONSTANT [8] { value: "e" } } } - CONSTANT [9] { value: "r" } + CONSTANT [9] { value: "l" } } } CONSTANT [10] { value: "l" } } } - CONSTANT [11] { value: "d" } + CONSTANT [11] { value: "o" } } } CALL [12] { @@ -3285,40 +4785,37 @@ CALL [1] { CALL [15] { function: _+_ args: { - CONSTANT [16] { value: "h" } - CONSTANT [17] { value: "e" } + CONSTANT [16] { value: "w" } + CONSTANT [17] { value: "o" } } } - CONSTANT [18] { value: "l" } + CONSTANT [18] { value: "r" } } } CONSTANT [19] { value: "l" } } } - CONSTANT [20] { value: "o" } - } - } - CALL [21] { - function: _+_ - args: { - IDENT [22] { - name: @index1 - } - CONSTANT [23] { value: " world" } + CONSTANT [20] { value: "d" } } } } } - CALL [24] { + CALL [21] { function: matches target: { - IDENT [25] { - name: @index2 + CALL [22] { + function: _+_ + args: { + IDENT [23] { + name: @index0 + } + CONSTANT [24] { value: " world" } + } } } args: { - IDENT [26] { - name: @index0 + IDENT [25] { + name: @index1 } } } @@ -3330,77 +4827,69 @@ Source: non_pure_custom_func(msg.oneof_type.payload.single_int64) + non_pure_cus CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 - }.single_int64 - } - SELECT [9] { - IDENT [10] { - name: msg + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload }.single_int64 } - SELECT [11] { - IDENT [12] { - name: @index1 - }.single_int32 - } } } - CALL [13] { + CALL [7] { function: _+_ args: { - CALL [14] { + CALL [8] { function: _+_ args: { - CALL [15] { + CALL [9] { function: _+_ args: { - CALL [16] { + CALL [10] { function: non_pure_custom_func args: { - IDENT [17] { - name: @index2 + IDENT [11] { + name: @index0 } } } - CALL [18] { + CALL [12] { function: non_pure_custom_func args: { - IDENT [19] { - name: @index4 + SELECT [13] { + SELECT [14] { + SELECT [15] { + IDENT [16] { + name: msg + }.oneof_type + }.payload + }.single_int32 } } } } } - CALL [20] { + CALL [17] { function: non_pure_custom_func args: { - IDENT [21] { - name: @index2 + IDENT [18] { + name: @index0 } } } } } - CALL [22] { + CALL [19] { function: non_pure_custom_func args: { - IDENT [23] { - name: @index3 + SELECT [20] { + IDENT [21] { + name: msg + }.single_int64 } } } @@ -3414,41 +4903,41 @@ Source: pure_custom_func(msg.oneof_type.payload.single_int64) + pure_custom_func CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 - }.single_int64 - } - CALL [9] { + CALL [3] { function: pure_custom_func args: { - IDENT [10] { - name: @index2 + SELECT [4] { + SELECT [5] { + SELECT [6] { + IDENT [7] { + name: msg + }.oneof_type + }.payload + }.single_int64 } } } - CALL [11] { + CALL [8] { function: pure_custom_func args: { - SELECT [12] { - IDENT [13] { - name: msg - }.single_int64 + SELECT [9] { + SELECT [10] { + SELECT [11] { + IDENT [12] { + name: msg + }.oneof_type + }.payload + }.single_int32 } } } + } + } + CALL [13] { + function: _+_ + args: { CALL [14] { function: _+_ args: { @@ -3456,37 +4945,29 @@ CALL [1] { function: _+_ args: { IDENT [16] { - name: @index3 + name: @index0 } - CALL [17] { - function: pure_custom_func - args: { - SELECT [18] { - IDENT [19] { - name: @index1 - }.single_int32 - } - } + IDENT [17] { + name: @index1 } } } - IDENT [20] { - name: @index3 + IDENT [18] { + name: @index0 } } } - } - } - CALL [21] { - function: _+_ - args: { - IDENT [22] { - name: @index5 - } - IDENT [23] { - name: @index4 + CALL [19] { + function: pure_custom_func + args: { + SELECT [20] { + IDENT [21] { + name: msg + }.single_int64 + } + } } } } } -} +} \ No newline at end of file diff --git a/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_5.baseline b/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_5.baseline index d5d799e1f..c7ab9e404 100644 --- a/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_5.baseline +++ b/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_5.baseline @@ -4,22 +4,24 @@ Source: size([1,2]) + size([1,2]) + 1 == 5 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - CONSTANT [5] { value: 2 } - } - } - CALL [6] { + CALL [3] { function: size args: { - IDENT [7] { - name: @index0 + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + CONSTANT [6] { value: 2 } + } } } } + } + } + CALL [7] { + function: _==_ + args: { CALL [8] { function: _+_ args: { @@ -27,25 +29,17 @@ CALL [1] { function: _+_ args: { IDENT [10] { - name: @index1 + name: @index0 } IDENT [11] { - name: @index1 + name: @index0 } } } CONSTANT [12] { value: 1 } } } - } - } - CALL [13] { - function: _==_ - args: { - IDENT [14] { - name: @index2 - } - CONSTANT [15] { value: 5 } + CONSTANT [13] { value: 5 } } } } @@ -56,22 +50,24 @@ Source: 2 + size([1,2]) + size([1,2]) + 1 == 7 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - CONSTANT [5] { value: 2 } - } - } - CALL [6] { + CALL [3] { function: size args: { - IDENT [7] { - name: @index0 + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + CONSTANT [6] { value: 2 } + } } } } + } + } + CALL [7] { + function: _==_ + args: { CALL [8] { function: _+_ args: { @@ -83,27 +79,19 @@ CALL [1] { args: { CONSTANT [11] { value: 2 } IDENT [12] { - name: @index1 + name: @index0 } } } IDENT [13] { - name: @index1 + name: @index0 } } } CONSTANT [14] { value: 1 } } } - } - } - CALL [15] { - function: _==_ - args: { - IDENT [16] { - name: @index2 - } - CONSTANT [17] { value: 7 } + CONSTANT [15] { value: 7 } } } } @@ -114,71 +102,62 @@ Source: size([0]) + size([0]) + size([1,2]) + size([1,2]) == 6 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 0 } - } - } - CALL [5] { + CALL [3] { function: size args: { - IDENT [6] { - name: @index0 + LIST [4] { + elements: { + CONSTANT [5] { value: 0 } + } } } } - CREATE_LIST [7] { - elements: { - CONSTANT [8] { value: 1 } - CONSTANT [9] { value: 2 } - } - } - CALL [10] { + CALL [6] { function: size args: { - IDENT [11] { - name: @index2 + LIST [7] { + elements: { + CONSTANT [8] { value: 1 } + CONSTANT [9] { value: 2 } + } } } } - CALL [12] { + } + } + CALL [10] { + function: _==_ + args: { + CALL [11] { function: _+_ args: { - CALL [13] { + CALL [12] { function: _+_ args: { - CALL [14] { + CALL [13] { function: _+_ args: { - IDENT [15] { - name: @index1 + IDENT [14] { + name: @index0 } - IDENT [16] { - name: @index1 + IDENT [15] { + name: @index0 } } } - IDENT [17] { - name: @index3 + IDENT [16] { + name: @index1 } } } - IDENT [18] { - name: @index3 + IDENT [17] { + name: @index1 } } } - } - } - CALL [19] { - function: _==_ - args: { - IDENT [20] { - name: @index4 - } - CONSTANT [21] { value: 6 } + CONSTANT [18] { value: 6 } } } } @@ -189,111 +168,99 @@ Source: 5 + size([0]) + size([0]) + size([1,2]) + size([1,2]) + size([1,2,3]) + CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 0 } - } - } - CALL [5] { + CALL [3] { function: size args: { - IDENT [6] { - name: @index0 + LIST [4] { + elements: { + CONSTANT [5] { value: 0 } + } } } } - CREATE_LIST [7] { - elements: { - CONSTANT [8] { value: 1 } - CONSTANT [9] { value: 2 } - } - } - CALL [10] { + CALL [6] { function: size args: { - IDENT [11] { - name: @index2 + LIST [7] { + elements: { + CONSTANT [8] { value: 1 } + CONSTANT [9] { value: 2 } + } } } } - CREATE_LIST [12] { - elements: { - CONSTANT [13] { value: 1 } - CONSTANT [14] { value: 2 } - CONSTANT [15] { value: 3 } - } - } - CALL [16] { + CALL [10] { function: size args: { - IDENT [17] { - name: @index4 + LIST [11] { + elements: { + CONSTANT [12] { value: 1 } + CONSTANT [13] { value: 2 } + CONSTANT [14] { value: 3 } + } } } } - CALL [18] { + CALL [15] { function: _+_ args: { - CALL [19] { + CALL [16] { function: _+_ args: { - CALL [20] { + CALL [17] { function: _+_ args: { - CALL [21] { + CALL [18] { function: _+_ args: { - CALL [22] { + CALL [19] { function: _+_ args: { - CONSTANT [23] { value: 5 } - IDENT [24] { - name: @index1 + CONSTANT [20] { value: 5 } + IDENT [21] { + name: @index0 } } } - IDENT [25] { - name: @index1 + IDENT [22] { + name: @index0 } } } - IDENT [26] { - name: @index3 + IDENT [23] { + name: @index1 } } } - IDENT [27] { - name: @index3 + IDENT [24] { + name: @index1 } } } - IDENT [28] { - name: @index5 + IDENT [25] { + name: @index2 } } } - CALL [29] { + } + } + CALL [26] { + function: _==_ + args: { + CALL [27] { function: _+_ args: { - IDENT [30] { - name: @index6 + IDENT [28] { + name: @index3 } - IDENT [31] { - name: @index5 + IDENT [29] { + name: @index2 } } } - } - } - CALL [32] { - function: _==_ - args: { - IDENT [33] { - name: @index7 - } - CONSTANT [34] { value: 17 } + CONSTANT [30] { value: 17 } } } } @@ -304,146 +271,106 @@ Source: timestamp(int(timestamp(1000000000))).getFullYear() + timestamp(int(time CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { - function: timestamp - args: { - CONSTANT [4] { value: 1000000000 } - } - } - CALL [5] { - function: int - args: { - IDENT [6] { - name: @index0 + function: getFullYear + target: { + CALL [4] { + function: timestamp + args: { + CALL [5] { + function: int + args: { + CALL [6] { + function: timestamp + args: { + CONSTANT [7] { value: 1000000000 } + } + } + } + } + } } } - } - CALL [7] { - function: timestamp args: { - IDENT [8] { - name: @index1 - } } } - CALL [9] { + CALL [8] { function: getFullYear target: { - IDENT [10] { - name: @index2 + CALL [9] { + function: timestamp + args: { + CALL [10] { + function: int + args: { + CALL [11] { + function: timestamp + args: { + CONSTANT [12] { value: 200 } + } + } + } + } + } } } args: { } } - CALL [11] { - function: timestamp - args: { - CONSTANT [12] { value: 50 } - } - } CALL [13] { - function: int - args: { - IDENT [14] { - name: @index4 - } - } - } - CALL [15] { function: timestamp args: { - IDENT [16] { - name: @index5 + CALL [14] { + function: int + args: { + CALL [15] { + function: timestamp + args: { + CONSTANT [16] { value: 50 } + } + } + } } } } CALL [17] { function: timestamp args: { - CONSTANT [18] { value: 200 } - } - } - CALL [19] { - function: int - args: { - IDENT [20] { - name: @index7 + CALL [18] { + function: int + args: { + CALL [19] { + function: timestamp + args: { + CONSTANT [20] { value: 75 } + } + } + } } } } CALL [21] { - function: timestamp - args: { - IDENT [22] { - name: @index8 - } - } - } - CALL [23] { - function: getFullYear - target: { - IDENT [24] { - name: @index9 - } - } - args: { - } - } - CALL [25] { - function: timestamp - args: { - CONSTANT [26] { value: 75 } - } - } - CALL [27] { - function: int - args: { - IDENT [28] { - name: @index11 - } - } - } - CALL [29] { - function: timestamp + function: _+_ args: { - IDENT [30] { - name: @index12 - } - } - } - CALL [31] { - function: getMinutes - target: { - IDENT [32] { - name: @index13 - } - } - args: { - } - } - CALL [33] { - function: _+_ - args: { - CALL [34] { + CALL [22] { function: _+_ args: { - CALL [35] { + CALL [23] { function: _+_ args: { - CALL [36] { + CALL [24] { function: _+_ args: { - IDENT [37] { - name: @index3 + IDENT [25] { + name: @index0 } - CALL [38] { + CALL [26] { function: getFullYear target: { - IDENT [39] { - name: @index13 + IDENT [27] { + name: @index3 } } args: { @@ -451,11 +378,11 @@ CALL [1] { } } } - CALL [40] { + CALL [28] { function: getFullYear target: { - IDENT [41] { - name: @index6 + IDENT [29] { + name: @index2 } } args: { @@ -463,16 +390,16 @@ CALL [1] { } } } - IDENT [42] { - name: @index3 + IDENT [30] { + name: @index0 } } } - CALL [43] { + CALL [31] { function: getSeconds target: { - IDENT [44] { - name: @index6 + IDENT [32] { + name: @index2 } } args: { @@ -480,50 +407,54 @@ CALL [1] { } } } - CALL [45] { + } + } + CALL [33] { + function: _==_ + args: { + CALL [34] { function: _+_ args: { - CALL [46] { + CALL [35] { function: _+_ args: { - CALL [47] { + CALL [36] { function: _+_ args: { - CALL [48] { + CALL [37] { function: _+_ args: { - IDENT [49] { - name: @index15 + IDENT [38] { + name: @index4 } - IDENT [50] { - name: @index10 + IDENT [39] { + name: @index1 } } } - IDENT [51] { - name: @index10 + IDENT [40] { + name: @index1 } } } - IDENT [52] { - name: @index14 + CALL [41] { + function: getMinutes + target: { + IDENT [42] { + name: @index3 + } + } + args: { + } } } } - IDENT [53] { - name: @index3 + IDENT [43] { + name: @index0 } } } - } - } - CALL [54] { - function: _==_ - args: { - IDENT [55] { - name: @index16 - } - CONSTANT [56] { value: 13934 } + CONSTANT [44] { value: 13934 } } } } @@ -534,55 +465,49 @@ Source: {"a": 2}["a"] + {"a": 2}["a"] * {"a": 2}["a"] == 6 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_MAP [3] { - MAP_ENTRY [4] { - key: { - CONSTANT [5] { value: "a" } - } - value: { - CONSTANT [6] { value: 2 } - } - } - } - CALL [7] { + CALL [3] { function: _[_] args: { - IDENT [8] { - name: @index0 + MAP [4] { + MAP_ENTRY [5] { + key: { + CONSTANT [6] { value: "a" } + } + value: { + CONSTANT [7] { value: 2 } + } + } } - CONSTANT [9] { value: "a" } + CONSTANT [8] { value: "a" } } } + } + } + CALL [9] { + function: _==_ + args: { CALL [10] { function: _+_ args: { IDENT [11] { - name: @index1 + name: @index0 } CALL [12] { function: _*_ args: { IDENT [13] { - name: @index1 + name: @index0 } IDENT [14] { - name: @index1 + name: @index0 } } } } } - } - } - CALL [15] { - function: _==_ - args: { - IDENT [16] { - name: @index2 - } - CONSTANT [17] { value: 6 } + CONSTANT [15] { value: 6 } } } } @@ -593,56 +518,53 @@ Source: {'a': {'b': 1}, 'c': {'b': 1}, 'd': {'e': {'b': 1}}, 'e': {'e': {'b': 1} CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_MAP [3] { + MAP [3] { MAP_ENTRY [4] { key: { - CONSTANT [5] { value: "b" } + CONSTANT [5] { value: "e" } } value: { - CONSTANT [6] { value: 1 } + MAP [6] { + MAP_ENTRY [7] { + key: { + CONSTANT [8] { value: "b" } + } + value: { + CONSTANT [9] { value: 1 } + } + } + } } } } - CREATE_MAP [7] { - MAP_ENTRY [8] { + MAP [10] { + MAP_ENTRY [11] { key: { - CONSTANT [9] { value: "e" } + CONSTANT [12] { value: "b" } } value: { - IDENT [10] { - name: @index0 - } + CONSTANT [13] { value: 1 } } } } } } - CREATE_MAP [11] { - MAP_ENTRY [12] { - key: { - CONSTANT [13] { value: "a" } - } - value: { - IDENT [14] { - name: @index0 - } - } - } + MAP [14] { MAP_ENTRY [15] { key: { - CONSTANT [16] { value: "c" } + CONSTANT [16] { value: "a" } } value: { IDENT [17] { - name: @index0 + name: @index1 } } } MAP_ENTRY [18] { key: { - CONSTANT [19] { value: "d" } + CONSTANT [19] { value: "c" } } value: { IDENT [20] { @@ -652,11 +574,21 @@ CALL [1] { } MAP_ENTRY [21] { key: { - CONSTANT [22] { value: "e" } + CONSTANT [22] { value: "d" } } value: { IDENT [23] { - name: @index1 + name: @index0 + } + } + } + MAP_ENTRY [24] { + key: { + CONSTANT [25] { value: "e" } + } + value: { + IDENT [26] { + name: @index0 } } } @@ -669,9 +601,9 @@ Source: [1, [1,2,3,4], 2, [1,2,3,4], 5, [1,2,3,4], 7, [[1,2], [1,2,3,4]], [1,2]] CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { + LIST [3] { elements: { CONSTANT [4] { value: 1 } CONSTANT [5] { value: 2 } @@ -679,43 +611,40 @@ CALL [1] { CONSTANT [7] { value: 4 } } } - CREATE_LIST [8] { + LIST [8] { elements: { CONSTANT [9] { value: 1 } CONSTANT [10] { value: 2 } } } - CREATE_LIST [11] { - elements: { - IDENT [12] { - name: @index1 - } - IDENT [13] { - name: @index0 - } - } - } } } - CREATE_LIST [14] { + LIST [11] { elements: { - CONSTANT [15] { value: 1 } - IDENT [16] { + CONSTANT [12] { value: 1 } + IDENT [13] { name: @index0 } - CONSTANT [17] { value: 2 } - IDENT [18] { + CONSTANT [14] { value: 2 } + IDENT [15] { name: @index0 } - CONSTANT [19] { value: 5 } - IDENT [20] { + CONSTANT [16] { value: 5 } + IDENT [17] { name: @index0 } - CONSTANT [21] { value: 7 } - IDENT [22] { - name: @index2 + CONSTANT [18] { value: 7 } + LIST [19] { + elements: { + IDENT [20] { + name: @index1 + } + IDENT [21] { + name: @index0 + } + } } - IDENT [23] { + IDENT [22] { name: @index1 } } @@ -728,33 +657,30 @@ Source: msg.single_int64 + msg.single_int64 == 6 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { name: msg }.single_int64 } - CALL [5] { + } + } + CALL [5] { + function: _==_ + args: { + CALL [6] { function: _+_ args: { - IDENT [6] { + IDENT [7] { name: @index0 } - IDENT [7] { + IDENT [8] { name: @index0 } } } - } - } - CALL [8] { - function: _==_ - args: { - IDENT [9] { - name: @index1 - } - CONSTANT [10] { value: 6 } + CONSTANT [9] { value: 6 } } } } @@ -765,61 +691,62 @@ Source: msg.oneof_type.payload.single_int64 + msg.oneof_type.payload.single_int3 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.single_int64 } SELECT [7] { - IDENT [8] { - name: @index1 - }.single_int64 + SELECT [8] { + IDENT [9] { + name: msg + }.oneof_type + }.payload } - CALL [9] { + CALL [10] { function: _+_ args: { - CALL [10] { + CALL [11] { function: _+_ args: { - CALL [11] { + CALL [12] { function: _+_ args: { - CALL [12] { + CALL [13] { function: _+_ args: { - IDENT [13] { - name: @index2 + IDENT [14] { + name: @index0 } - SELECT [14] { - IDENT [15] { + SELECT [15] { + IDENT [16] { name: @index1 }.single_int32 } } } - IDENT [16] { - name: @index2 + IDENT [17] { + name: @index0 } } } - SELECT [17] { - IDENT [18] { + SELECT [18] { + IDENT [19] { name: msg }.single_int64 } } } - SELECT [19] { - SELECT [20] { - SELECT [21] { - IDENT [22] { + SELECT [20] { + SELECT [21] { + SELECT [22] { + IDENT [23] { name: @index1 }.oneof_type }.payload @@ -829,13 +756,13 @@ CALL [1] { } } } - CALL [23] { + CALL [24] { function: _==_ args: { - IDENT [24] { - name: @index3 + IDENT [25] { + name: @index2 } - CONSTANT [25] { value: 31 } + CONSTANT [26] { value: 31 } } } } @@ -846,54 +773,31 @@ Source: true || msg.oneof_type.payload.oneof_type.payload.oneof_type.payload.one CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 - }.oneof_type - } - SELECT [9] { - IDENT [10] { - name: @index2 - }.payload - } - SELECT [11] { - IDENT [12] { - name: @index3 - }.oneof_type - } - SELECT [13] { - SELECT [14] { - SELECT [15] { - SELECT [16] { - IDENT [17] { - name: @index4 - }.child - }.child + SELECT [4] { + SELECT [5] { + SELECT [6] { + SELECT [7] { + IDENT [8] { + name: msg + }.oneof_type + }.payload + }.oneof_type }.payload - }.single_bool + }.oneof_type } - CALL [18] { + CALL [9] { function: _||_ args: { - CONSTANT [19] { value: true } - SELECT [20] { - SELECT [21] { - SELECT [22] { - SELECT [23] { - IDENT [24] { - name: @index4 + CONSTANT [10] { value: true } + SELECT [11] { + SELECT [12] { + SELECT [13] { + SELECT [14] { + IDENT [15] { + name: @index0 }.payload }.oneof_type }.payload @@ -903,14 +807,22 @@ CALL [1] { } } } - CALL [25] { + CALL [16] { function: _||_ args: { - IDENT [26] { - name: @index6 + IDENT [17] { + name: @index1 } - IDENT [27] { - name: @index5 + SELECT [18] { + SELECT [19] { + SELECT [20] { + SELECT [21] { + IDENT [22] { + name: @index0 + }.child + }.child + }.payload + }.single_bool } } } @@ -922,60 +834,48 @@ Source: msg.oneof_type.payload.map_int32_int64[1] + msg.oneof_type.payload.map_i CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 - }.map_int32_int64 - } - CALL [9] { + CALL [3] { function: _[_] args: { - IDENT [10] { - name: @index2 + SELECT [4] { + SELECT [5] { + SELECT [6] { + IDENT [7] { + name: msg + }.oneof_type + }.payload + }.map_int32_int64 } - CONSTANT [11] { value: 1 } + CONSTANT [8] { value: 1 } } } - CALL [12] { - function: _+_ - args: { - CALL [13] { + } + } + CALL [9] { + function: _==_ + args: { + CALL [10] { + function: _+_ + args: { + CALL [11] { function: _+_ args: { - IDENT [14] { - name: @index3 + IDENT [12] { + name: @index0 } - IDENT [15] { - name: @index3 + IDENT [13] { + name: @index0 } } } - IDENT [16] { - name: @index3 + IDENT [14] { + name: @index0 } } } - } - } - CALL [17] { - function: _==_ - args: { - IDENT [18] { - name: @index4 - } - CONSTANT [19] { value: 15 } + CONSTANT [15] { value: 15 } } } } @@ -986,69 +886,60 @@ Source: msg.oneof_type.payload.map_int32_int64[0] + msg.oneof_type.payload.map_i CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload }.map_int32_int64 } - CALL [9] { + } + } + CALL [7] { + function: _==_ + args: { + CALL [8] { function: _+_ args: { - CALL [10] { + CALL [9] { function: _+_ args: { - CALL [11] { + CALL [10] { function: _[_] args: { - IDENT [12] { - name: @index2 + IDENT [11] { + name: @index0 } - CONSTANT [13] { value: 0 } + CONSTANT [12] { value: 0 } } } - CALL [14] { + CALL [13] { function: _[_] args: { - IDENT [15] { - name: @index2 + IDENT [14] { + name: @index0 } - CONSTANT [16] { value: 1 } + CONSTANT [15] { value: 1 } } } } } - CALL [17] { + CALL [16] { function: _[_] args: { - IDENT [18] { - name: @index2 + IDENT [17] { + name: @index0 } - CONSTANT [19] { value: 2 } + CONSTANT [18] { value: 2 } } } } } - } - } - CALL [20] { - function: _==_ - args: { - IDENT [21] { - name: @index3 - } - CONSTANT [22] { value: 8 } + CONSTANT [19] { value: 8 } } } } @@ -1059,7 +950,7 @@ Source: msg.oneof_type.payload.oneof_type.payload.oneof_type.payload.oneof_type. CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { SELECT [4] { @@ -1074,20 +965,17 @@ CALL [1] { }.payload }.oneof_type } - SELECT [9] { - SELECT [10] { - SELECT [11] { - IDENT [12] { - name: @index0 - }.payload - }.oneof_type - }.payload - } } } - SELECT [13] { - IDENT [14] { - name: @index1 + SELECT [9] { + SELECT [10] { + SELECT [11] { + SELECT [12] { + IDENT [13] { + name: @index0 + }.payload + }.oneof_type + }.payload }.single_int64 } } @@ -1098,40 +986,37 @@ Source: (msg.single_int64 > 0 ? msg.single_int64 : 0) == 3 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { name: msg }.single_int64 } - CALL [5] { + } + } + CALL [5] { + function: _==_ + args: { + CALL [6] { function: _?_:_ args: { - CALL [6] { + CALL [7] { function: _>_ args: { - IDENT [7] { + IDENT [8] { name: @index0 } - CONSTANT [8] { value: 0 } + CONSTANT [9] { value: 0 } } } - IDENT [9] { + IDENT [10] { name: @index0 } - CONSTANT [10] { value: 0 } + CONSTANT [11] { value: 0 } } } - } - } - CALL [11] { - function: _==_ - args: { - IDENT [12] { - name: @index1 - } - CONSTANT [13] { value: 3 } + CONSTANT [12] { value: 3 } } } } @@ -1142,54 +1027,51 @@ Source: false ? false : (msg.single_int64) + ((msg.single_int64 + 1) * 2) == 11 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { name: msg }.single_int64 } - CALL [5] { + } + } + CALL [5] { + function: _?_:_ + args: { + CONSTANT [6] { value: false } + CONSTANT [7] { value: false } + CALL [8] { function: _==_ args: { - CALL [6] { + CALL [9] { function: _+_ args: { - IDENT [7] { + IDENT [10] { name: @index0 } - CALL [8] { + CALL [11] { function: _*_ args: { - CALL [9] { + CALL [12] { function: _+_ args: { - IDENT [10] { + IDENT [13] { name: @index0 } - CONSTANT [11] { value: 1 } + CONSTANT [14] { value: 1 } } } - CONSTANT [12] { value: 2 } + CONSTANT [15] { value: 2 } } } } } - CONSTANT [13] { value: 11 } + CONSTANT [16] { value: 11 } } } } } - CALL [14] { - function: _?_:_ - args: { - CONSTANT [15] { value: false } - CONSTANT [16] { value: false } - IDENT [17] { - name: @index1 - } - } - } } } Test case: NESTED_TERNARY @@ -1198,7 +1080,7 @@ Source: (msg.single_int64 > 0 ? (msg.single_int32 > 0 ? msg.single_int64 + msg.s CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { @@ -1210,56 +1092,53 @@ CALL [1] { name: msg }.single_int32 } - CALL [7] { + } + } + CALL [7] { + function: _==_ + args: { + CALL [8] { function: _?_:_ args: { - CALL [8] { + CALL [9] { function: _>_ args: { - IDENT [9] { + IDENT [10] { name: @index0 } - CONSTANT [10] { value: 0 } + CONSTANT [11] { value: 0 } } } - CALL [11] { + CALL [12] { function: _?_:_ args: { - CALL [12] { + CALL [13] { function: _>_ args: { - IDENT [13] { + IDENT [14] { name: @index1 } - CONSTANT [14] { value: 0 } + CONSTANT [15] { value: 0 } } } - CALL [15] { + CALL [16] { function: _+_ args: { - IDENT [16] { + IDENT [17] { name: @index0 } - IDENT [17] { + IDENT [18] { name: @index1 } } } - CONSTANT [18] { value: 0 } + CONSTANT [19] { value: 0 } } } - CONSTANT [19] { value: 0 } + CONSTANT [20] { value: 0 } } } - } - } - CALL [20] { - function: _==_ - args: { - IDENT [21] { - name: @index2 - } - CONSTANT [22] { value: 8 } + CONSTANT [21] { value: 8 } } } } @@ -1270,53 +1149,30 @@ Source: size([[1].exists(i, i > 0)]) + size([[1].exists(j, j > 0)]) + size([[2]. CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - } - } - CALL [5] { - function: _>_ - args: { - IDENT [6] { - name: @c0:0 - } - CONSTANT [7] { value: 0 } - } - } - CALL [8] { - function: _||_ - args: { - IDENT [9] { - name: @x0:0 - } - IDENT [10] { - name: @index1 - } - } - } - COMPREHENSION [11] { - iter_var: @c0:0 + COMPREHENSION [3] { + iter_var: @it:0:0 iter_range: { - IDENT [12] { - name: @index0 + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + } } } - accu_var: @x0:0 + accu_var: @ac:0:0 accu_init: { - CONSTANT [13] { value: false } + CONSTANT [6] { value: false } } loop_condition: { - CALL [14] { + CALL [7] { function: @not_strictly_false args: { - CALL [15] { + CALL [8] { function: !_ args: { - IDENT [16] { - name: @x0:0 + IDENT [9] { + name: @ac:0:0 } } } @@ -1324,76 +1180,52 @@ CALL [1] { } } loop_step: { - IDENT [17] { - name: @index2 + CALL [10] { + function: _||_ + args: { + IDENT [11] { + name: @ac:0:0 + } + CALL [12] { + function: _>_ + args: { + IDENT [13] { + name: @it:0:0 + } + CONSTANT [14] { value: 0 } + } + } + } } } result: { - IDENT [18] { - name: @x0:0 - } - } - } - CREATE_LIST [19] { - elements: { - IDENT [20] { - name: @index3 - } - } - } - CALL [21] { - function: size - args: { - IDENT [22] { - name: @index4 - } - } - } - CREATE_LIST [23] { - elements: { - CONSTANT [24] { value: 2 } - } - } - CALL [25] { - function: _>_ - args: { - IDENT [26] { - name: @c0:0 - } - CONSTANT [27] { value: 1 } - } - } - CALL [28] { - function: _||_ - args: { - IDENT [29] { - name: @x0:0 - } - IDENT [30] { - name: @index7 + IDENT [15] { + name: @ac:0:0 } } } - COMPREHENSION [31] { - iter_var: @c0:0 + COMPREHENSION [16] { + iter_var: @it:0:0 iter_range: { - IDENT [32] { - name: @index6 + LIST [17] { + elements: { + CONSTANT [18] { value: 2 } + } } } - accu_var: @x0:0 + accu_var: @ac:0:0 accu_init: { - CONSTANT [33] { value: false } + CONSTANT [19] { value: false } } loop_condition: { - CALL [34] { + CALL [20] { function: @not_strictly_false args: { - CALL [35] { + CALL [21] { function: !_ args: { - IDENT [36] { - name: @x0:0 + IDENT [22] { + name: @ac:0:0 } } } @@ -1401,67 +1233,87 @@ CALL [1] { } } loop_step: { - IDENT [37] { - name: @index8 + CALL [23] { + function: _||_ + args: { + IDENT [24] { + name: @ac:0:0 + } + CALL [25] { + function: _>_ + args: { + IDENT [26] { + name: @it:0:0 + } + CONSTANT [27] { value: 1 } + } + } + } } } result: { - IDENT [38] { - name: @x0:0 + IDENT [28] { + name: @ac:0:0 } } } - CREATE_LIST [39] { - elements: { - IDENT [40] { - name: @index9 + CALL [29] { + function: size + args: { + LIST [30] { + elements: { + IDENT [31] { + name: @index0 + } + } } } } - CALL [41] { + CALL [32] { function: size args: { - IDENT [42] { - name: @index10 + LIST [33] { + elements: { + IDENT [34] { + name: @index1 + } + } } } } - CALL [43] { - function: _+_ - args: { - CALL [44] { + } + } + CALL [35] { + function: _==_ + args: { + CALL [36] { + function: _+_ + args: { + CALL [37] { function: _+_ args: { - CALL [45] { + CALL [38] { function: _+_ args: { - IDENT [46] { - name: @index5 + IDENT [39] { + name: @index2 } - IDENT [47] { - name: @index5 + IDENT [40] { + name: @index2 } } } - IDENT [48] { - name: @index11 + IDENT [41] { + name: @index3 } } } - IDENT [49] { - name: @index11 + IDENT [42] { + name: @index3 } } } - } - } - CALL [50] { - function: _==_ - args: { - IDENT [51] { - name: @index12 - } - CONSTANT [52] { value: 4 } + CONSTANT [43] { value: 4 } } } } @@ -1472,53 +1324,30 @@ Source: [[1].exists(i, i > 0)] + [[1].exists(j, j > 0)] + [['a'].exists(k, k == CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - } - } - CALL [5] { - function: _>_ - args: { - IDENT [6] { - name: @c0:0 - } - CONSTANT [7] { value: 0 } - } - } - CALL [8] { - function: _||_ - args: { - IDENT [9] { - name: @x0:0 - } - IDENT [10] { - name: @index1 - } - } - } - COMPREHENSION [11] { - iter_var: @c0:0 + COMPREHENSION [3] { + iter_var: @it:0:0 iter_range: { - IDENT [12] { - name: @index0 + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + } } } - accu_var: @x0:0 + accu_var: @ac:0:0 accu_init: { - CONSTANT [13] { value: false } + CONSTANT [6] { value: false } } loop_condition: { - CALL [14] { + CALL [7] { function: @not_strictly_false args: { - CALL [15] { + CALL [8] { function: !_ args: { - IDENT [16] { - name: @x0:0 + IDENT [9] { + name: @ac:0:0 } } } @@ -1526,68 +1355,52 @@ CALL [1] { } } loop_step: { - IDENT [17] { - name: @index2 + CALL [10] { + function: _||_ + args: { + IDENT [11] { + name: @ac:0:0 + } + CALL [12] { + function: _>_ + args: { + IDENT [13] { + name: @it:0:0 + } + CONSTANT [14] { value: 0 } + } + } + } } } result: { - IDENT [18] { - name: @x0:0 - } - } - } - CREATE_LIST [19] { - elements: { - IDENT [20] { - name: @index3 - } - } - } - CREATE_LIST [21] { - elements: { - CONSTANT [22] { value: "a" } - } - } - CALL [23] { - function: _==_ - args: { - IDENT [24] { - name: @c0:1 - } - CONSTANT [25] { value: "a" } - } - } - CALL [26] { - function: _||_ - args: { - IDENT [27] { - name: @x0:1 - } - IDENT [28] { - name: @index6 + IDENT [15] { + name: @ac:0:0 } } } - COMPREHENSION [29] { - iter_var: @c0:1 + COMPREHENSION [16] { + iter_var: @it:0:1 iter_range: { - IDENT [30] { - name: @index5 + LIST [17] { + elements: { + CONSTANT [18] { value: "a" } + } } } - accu_var: @x0:1 + accu_var: @ac:0:1 accu_init: { - CONSTANT [31] { value: false } + CONSTANT [19] { value: false } } loop_condition: { - CALL [32] { + CALL [20] { function: @not_strictly_false args: { - CALL [33] { + CALL [21] { function: !_ args: { - IDENT [34] { - name: @x0:1 + IDENT [22] { + name: @ac:0:1 } } } @@ -1595,685 +1408,2387 @@ CALL [1] { } } loop_step: { - IDENT [35] { - name: @index7 + CALL [23] { + function: _||_ + args: { + IDENT [24] { + name: @ac:0:1 + } + CALL [25] { + function: _==_ + args: { + IDENT [26] { + name: @it:0:1 + } + CONSTANT [27] { value: "a" } + } + } + } } } result: { - IDENT [36] { - name: @x0:1 + IDENT [28] { + name: @ac:0:1 } } } - CREATE_LIST [37] { + LIST [29] { elements: { - IDENT [38] { - name: @index8 + IDENT [30] { + name: @index0 } } } - CREATE_LIST [39] { + LIST [31] { elements: { - CONSTANT [40] { value: true } - CONSTANT [41] { value: true } - CONSTANT [42] { value: true } - CONSTANT [43] { value: true } + IDENT [32] { + name: @index1 + } } } - CALL [44] { + } + } + CALL [33] { + function: _==_ + args: { + CALL [34] { function: _+_ args: { - CALL [45] { + CALL [35] { function: _+_ args: { - CALL [46] { + CALL [36] { function: _+_ args: { - IDENT [47] { - name: @index4 + IDENT [37] { + name: @index2 } - IDENT [48] { - name: @index4 + IDENT [38] { + name: @index2 } } } - IDENT [49] { - name: @index9 + IDENT [39] { + name: @index3 } } } - IDENT [50] { - name: @index9 + IDENT [40] { + name: @index3 } } } - } - } - CALL [51] { - function: _==_ - args: { - IDENT [52] { - name: @index11 - } - IDENT [53] { - name: @index10 + LIST [41] { + elements: { + CONSTANT [42] { value: true } + CONSTANT [43] { value: true } + CONSTANT [44] { value: true } + CONSTANT [45] { value: true } + } } } } } } -Test case: NESTED_MACROS -Source: [1,2,3].map(i, [1, 2, 3].map(i, i + 1)) == [[2, 3, 4], [2, 3, 4], [2, 3, 4]] +Test case: MULTIPLE_MACROS_3 +Source: [1].exists(i, i > 0) && [1].exists(j, j > 0) && [1].exists(k, k > 1) && [2].exists(l, l > 1) =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - CONSTANT [5] { value: 2 } - CONSTANT [6] { value: 3 } + COMPREHENSION [3] { + iter_var: @it:0:0 + iter_range: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + } + } } - } - CREATE_LIST [7] { - elements: { - CONSTANT [8] { value: 2 } - CONSTANT [9] { value: 3 } - CONSTANT [10] { value: 4 } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [6] { value: false } } - } - CREATE_LIST [11] { - elements: { - IDENT [12] { - name: @index1 + loop_condition: { + CALL [7] { + function: @not_strictly_false + args: { + CALL [8] { + function: !_ + args: { + IDENT [9] { + name: @ac:0:0 + } + } + } + } } - IDENT [13] { - name: @index1 + } + loop_step: { + CALL [10] { + function: _||_ + args: { + IDENT [11] { + name: @ac:0:0 + } + CALL [12] { + function: _>_ + args: { + IDENT [13] { + name: @it:0:0 + } + CONSTANT [14] { value: 0 } + } + } + } } - IDENT [14] { - name: @index1 + } + result: { + IDENT [15] { + name: @ac:0:0 } } } - CREATE_LIST [15] { - elements: { - COMPREHENSION [16] { - iter_var: @c1:0 + } + } + CALL [16] { + function: _&&_ + args: { + CALL [17] { + function: _&&_ + args: { + IDENT [18] { + name: @index0 + } + IDENT [19] { + name: @index0 + } + } + } + CALL [20] { + function: _&&_ + args: { + COMPREHENSION [21] { + iter_var: @it:0:0 iter_range: { - IDENT [17] { - name: @index0 - } - } - accu_var: @x1:0 - accu_init: { - CREATE_LIST [18] { + LIST [22] { elements: { + CONSTANT [23] { value: 1 } } } } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [24] { value: false } + } loop_condition: { - CONSTANT [19] { value: true } + CALL [25] { + function: @not_strictly_false + args: { + CALL [26] { + function: !_ + args: { + IDENT [27] { + name: @ac:0:0 + } + } + } + } + } } loop_step: { - CALL [20] { - function: _+_ + CALL [28] { + function: _||_ args: { - IDENT [21] { - name: @x1:0 + IDENT [29] { + name: @ac:0:0 } - CREATE_LIST [22] { - elements: { - CALL [23] { - function: _+_ - args: { - IDENT [24] { - name: @c1:0 - } - CONSTANT [25] { value: 1 } - } + CALL [30] { + function: _>_ + args: { + IDENT [31] { + name: @it:0:0 } + CONSTANT [32] { value: 1 } } } } } } result: { - IDENT [26] { - name: @x1:0 - } - } - } - } - } - COMPREHENSION [27] { - iter_var: @c0:0 - iter_range: { - IDENT [28] { - name: @index0 - } - } - accu_var: @x0:0 - accu_init: { - CREATE_LIST [29] { - elements: { - } - } - } - loop_condition: { - CONSTANT [30] { value: true } - } - loop_step: { - CALL [31] { - function: _+_ - args: { - IDENT [32] { - name: @x0:0 - } IDENT [33] { - name: @index3 + name: @ac:0:0 } } } - } - result: { - IDENT [34] { - name: @x0:0 + COMPREHENSION [34] { + iter_var: @it:0:0 + iter_range: { + LIST [35] { + elements: { + CONSTANT [36] { value: 2 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [37] { value: false } + } + loop_condition: { + CALL [38] { + function: @not_strictly_false + args: { + CALL [39] { + function: !_ + args: { + IDENT [40] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [41] { + function: _||_ + args: { + IDENT [42] { + name: @ac:0:0 + } + CALL [43] { + function: _>_ + args: { + IDENT [44] { + name: @it:0:0 + } + CONSTANT [45] { value: 1 } + } + } + } + } + } + result: { + IDENT [46] { + name: @ac:0:0 + } + } } } } } } - CALL [35] { - function: _==_ - args: { - IDENT [36] { - name: @index4 - } - IDENT [37] { - name: @index2 - } - } - } } } -Test case: NESTED_MACROS_2 -Source: [1, 2].map(y, [1, 2, 3].filter(x, x == y)) == [[1], [2]] +Test case: MULTIPLE_MACROS_COMP_V2_1 +Source: size([[1].exists(i,x,i > 0 && x >= 0)]) + size([[1].exists(j, y, j > 0 && y >= 0)]) + size([[2].exists(k,z,k > 1 && z >= 0)]) + size([[2].exists(l, w, l > 1 && w >= 0)]) == 4 =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CREATE_LIST [4] { + COMPREHENSION [3] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + LIST [4] { elements: { CONSTANT [5] { value: 1 } } } - CREATE_LIST [6] { - elements: { - CONSTANT [7] { value: 2 } - } - } } - } - CREATE_LIST [8] { - elements: { - COMPREHENSION [9] { - iter_var: @c1:0 - iter_range: { - CREATE_LIST [10] { - elements: { - CONSTANT [11] { value: 1 } - CONSTANT [12] { value: 2 } - CONSTANT [13] { value: 3 } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [6] { value: false } + } + loop_condition: { + CALL [7] { + function: @not_strictly_false + args: { + CALL [8] { + function: !_ + args: { + IDENT [9] { + name: @ac:0:0 + } } } } - accu_var: @x1:0 - accu_init: { - CREATE_LIST [14] { - elements: { - } + } + } + loop_step: { + CALL [10] { + function: _||_ + args: { + IDENT [11] { + name: @ac:0:0 } - } - loop_condition: { - CONSTANT [15] { value: true } - } - loop_step: { - CALL [16] { - function: _?_:_ + CALL [12] { + function: _&&_ args: { - CALL [17] { - function: _==_ + CALL [13] { + function: _>_ args: { - IDENT [18] { - name: @c1:0 - } - IDENT [19] { - name: @c0:0 + IDENT [14] { + name: @it:0:0 } + CONSTANT [15] { value: 0 } } } - CALL [20] { - function: _+_ + CALL [16] { + function: _>=_ args: { - IDENT [21] { - name: @x1:0 - } - CREATE_LIST [22] { - elements: { - IDENT [23] { - name: @c1:0 - } - } + IDENT [17] { + name: @it2:0:0 } + CONSTANT [18] { value: 0 } } } - IDENT [24] { - name: @x1:0 - } } } } - result: { - IDENT [25] { - name: @x1:0 - } - } + } + } + result: { + IDENT [19] { + name: @ac:0:0 } } } - COMPREHENSION [26] { - iter_var: @c0:0 + COMPREHENSION [20] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 iter_range: { - CREATE_LIST [27] { + LIST [21] { elements: { - CONSTANT [28] { value: 1 } - CONSTANT [29] { value: 2 } + CONSTANT [22] { value: 2 } } } } - accu_var: @x0:0 + accu_var: @ac:0:0 accu_init: { - CREATE_LIST [30] { - elements: { - } - } + CONSTANT [23] { value: false } } loop_condition: { - CONSTANT [31] { value: true } + CALL [24] { + function: @not_strictly_false + args: { + CALL [25] { + function: !_ + args: { + IDENT [26] { + name: @ac:0:0 + } + } + } + } + } } loop_step: { - CALL [32] { - function: _+_ + CALL [27] { + function: _||_ args: { - IDENT [33] { - name: @x0:0 + IDENT [28] { + name: @ac:0:0 } - IDENT [34] { - name: @index1 + CALL [29] { + function: _&&_ + args: { + CALL [30] { + function: _>_ + args: { + IDENT [31] { + name: @it:0:0 + } + CONSTANT [32] { value: 1 } + } + } + CALL [33] { + function: _>=_ + args: { + IDENT [34] { + name: @it2:0:0 + } + CONSTANT [35] { value: 0 } + } + } + } } } } } result: { - IDENT [35] { - name: @x0:0 + IDENT [36] { + name: @ac:0:0 + } + } + } + CALL [37] { + function: size + args: { + LIST [38] { + elements: { + IDENT [39] { + name: @index0 + } + } + } + } + } + CALL [40] { + function: size + args: { + LIST [41] { + elements: { + IDENT [42] { + name: @index1 + } + } } } } } } - CALL [36] { + CALL [43] { function: _==_ args: { - IDENT [37] { - name: @index2 - } - IDENT [38] { - name: @index0 + CALL [44] { + function: _+_ + args: { + CALL [45] { + function: _+_ + args: { + CALL [46] { + function: _+_ + args: { + IDENT [47] { + name: @index2 + } + IDENT [48] { + name: @index2 + } + } + } + IDENT [49] { + name: @index3 + } + } + } + IDENT [50] { + name: @index3 + } + } } + CONSTANT [51] { value: 4 } } } } } -Test case: INCLUSION_LIST -Source: 1 in [1,2,3] && 2 in [1,2,3] && 3 in [3, [1,2,3]] && 1 in [1,2,3] +Test case: MULTIPLE_MACROS_COMP_V2_2 +Source: [1].exists(i, x, i > 0 && x > 0) && [1].exists(j, y, j > 0 && y > 0) && [1].exists(k, z, k > 1 && z > 0) && [2].exists(l, w, l > 1 && w > 0) =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - CONSTANT [5] { value: 2 } - CONSTANT [6] { value: 3 } - } - } - CALL [7] { - function: @in - args: { - CONSTANT [8] { value: 1 } - IDENT [9] { - name: @index0 + COMPREHENSION [3] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + } } } - } - CALL [10] { - function: _&&_ - args: { - CALL [11] { - function: @in + accu_var: @ac:0:0 + accu_init: { + CONSTANT [6] { value: false } + } + loop_condition: { + CALL [7] { + function: @not_strictly_false args: { - CONSTANT [12] { value: 3 } - CREATE_LIST [13] { - elements: { - CONSTANT [14] { value: 3 } - IDENT [15] { - name: @index0 + CALL [8] { + function: !_ + args: { + IDENT [9] { + name: @ac:0:0 } } } } } - IDENT [16] { - name: @index1 - } } - } - CALL [17] { - function: _&&_ - args: { - IDENT [18] { - name: @index1 - } - CALL [19] { - function: @in + loop_step: { + CALL [10] { + function: _||_ args: { - CONSTANT [20] { value: 2 } - IDENT [21] { - name: @index0 + IDENT [11] { + name: @ac:0:0 + } + CALL [12] { + function: _&&_ + args: { + CALL [13] { + function: _>_ + args: { + IDENT [14] { + name: @it:0:0 + } + CONSTANT [15] { value: 0 } + } + } + CALL [16] { + function: _>_ + args: { + IDENT [17] { + name: @it2:0:0 + } + CONSTANT [18] { value: 0 } + } + } + } } } } } + result: { + IDENT [19] { + name: @ac:0:0 + } + } } } } - CALL [22] { + CALL [20] { function: _&&_ args: { - IDENT [23] { - name: @index3 - } - IDENT [24] { - name: @index2 + CALL [21] { + function: _&&_ + args: { + IDENT [22] { + name: @index0 + } + IDENT [23] { + name: @index0 + } + } + } + CALL [24] { + function: _&&_ + args: { + COMPREHENSION [25] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + LIST [26] { + elements: { + CONSTANT [27] { value: 1 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [28] { value: false } + } + loop_condition: { + CALL [29] { + function: @not_strictly_false + args: { + CALL [30] { + function: !_ + args: { + IDENT [31] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [32] { + function: _||_ + args: { + IDENT [33] { + name: @ac:0:0 + } + CALL [34] { + function: _&&_ + args: { + CALL [35] { + function: _>_ + args: { + IDENT [36] { + name: @it:0:0 + } + CONSTANT [37] { value: 1 } + } + } + CALL [38] { + function: _>_ + args: { + IDENT [39] { + name: @it2:0:0 + } + CONSTANT [40] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [41] { + name: @ac:0:0 + } + } + } + COMPREHENSION [42] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + LIST [43] { + elements: { + CONSTANT [44] { value: 2 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [45] { value: false } + } + loop_condition: { + CALL [46] { + function: @not_strictly_false + args: { + CALL [47] { + function: !_ + args: { + IDENT [48] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [49] { + function: _||_ + args: { + IDENT [50] { + name: @ac:0:0 + } + CALL [51] { + function: _&&_ + args: { + CALL [52] { + function: _>_ + args: { + IDENT [53] { + name: @it:0:0 + } + CONSTANT [54] { value: 1 } + } + } + CALL [55] { + function: _>_ + args: { + IDENT [56] { + name: @it2:0:0 + } + CONSTANT [57] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [58] { + name: @ac:0:0 + } + } + } + } + } + } + } + } +} +Test case: NESTED_MACROS +Source: [1,2,3].map(i, [1, 2, 3].map(i, i + 1)) == [[2, 3, 4], [2, 3, 4], [2, 3, 4]] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + CONSTANT [5] { value: 2 } + CONSTANT [6] { value: 3 } + } + } + LIST [7] { + elements: { + CONSTANT [8] { value: 2 } + CONSTANT [9] { value: 3 } + CONSTANT [10] { value: 4 } + } + } + COMPREHENSION [11] { + iter_var: @it:0:0 + iter_range: { + IDENT [12] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [13] { + elements: { + } + } + } + loop_condition: { + CONSTANT [14] { value: true } + } + loop_step: { + CALL [15] { + function: _+_ + args: { + IDENT [16] { + name: @ac:0:0 + } + LIST [17] { + elements: { + CALL [18] { + function: _+_ + args: { + IDENT [19] { + name: @it:0:0 + } + CONSTANT [20] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [21] { + name: @ac:0:0 + } + } + } + } + } + CALL [22] { + function: _==_ + args: { + COMPREHENSION [23] { + iter_var: @it:1:0 + iter_range: { + IDENT [24] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [25] { + elements: { + } + } + } + loop_condition: { + CONSTANT [26] { value: true } + } + loop_step: { + CALL [27] { + function: _+_ + args: { + IDENT [28] { + name: @ac:1:0 + } + LIST [29] { + elements: { + IDENT [30] { + name: @index2 + } + } + } + } + } + } + result: { + IDENT [31] { + name: @ac:1:0 + } + } + } + LIST [32] { + elements: { + IDENT [33] { + name: @index1 + } + IDENT [34] { + name: @index1 + } + IDENT [35] { + name: @index1 + } + } + } + } + } + } +} +Test case: NESTED_MACROS_2 +Source: [1, 2].map(y, [1, 2, 3].filter(x, x == y)) == [[1], [2]] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + } + } + LIST [6] { + elements: { + CONSTANT [7] { value: 2 } + } + } + } + } + LIST [8] { + elements: { + CONSTANT [9] { value: 1 } + CONSTANT [10] { value: 2 } + } + } + LIST [11] { + elements: { + CONSTANT [12] { value: 1 } + CONSTANT [13] { value: 2 } + CONSTANT [14] { value: 3 } + } + } + } + } + CALL [15] { + function: _==_ + args: { + COMPREHENSION [16] { + iter_var: @it:1:0 + iter_range: { + IDENT [17] { + name: @index1 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [18] { + elements: { + } + } + } + loop_condition: { + CONSTANT [19] { value: true } + } + loop_step: { + CALL [20] { + function: _+_ + args: { + IDENT [21] { + name: @ac:1:0 + } + LIST [22] { + elements: { + COMPREHENSION [23] { + iter_var: @it:0:0 + iter_range: { + IDENT [24] { + name: @index2 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [25] { + elements: { + } + } + } + loop_condition: { + CONSTANT [26] { value: true } + } + loop_step: { + CALL [27] { + function: _?_:_ + args: { + CALL [28] { + function: _==_ + args: { + IDENT [29] { + name: @it:0:0 + } + IDENT [30] { + name: @it:1:0 + } + } + } + CALL [31] { + function: _+_ + args: { + IDENT [32] { + name: @ac:0:0 + } + LIST [33] { + elements: { + IDENT [34] { + name: @it:0:0 + } + } + } + } + } + IDENT [35] { + name: @ac:0:0 + } + } + } + } + result: { + IDENT [36] { + name: @ac:0:0 + } + } + } + } + } + } + } + } + result: { + IDENT [37] { + name: @ac:1:0 + } + } + } + IDENT [38] { + name: @index0 + } + } + } + } +} +Test case: NESTED_MACROS_3 +Source: [1,2,3].map(i, i * 2) + [1,2,3].map(i, i * 2).map(i, i * 2) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + COMPREHENSION [3] { + iter_var: @it:0:0 + iter_range: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + CONSTANT [6] { value: 2 } + CONSTANT [7] { value: 3 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [8] { + elements: { + } + } + } + loop_condition: { + CONSTANT [9] { value: true } + } + loop_step: { + CALL [10] { + function: _+_ + args: { + IDENT [11] { + name: @ac:0:0 + } + LIST [12] { + elements: { + CALL [13] { + function: _*_ + args: { + IDENT [14] { + name: @it:0:0 + } + CONSTANT [15] { value: 2 } + } + } + } + } + } + } + } + result: { + IDENT [16] { + name: @ac:0:0 + } + } + } + } + } + CALL [17] { + function: _+_ + args: { + IDENT [18] { + name: @index0 + } + COMPREHENSION [19] { + iter_var: @it:1:0 + iter_range: { + IDENT [20] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [21] { + elements: { + } + } + } + loop_condition: { + CONSTANT [22] { value: true } + } + loop_step: { + CALL [23] { + function: _+_ + args: { + IDENT [24] { + name: @ac:1:0 + } + LIST [25] { + elements: { + CALL [26] { + function: _*_ + args: { + IDENT [27] { + name: @it:1:0 + } + CONSTANT [28] { value: 2 } + } + } + } + } + } + } + } + result: { + IDENT [29] { + name: @ac:1:0 + } + } + } + } + } + } +} +Test case: NESTED_MACROS_COMP_V2_1 +Source: [1,2,3].transformList(i, v, [1, 2, 3].transformList(i, v, i + v + 1)) == [[2, 4, 6], [2, 4, 6], [2, 4, 6]] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + CONSTANT [5] { value: 2 } + CONSTANT [6] { value: 3 } + } + } + LIST [7] { + elements: { + CONSTANT [8] { value: 2 } + CONSTANT [9] { value: 4 } + CONSTANT [10] { value: 6 } + } + } + COMPREHENSION [11] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + IDENT [12] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [13] { + elements: { + } + } + } + loop_condition: { + CONSTANT [14] { value: true } + } + loop_step: { + CALL [15] { + function: _+_ + args: { + IDENT [16] { + name: @ac:0:0 + } + LIST [17] { + elements: { + CALL [18] { + function: _+_ + args: { + CALL [19] { + function: _+_ + args: { + IDENT [20] { + name: @it:0:0 + } + IDENT [21] { + name: @it2:0:0 + } + } + } + CONSTANT [22] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [23] { + name: @ac:0:0 + } + } + } + } + } + CALL [24] { + function: _==_ + args: { + COMPREHENSION [25] { + iter_var: @it:1:0 + iter_var2: @it2:1:0 + iter_range: { + IDENT [26] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [27] { + elements: { + } + } + } + loop_condition: { + CONSTANT [28] { value: true } + } + loop_step: { + CALL [29] { + function: _+_ + args: { + IDENT [30] { + name: @ac:1:0 + } + LIST [31] { + elements: { + IDENT [32] { + name: @index2 + } + } + } + } + } + } + result: { + IDENT [33] { + name: @ac:1:0 + } + } + } + LIST [34] { + elements: { + IDENT [35] { + name: @index1 + } + IDENT [36] { + name: @index1 + } + IDENT [37] { + name: @index1 + } + } + } + } + } + } +} +Test case: NESTED_MACROS_COMP_V2_2 +Source: [1, 2].transformList(i, y, [1, 2, 3].filter(x, x == y && i < y)) == [[1], [2]] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + } + } + LIST [6] { + elements: { + CONSTANT [7] { value: 2 } + } + } + } + } + LIST [8] { + elements: { + CONSTANT [9] { value: 1 } + CONSTANT [10] { value: 2 } + } + } + LIST [11] { + elements: { + CONSTANT [12] { value: 1 } + CONSTANT [13] { value: 2 } + CONSTANT [14] { value: 3 } + } + } + } + } + CALL [15] { + function: _==_ + args: { + COMPREHENSION [16] { + iter_var: @it:1:0 + iter_var2: @it2:1:0 + iter_range: { + IDENT [17] { + name: @index1 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [18] { + elements: { + } + } + } + loop_condition: { + CONSTANT [19] { value: true } + } + loop_step: { + CALL [20] { + function: _+_ + args: { + IDENT [21] { + name: @ac:1:0 + } + LIST [22] { + elements: { + COMPREHENSION [23] { + iter_var: @it:0:0 + iter_range: { + IDENT [24] { + name: @index2 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [25] { + elements: { + } + } + } + loop_condition: { + CONSTANT [26] { value: true } + } + loop_step: { + CALL [27] { + function: _?_:_ + args: { + CALL [28] { + function: _&&_ + args: { + CALL [29] { + function: _==_ + args: { + IDENT [30] { + name: @it:0:0 + } + IDENT [31] { + name: @it2:1:0 + } + } + } + CALL [32] { + function: _<_ + args: { + IDENT [33] { + name: @it:1:0 + } + IDENT [34] { + name: @it2:1:0 + } + } + } + } + } + CALL [35] { + function: _+_ + args: { + IDENT [36] { + name: @ac:0:0 + } + LIST [37] { + elements: { + IDENT [38] { + name: @it:0:0 + } + } + } + } + } + IDENT [39] { + name: @ac:0:0 + } + } + } + } + result: { + IDENT [40] { + name: @ac:0:0 + } + } + } + } + } + } + } + } + result: { + IDENT [41] { + name: @ac:1:0 + } + } + } + IDENT [42] { + name: @index0 + } + } + } + } +} +Test case: ADJACENT_NESTED_MACROS +Source: [1,2,3].map(i, [1, 2, 3].map(i, i + 1)) == [1,2,3].map(j, [1, 2, 3].map(j, j + 1)) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + COMPREHENSION [3] { + iter_var: @it:0:0 + iter_range: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + CONSTANT [6] { value: 2 } + CONSTANT [7] { value: 3 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [8] { + elements: { + } + } + } + loop_condition: { + CONSTANT [9] { value: true } + } + loop_step: { + CALL [10] { + function: _+_ + args: { + IDENT [11] { + name: @ac:0:0 + } + LIST [12] { + elements: { + CALL [13] { + function: _+_ + args: { + IDENT [14] { + name: @it:0:0 + } + CONSTANT [15] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [16] { + name: @ac:0:0 + } + } + } + COMPREHENSION [17] { + iter_var: @it:1:0 + iter_range: { + LIST [18] { + elements: { + CONSTANT [19] { value: 1 } + CONSTANT [20] { value: 2 } + CONSTANT [21] { value: 3 } + } + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [22] { + elements: { + } + } + } + loop_condition: { + CONSTANT [23] { value: true } + } + loop_step: { + CALL [24] { + function: _+_ + args: { + IDENT [25] { + name: @ac:1:0 + } + LIST [26] { + elements: { + IDENT [27] { + name: @index0 + } + } + } + } + } + } + result: { + IDENT [28] { + name: @ac:1:0 + } + } + } + } + } + CALL [29] { + function: _==_ + args: { + IDENT [30] { + name: @index1 + } + IDENT [31] { + name: @index1 + } + } + } + } +} +Test case: ADJACENT_NESTED_MACROS_COMP_V2 +Source: [1,2,3].transformMap(i, x, [1, 2, 3].transformMap(i, x, i + x + 1)) == [1,2,3].transformMap(j, y, [1, 2, 3].transformMap(j, y, j + y + 1)) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + COMPREHENSION [3] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + CONSTANT [6] { value: 2 } + CONSTANT [7] { value: 3 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + MAP [8] { + + } + } + loop_condition: { + CONSTANT [9] { value: true } + } + loop_step: { + CALL [10] { + function: cel.@mapInsert + args: { + IDENT [11] { + name: @ac:0:0 + } + IDENT [12] { + name: @it:0:0 + } + CALL [13] { + function: _+_ + args: { + CALL [14] { + function: _+_ + args: { + IDENT [15] { + name: @it:0:0 + } + IDENT [16] { + name: @it2:0:0 + } + } + } + CONSTANT [17] { value: 1 } + } + } + } + } + } + result: { + IDENT [18] { + name: @ac:0:0 + } + } + } + COMPREHENSION [19] { + iter_var: @it:1:0 + iter_var2: @it2:1:0 + iter_range: { + LIST [20] { + elements: { + CONSTANT [21] { value: 1 } + CONSTANT [22] { value: 2 } + CONSTANT [23] { value: 3 } + } + } + } + accu_var: @ac:1:0 + accu_init: { + MAP [24] { + + } + } + loop_condition: { + CONSTANT [25] { value: true } + } + loop_step: { + CALL [26] { + function: cel.@mapInsert + args: { + IDENT [27] { + name: @ac:1:0 + } + IDENT [28] { + name: @it:1:0 + } + IDENT [29] { + name: @index0 + } + } + } + } + result: { + IDENT [30] { + name: @ac:1:0 + } + } + } + } + } + CALL [31] { + function: _==_ + args: { + IDENT [32] { + name: @index1 + } + IDENT [33] { + name: @index1 + } + } + } + } +} +Test case: INCLUSION_LIST +Source: 1 in [1,2,3] && 2 in [1,2,3] && 3 in [3, [1,2,3]] && 1 in [1,2,3] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + CALL [3] { + function: @in + args: { + CONSTANT [4] { value: 1 } + LIST [5] { + elements: { + CONSTANT [6] { value: 1 } + CONSTANT [7] { value: 2 } + CONSTANT [8] { value: 3 } + } + } + } + } + LIST [9] { + elements: { + CONSTANT [10] { value: 1 } + CONSTANT [11] { value: 2 } + CONSTANT [12] { value: 3 } + } + } + } + } + CALL [13] { + function: _&&_ + args: { + CALL [14] { + function: _&&_ + args: { + IDENT [15] { + name: @index0 + } + CALL [16] { + function: @in + args: { + CONSTANT [17] { value: 2 } + IDENT [18] { + name: @index1 + } + } + } + } + } + CALL [19] { + function: _&&_ + args: { + CALL [20] { + function: @in + args: { + CONSTANT [21] { value: 3 } + LIST [22] { + elements: { + CONSTANT [23] { value: 3 } + IDENT [24] { + name: @index1 + } + } + } + } + } + IDENT [25] { + name: @index0 + } + } + } + } + } + } +} +Test case: INCLUSION_MAP +Source: 2 in {'a': 1, 2: {true: false}, 3: {true: false}} +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + MAP [3] { + MAP_ENTRY [4] { + key: { + CONSTANT [5] { value: true } + } + value: { + CONSTANT [6] { value: false } + } + } + } + } + } + CALL [7] { + function: @in + args: { + CONSTANT [8] { value: 2 } + MAP [9] { + MAP_ENTRY [10] { + key: { + CONSTANT [11] { value: "a" } + } + value: { + CONSTANT [12] { value: 1 } + } + } + MAP_ENTRY [13] { + key: { + CONSTANT [14] { value: 2 } + } + value: { + IDENT [15] { + name: @index0 + } + } + } + MAP_ENTRY [16] { + key: { + CONSTANT [17] { value: 3 } + } + value: { + IDENT [18] { + name: @index0 + } + } + } + } + } + } + } +} +Test case: MACRO_ITER_VAR_NOT_REFERENCED +Source: [1,2].map(i, [1, 2].map(i, [3,4])) == [[[3, 4], [3, 4]], [[3, 4], [3, 4]]] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + LIST [4] { + elements: { + CONSTANT [5] { value: 3 } + CONSTANT [6] { value: 4 } + } + } + LIST [7] { + elements: { + CONSTANT [8] { value: 3 } + CONSTANT [9] { value: 4 } + } + } + } + } + LIST [10] { + elements: { + CONSTANT [11] { value: 1 } + CONSTANT [12] { value: 2 } + } + } + COMPREHENSION [13] { + iter_var: @it:0:0 + iter_range: { + IDENT [14] { + name: @index1 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [15] { + elements: { + } + } + } + loop_condition: { + CONSTANT [16] { value: true } + } + loop_step: { + CALL [17] { + function: _+_ + args: { + IDENT [18] { + name: @ac:0:0 + } + LIST [19] { + elements: { + LIST [20] { + elements: { + CONSTANT [21] { value: 3 } + CONSTANT [22] { value: 4 } + } + } + } + } + } + } + } + result: { + IDENT [23] { + name: @ac:0:0 + } + } + } + } + } + CALL [24] { + function: _==_ + args: { + COMPREHENSION [25] { + iter_var: @it:1:0 + iter_range: { + IDENT [26] { + name: @index1 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [27] { + elements: { + } + } + } + loop_condition: { + CONSTANT [28] { value: true } + } + loop_step: { + CALL [29] { + function: _+_ + args: { + IDENT [30] { + name: @ac:1:0 + } + LIST [31] { + elements: { + IDENT [32] { + name: @index2 + } + } + } + } + } + } + result: { + IDENT [33] { + name: @ac:1:0 + } + } + } + LIST [34] { + elements: { + IDENT [35] { + name: @index0 + } + IDENT [36] { + name: @index0 + } + } + } + } + } + } +} +Test case: MACRO_ITER_VAR2_NOT_REFERENCED +Source: [1,2].transformList(i, v, [1, 2].transformList(i, v, [3,4])) == [[[3, 4], [3, 4]], [[3, 4], [3, 4]]] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + LIST [4] { + elements: { + CONSTANT [5] { value: 3 } + CONSTANT [6] { value: 4 } + } + } + LIST [7] { + elements: { + CONSTANT [8] { value: 3 } + CONSTANT [9] { value: 4 } + } + } + } + } + LIST [10] { + elements: { + CONSTANT [11] { value: 1 } + CONSTANT [12] { value: 2 } + } + } + COMPREHENSION [13] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + IDENT [14] { + name: @index1 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [15] { + elements: { + } + } + } + loop_condition: { + CONSTANT [16] { value: true } + } + loop_step: { + CALL [17] { + function: _+_ + args: { + IDENT [18] { + name: @ac:0:0 + } + LIST [19] { + elements: { + LIST [20] { + elements: { + CONSTANT [21] { value: 3 } + CONSTANT [22] { value: 4 } + } + } + } + } + } + } + } + result: { + IDENT [23] { + name: @ac:0:0 + } + } + } + } + } + CALL [24] { + function: _==_ + args: { + COMPREHENSION [25] { + iter_var: @it:1:0 + iter_var2: @it2:1:0 + iter_range: { + IDENT [26] { + name: @index1 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [27] { + elements: { + } + } + } + loop_condition: { + CONSTANT [28] { value: true } + } + loop_step: { + CALL [29] { + function: _+_ + args: { + IDENT [30] { + name: @ac:1:0 + } + LIST [31] { + elements: { + IDENT [32] { + name: @index2 + } + } + } + } + } + } + result: { + IDENT [33] { + name: @ac:1:0 + } + } + } + LIST [34] { + elements: { + IDENT [35] { + name: @index0 + } + IDENT [36] { + name: @index0 + } + } } } } } } -Test case: INCLUSION_MAP -Source: 2 in {'a': 1, 2: {true: false}, 3: {true: false}} +Test case: MACRO_SHADOWED_VARIABLE +Source: [x - 1 > 3 ? x - 1 : 5].exists(x, x - 1 > 3) || x - 1 > 3 =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_MAP [3] { - MAP_ENTRY [4] { - key: { - CONSTANT [5] { value: true } - } - value: { - CONSTANT [6] { value: false } + CALL [3] { + function: _>_ + args: { + CALL [4] { + function: _-_ + args: { + IDENT [5] { + name: x + } + CONSTANT [6] { value: 1 } + } } + CONSTANT [7] { value: 3 } } } - CREATE_MAP [7] { - MAP_ENTRY [8] { - key: { - CONSTANT [9] { value: "a" } - } - value: { - CONSTANT [10] { value: 1 } + } + } + CALL [8] { + function: _||_ + args: { + COMPREHENSION [9] { + iter_var: @it:0:0 + iter_range: { + LIST [10] { + elements: { + CALL [11] { + function: _?_:_ + args: { + IDENT [12] { + name: @index0 + } + CALL [13] { + function: _-_ + args: { + IDENT [14] { + name: x + } + CONSTANT [15] { value: 1 } + } + } + CONSTANT [16] { value: 5 } + } + } + } } } - MAP_ENTRY [11] { - key: { - CONSTANT [12] { value: 2 } - } - value: { - IDENT [13] { - name: @index0 + accu_var: @ac:0:0 + accu_init: { + CONSTANT [17] { value: false } + } + loop_condition: { + CALL [18] { + function: @not_strictly_false + args: { + CALL [19] { + function: !_ + args: { + IDENT [20] { + name: @ac:0:0 + } + } + } } } } - MAP_ENTRY [14] { - key: { - CONSTANT [15] { value: 3 } - } - value: { - IDENT [16] { - name: @index0 + loop_step: { + CALL [21] { + function: _||_ + args: { + IDENT [22] { + name: @ac:0:0 + } + CALL [23] { + function: _>_ + args: { + CALL [24] { + function: _-_ + args: { + IDENT [25] { + name: @it:0:0 + } + CONSTANT [26] { value: 1 } + } + } + CONSTANT [27] { value: 3 } + } + } } } } + result: { + IDENT [28] { + name: @ac:0:0 + } + } } - } - } - CALL [17] { - function: @in - args: { - CONSTANT [18] { value: 2 } - IDENT [19] { - name: @index1 + IDENT [29] { + name: @index0 } } } } } -Test case: MACRO_ITER_VAR_NOT_REFERENCED -Source: [1,2].map(i, [1, 2].map(i, [3,4])) == [[[3, 4], [3, 4]], [[3, 4], [3, 4]]] +Test case: MACRO_SHADOWED_VARIABLE_2 +Source: ["foo", "bar"].map(x, [x + x, x + x]).map(x, [x + x, x + x]) =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - CONSTANT [5] { value: 2 } + COMPREHENSION [3] { + iter_var: @it:0:0 + iter_range: { + LIST [4] { + elements: { + CONSTANT [5] { value: "foo" } + CONSTANT [6] { value: "bar" } + } + } } - } - CREATE_LIST [6] { - elements: { - CONSTANT [7] { value: 3 } - CONSTANT [8] { value: 4 } + accu_var: @ac:0:0 + accu_init: { + LIST [7] { + elements: { + } + } } - } - CREATE_LIST [9] { - elements: { - IDENT [10] { - name: @index1 + loop_condition: { + CONSTANT [8] { value: true } + } + loop_step: { + CALL [9] { + function: _+_ + args: { + IDENT [10] { + name: @ac:0:0 + } + LIST [11] { + elements: { + LIST [12] { + elements: { + CALL [13] { + function: _+_ + args: { + IDENT [14] { + name: @it:0:0 + } + IDENT [15] { + name: @it:0:0 + } + } + } + CALL [16] { + function: _+_ + args: { + IDENT [17] { + name: @it:0:0 + } + IDENT [18] { + name: @it:0:0 + } + } + } + } + } + } + } + } } - IDENT [11] { - name: @index1 + } + result: { + IDENT [19] { + name: @ac:0:0 } } } - CREATE_LIST [12] { + } + } + COMPREHENSION [20] { + iter_var: @it:1:0 + iter_range: { + IDENT [21] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [22] { elements: { - IDENT [13] { - name: @index2 - } - IDENT [14] { - name: @index2 - } } } - CALL [15] { + } + loop_condition: { + CONSTANT [23] { value: true } + } + loop_step: { + CALL [24] { function: _+_ args: { - IDENT [16] { - name: @x0:0 + IDENT [25] { + name: @ac:1:0 } - CREATE_LIST [17] { + LIST [26] { elements: { - COMPREHENSION [18] { - iter_var: @c1:0 - iter_range: { - IDENT [19] { - name: @index0 - } - } - accu_var: @x1:0 - accu_init: { - CREATE_LIST [20] { - elements: { + LIST [27] { + elements: { + CALL [28] { + function: _+_ + args: { + IDENT [29] { + name: @it:1:0 + } + IDENT [30] { + name: @it:1:0 + } } } - } - loop_condition: { - CONSTANT [21] { value: true } - } - loop_step: { - CALL [22] { + CALL [31] { function: _+_ args: { - IDENT [23] { - name: @x1:0 + IDENT [32] { + name: @it:1:0 } - CREATE_LIST [24] { - elements: { - IDENT [25] { - name: @index1 - } - } + IDENT [33] { + name: @it:1:0 } } } } - result: { - IDENT [26] { - name: @x1:0 - } - } } } } } } - COMPREHENSION [27] { - iter_var: @c0:0 - iter_range: { - IDENT [28] { - name: @index0 - } - } - accu_var: @x0:0 - accu_init: { - CREATE_LIST [29] { - elements: { - } - } - } - loop_condition: { - CONSTANT [30] { value: true } - } - loop_step: { - IDENT [31] { - name: @index4 - } - } - result: { - IDENT [32] { - name: @x0:0 - } - } - } } - } - CALL [33] { - function: _==_ - args: { + result: { IDENT [34] { - name: @index5 - } - IDENT [35] { - name: @index3 + name: @ac:1:0 } } } } } -Test case: MACRO_SHADOWED_VARIABLE -Source: [x - 1 > 3 ? x - 1 : 5].exists(x, x - 1 > 3) || x - 1 > 3 +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 =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { - function: _-_ - args: { - IDENT [4] { - name: x - } - CONSTANT [5] { value: 1 } - } - } - CALL [6] { function: _>_ args: { - IDENT [7] { - name: @index0 + CALL [4] { + function: _-_ + args: { + CALL [5] { + function: _-_ + args: { + IDENT [6] { + name: x + } + IDENT [7] { + name: y + } + } + } + CONSTANT [8] { value: 1 } + } } - CONSTANT [8] { value: 3 } + CONSTANT [9] { value: 3 } } } - COMPREHENSION [9] { - iter_var: @c0:0 + } + } + CALL [10] { + function: _||_ + args: { + COMPREHENSION [11] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 iter_range: { - CREATE_LIST [10] { + LIST [12] { elements: { - CALL [11] { + CALL [13] { function: _?_:_ args: { - IDENT [12] { - name: @index1 - } - IDENT [13] { + IDENT [14] { name: @index0 } - CONSTANT [14] { value: 5 } + CALL [15] { + function: _-_ + args: { + CALL [16] { + function: _-_ + args: { + IDENT [17] { + name: x + } + IDENT [18] { + name: y + } + } + } + CONSTANT [19] { value: 1 } + } + } + CONSTANT [20] { value: 5 } } } } } } - accu_var: @x0:0 + accu_var: @ac:0:0 accu_init: { - CONSTANT [15] { value: false } + CONSTANT [21] { value: false } } loop_condition: { - CALL [16] { + CALL [22] { function: @not_strictly_false args: { - CALL [17] { + CALL [23] { function: !_ args: { - IDENT [18] { - name: @x0:0 + IDENT [24] { + name: @ac:0:0 } } } @@ -2281,96 +3796,109 @@ CALL [1] { } } loop_step: { - CALL [19] { + CALL [25] { function: _||_ args: { - IDENT [20] { - name: @x0:0 + IDENT [26] { + name: @ac:0:0 } - CALL [21] { + CALL [27] { function: _>_ args: { - CALL [22] { + CALL [28] { function: _-_ args: { - IDENT [23] { - name: @c0:0 + CALL [29] { + function: _-_ + args: { + IDENT [30] { + name: @it:0:0 + } + IDENT [31] { + name: @it2:0:0 + } + } } - CONSTANT [24] { value: 1 } + CONSTANT [32] { value: 1 } } } - CONSTANT [25] { value: 3 } + CONSTANT [33] { value: 3 } } } } } } result: { - IDENT [26] { - name: @x0:0 + IDENT [34] { + name: @ac:0:0 } } } - } - } - CALL [27] { - function: _||_ - args: { - IDENT [28] { - name: @index2 - } - IDENT [29] { - name: @index1 + IDENT [35] { + name: @index0 } } } } } -Test case: MACRO_SHADOWED_VARIABLE_2 -Source: ["foo", "bar"].map(x, [x + x, x + x]).map(x, [x + x, x + x]) +Test case: MACRO_SHADOWED_VARIABLE_COMP_V2_2 +Source: ["foo", "bar"].transformMap(x, y, [x + x, y + y]).transformMap(x, y, [x + x, y + y]) =====> -CALL [1] { - function: cel.@block - args: { - CREATE_LIST [2] { - elements: { - CALL [3] { - function: _+_ - args: { - IDENT [4] { - name: @c1:0 - } - IDENT [5] { - name: @c1:0 - } +COMPREHENSION [35] { + iter_var: x + iter_var2: y + iter_range: { + COMPREHENSION [19] { + iter_var: x + iter_var2: y + iter_range: { + LIST [1] { + elements: { + CONSTANT [2] { value: "foo" } + CONSTANT [3] { value: "bar" } } } - CALL [6] { - function: _+_ - args: { - IDENT [7] { - name: @c0:0 - } - IDENT [8] { - name: @c0:0 - } - } + } + accu_var: @result + accu_init: { + MAP [14] { + } - CALL [9] { - function: _+_ + } + loop_condition: { + CONSTANT [15] { value: true } + } + loop_step: { + CALL [17] { + function: cel.@mapInsert args: { - IDENT [10] { - name: @x0:0 + IDENT [16] { + name: @result + } + IDENT [5] { + name: x } - CREATE_LIST [11] { + LIST [7] { elements: { - CREATE_LIST [12] { - elements: { - IDENT [13] { - name: @index1 + CALL [9] { + function: _+_ + args: { + IDENT [8] { + name: x } - IDENT [14] { - name: @index1 + IDENT [10] { + name: x + } + } + } + CALL [12] { + function: _+_ + args: { + IDENT [11] { + name: y + } + IDENT [13] { + name: y } } } @@ -2378,85 +3906,65 @@ CALL [1] { } } } - COMPREHENSION [15] { - iter_var: @c1:0 - iter_range: { - CREATE_LIST [16] { - elements: { - CONSTANT [17] { value: "foo" } - CONSTANT [18] { value: "bar" } - } - } - } - accu_var: @x1:0 - accu_init: { - CREATE_LIST [19] { - elements: { + } + result: { + IDENT [18] { + name: @result + } + } + } + } + accu_var: @result + accu_init: { + MAP [30] { + + } + } + loop_condition: { + CONSTANT [31] { value: true } + } + loop_step: { + CALL [33] { + function: cel.@mapInsert + args: { + IDENT [32] { + name: @result + } + IDENT [21] { + name: x + } + LIST [23] { + elements: { + CALL [25] { + function: _+_ + args: { + IDENT [24] { + name: x + } + IDENT [26] { + name: x + } } } - } - loop_condition: { - CONSTANT [20] { value: true } - } - loop_step: { - CALL [21] { + CALL [28] { function: _+_ args: { - IDENT [22] { - name: @x1:0 + IDENT [27] { + name: y } - CREATE_LIST [23] { - elements: { - CREATE_LIST [24] { - elements: { - IDENT [25] { - name: @index0 - } - IDENT [26] { - name: @index0 - } - } - } - } + IDENT [29] { + name: y } } } } - result: { - IDENT [27] { - name: @x1:0 - } - } } } } - COMPREHENSION [28] { - iter_var: @c0:0 - iter_range: { - IDENT [29] { - name: @index3 - } - } - accu_var: @x0:0 - accu_init: { - CREATE_LIST [30] { - elements: { - } - } - } - loop_condition: { - CONSTANT [31] { value: true } - } - loop_step: { - IDENT [32] { - name: @index2 - } - } - result: { - IDENT [33] { - name: @x0:0 - } - } + } + result: { + IDENT [34] { + name: @result } } } @@ -2466,9 +3974,9 @@ Source: has({'a': true}.a) && {'a':true}['a'] CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_MAP [3] { + MAP [3] { MAP_ENTRY [4] { key: { CONSTANT [5] { value: "a" } @@ -2478,27 +3986,59 @@ CALL [1] { } } } - CALL [7] { + } + } + CALL [7] { + function: _&&_ + args: { + SELECT [8] { + IDENT [9] { + name: @index0 + }.a~presence_test + } + CALL [10] { function: _[_] args: { - IDENT [8] { + IDENT [11] { name: @index0 } - CONSTANT [9] { value: "a" } + CONSTANT [12] { value: "a" } } } } } - CALL [10] { + } +} +Test case: PRESENCE_TEST_2 +Source: has({'a': true}.a) && has({'a': true}.a) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + SELECT [3] { + MAP [4] { + MAP_ENTRY [5] { + key: { + CONSTANT [6] { value: "a" } + } + value: { + CONSTANT [7] { value: true } + } + } + }.a~presence_test + } + } + } + CALL [8] { function: _&&_ args: { - SELECT [11] { - IDENT [12] { - name: @index0 - }.a~presence_test + IDENT [9] { + name: @index0 } - IDENT [13] { - name: @index1 + IDENT [10] { + name: @index0 } } } @@ -2510,40 +4050,37 @@ Source: (has(msg.oneof_type.payload) ? msg.oneof_type.payload.single_int64 : 0) CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { name: msg }.oneof_type } - CALL [5] { + } + } + CALL [5] { + function: _==_ + args: { + CALL [6] { function: _?_:_ args: { - SELECT [6] { - IDENT [7] { + SELECT [7] { + IDENT [8] { name: @index0 }.payload~presence_test } - SELECT [8] { - SELECT [9] { - IDENT [10] { + SELECT [9] { + SELECT [10] { + IDENT [11] { name: @index0 }.payload }.single_int64 } - CONSTANT [11] { value: 0 } + CONSTANT [12] { value: 0 } } } - } - } - CALL [12] { - function: _==_ - args: { - IDENT [13] { - name: @index1 - } - CONSTANT [14] { value: 10 } + CONSTANT [13] { value: 10 } } } } @@ -2554,112 +4091,100 @@ Source: (has(msg.oneof_type.payload) ? msg.oneof_type.payload.single_int64 : msg CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload }.single_int64 } - CALL [9] { + } + } + CALL [7] { + function: _==_ + args: { + CALL [8] { function: _?_:_ args: { - SELECT [10] { - IDENT [11] { - name: @index0 + SELECT [9] { + SELECT [10] { + IDENT [11] { + name: msg + }.oneof_type }.payload~presence_test } IDENT [12] { - name: @index2 + name: @index0 } CALL [13] { function: _*_ args: { IDENT [14] { - name: @index2 + name: @index0 } CONSTANT [15] { value: 0 } } } } } - } - } - CALL [16] { - function: _==_ - args: { - IDENT [17] { - name: @index3 - } - CONSTANT [18] { value: 10 } + CONSTANT [16] { value: 10 } } } } } -Test case: PRESENCE_TEST_WITH_TERNARY_3 -Source: (has(msg.oneof_type.payload.single_int64) ? msg.oneof_type.payload.single_int64 : msg.oneof_type.payload.single_int64 * 0) == 10 -=====> -CALL [1] { - function: cel.@block - args: { - CREATE_LIST [2] { - elements: { - SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 +Test case: PRESENCE_TEST_WITH_TERNARY_3 +Source: (has(msg.oneof_type.payload.single_int64) ? msg.oneof_type.payload.single_int64 : msg.oneof_type.payload.single_int64 * 0) == 10 +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + SELECT [3] { + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload }.single_int64 } - CALL [9] { + } + } + CALL [7] { + function: _==_ + args: { + CALL [8] { function: _?_:_ args: { - SELECT [10] { - IDENT [11] { - name: @index1 + SELECT [9] { + SELECT [10] { + SELECT [11] { + IDENT [12] { + name: msg + }.oneof_type + }.payload }.single_int64~presence_test } - IDENT [12] { - name: @index2 + IDENT [13] { + name: @index0 } - CALL [13] { + CALL [14] { function: _*_ args: { - IDENT [14] { - name: @index2 + IDENT [15] { + name: @index0 } - CONSTANT [15] { value: 0 } + CONSTANT [16] { value: 0 } } } } } - } - } - CALL [16] { - function: _==_ - args: { - IDENT [17] { - name: @index3 - } - CONSTANT [18] { value: 10 } + CONSTANT [17] { value: 10 } } } } @@ -2670,91 +4195,88 @@ Source: (has(msg.oneof_type) && has(msg.oneof_type.payload) && has(msg.oneof_typ CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.map_string_string } SELECT [7] { - IDENT [8] { - name: @index1 - }.map_string_string + SELECT [8] { + IDENT [9] { + name: msg + }.oneof_type + }.payload } - CALL [9] { - function: _?_:_ + } + } + CALL [10] { + function: _?_:_ + args: { + CALL [11] { + function: _&&_ args: { - CALL [10] { + CALL [12] { function: _&&_ args: { - SELECT [11] { - IDENT [12] { - name: @index1 - }.map_string_string~presence_test - } SELECT [13] { IDENT [14] { - name: @index2 - }.key~presence_test + name: msg + }.oneof_type~presence_test } - } - } - CALL [15] { - function: _==_ - args: { - SELECT [16] { - IDENT [17] { - name: @index2 - }.key + SELECT [15] { + SELECT [16] { + IDENT [17] { + name: msg + }.oneof_type + }.payload~presence_test } - CONSTANT [18] { value: "A" } } } - CONSTANT [19] { value: false } + SELECT [18] { + IDENT [19] { + name: @index1 + }.single_int64~presence_test + } } } CALL [20] { - function: _&&_ + function: _?_:_ args: { CALL [21] { function: _&&_ args: { SELECT [22] { IDENT [23] { - name: msg - }.oneof_type~presence_test + name: @index1 + }.map_string_string~presence_test } SELECT [24] { IDENT [25] { name: @index0 - }.payload~presence_test + }.key~presence_test } } } - SELECT [26] { - IDENT [27] { - name: @index1 - }.single_int64~presence_test + CALL [26] { + function: _==_ + args: { + SELECT [27] { + IDENT [28] { + name: @index0 + }.key + } + CONSTANT [29] { value: "A" } + } } + CONSTANT [30] { value: false } } } - } - } - CALL [28] { - function: _?_:_ - args: { - IDENT [29] { - name: @index4 - } - IDENT [30] { - name: @index3 - } CONSTANT [31] { value: false } } } @@ -2766,46 +4288,51 @@ Source: [10, ?optional.none(), [?optional.none(), ?opt_x], [?optional.none(), ?o CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CALL [3] { - function: optional.none - args: { - } - } - CREATE_LIST [4] { + LIST [3] { elements: { - IDENT [5] { - name: @index0 + CALL [4] { + function: optional.none + args: { + } } - IDENT [6] { + IDENT [5] { name: opt_x } } optional_indices: [0, 1] } - CREATE_LIST [7] { + LIST [6] { elements: { - CONSTANT [8] { value: 5 } + CONSTANT [7] { value: 5 } } } - CREATE_LIST [9] { + } + } + CALL [8] { + function: _==_ + args: { + LIST [9] { elements: { CONSTANT [10] { value: 10 } - IDENT [11] { - name: @index2 + CALL [11] { + function: optional.none + args: { + } } IDENT [12] { - name: @index2 + name: @index0 + } + IDENT [13] { + name: @index0 } } + optional_indices: [0] } - CREATE_LIST [13] { + LIST [14] { elements: { - CONSTANT [14] { value: 10 } - IDENT [15] { - name: @index0 - } + CONSTANT [15] { value: 10 } IDENT [16] { name: @index1 } @@ -2813,18 +4340,6 @@ CALL [1] { name: @index1 } } - optional_indices: [0] - } - } - } - CALL [18] { - function: _==_ - args: { - IDENT [19] { - name: @index4 - } - IDENT [20] { - name: @index3 } } } @@ -2836,56 +4351,47 @@ Source: {?'hello': optional.of('hello')}['hello'] + {?'hello': optional.of('hell CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { - function: optional.of - args: { - CONSTANT [4] { value: "hello" } - } - } - CREATE_MAP [5] { - MAP_ENTRY [6] { - key: { - CONSTANT [7] { value: "hello" } - } - optional_entry: true - value: { - IDENT [8] { - name: @index0 - } - } - } - } - CALL [9] { function: _[_] args: { - IDENT [10] { - name: @index1 + MAP [4] { + MAP_ENTRY [5] { + key: { + CONSTANT [6] { value: "hello" } + } + optional_entry: true + value: { + CALL [7] { + function: optional.of + args: { + CONSTANT [8] { value: "hello" } + } + } + } + } } - CONSTANT [11] { value: "hello" } + CONSTANT [9] { value: "hello" } } } - CALL [12] { + } + } + CALL [10] { + function: _==_ + args: { + CALL [11] { function: _+_ args: { - IDENT [13] { - name: @index2 + IDENT [12] { + name: @index0 } - IDENT [14] { - name: @index2 + IDENT [13] { + name: @index0 } } } - } - } - CALL [15] { - function: _==_ - args: { - IDENT [16] { - name: @index3 - } - CONSTANT [17] { value: "hellohello" } + CONSTANT [14] { value: "hellohello" } } } } @@ -2896,9 +4402,9 @@ Source: {?'key': optional.of('test')}[?'bogus'].or({'key': 'test'}[?'bogus']).or CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_MAP [3] { + MAP [3] { MAP_ENTRY [4] { key: { CONSTANT [5] { value: "key" } @@ -2917,7 +4423,7 @@ CALL [1] { CALL [9] { function: _[?_] args: { - CREATE_MAP [10] { + MAP [10] { MAP_ENTRY [11] { key: { CONSTANT [12] { value: "key" } @@ -2981,146 +4487,69 @@ Source: TestAllTypes{?single_int64: optional.ofNonZeroValue(1), ?single_int32: o CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { - elements: { - CALL [3] { - function: optional.ofNonZeroValue - args: { - CONSTANT [4] { value: 1 } - } - } - CALL [5] { - function: optional.of - args: { - CONSTANT [6] { value: 4 } - } - } - CREATE_STRUCT [7] { - name: TestAllTypes - entries: { - ENTRY [8] { - field_key: single_int64 - optional_entry: true - value: { - IDENT [9] { - name: @index0 - } - } - } - ENTRY [10] { - field_key: single_int32 - optional_entry: true - value: { - IDENT [11] { - name: @index1 - } - } - } - } - } - CALL [12] { - function: _+_ - args: { - SELECT [13] { - IDENT [14] { - name: @index2 - }.single_int32 - } - SELECT [15] { - IDENT [16] { - name: @index2 - }.single_int64 - } - } - } - } - } - CALL [17] { - function: _==_ - args: { - IDENT [18] { - name: @index3 - } - CONSTANT [19] { value: 5 } - } - } - } -} -Test case: CALL -Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('h' + 'e' + 'l' + 'l' + 'o') -=====> -CALL [1] { - function: cel.@block - args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CALL [3] { - function: _+_ - args: { - CONSTANT [4] { value: "h" } - CONSTANT [5] { value: "e" } - } - } - CALL [6] { - function: _+_ - args: { - IDENT [7] { - name: @index0 + STRUCT [3] { + name: cel.expr.conformance.proto3.TestAllTypes + entries: { + ENTRY [4] { + field_key: single_int64 + optional_entry: true + value: { + CALL [5] { + function: optional.ofNonZeroValue + args: { + CONSTANT [6] { value: 1 } + } + } + } } - CONSTANT [8] { value: "l" } - } - } - CALL [9] { - function: _+_ - args: { - IDENT [10] { - name: @index1 + ENTRY [7] { + field_key: single_int32 + optional_entry: true + value: { + CALL [8] { + function: optional.of + args: { + CONSTANT [9] { value: 4 } + } + } + } } - CONSTANT [11] { value: "l" } } } - CALL [12] { + } + } + CALL [10] { + function: _==_ + args: { + CALL [11] { function: _+_ args: { - IDENT [13] { - name: @index2 + SELECT [12] { + IDENT [13] { + name: @index0 + }.single_int32 } - CONSTANT [14] { value: "o" } - } - } - CALL [15] { - function: _+_ - args: { - IDENT [16] { - name: @index3 + SELECT [14] { + IDENT [15] { + name: @index0 + }.single_int64 } - CONSTANT [17] { value: " world" } } } - } - } - CALL [18] { - function: matches - target: { - IDENT [19] { - name: @index4 - } - } - args: { - IDENT [20] { - name: @index3 - } + CONSTANT [16] { value: 5 } } } } } -Test case: CALL_ARGUMENT_NESTED_NO_COMMON_SUBEXPR -Source: 'hello world'.matches('h' + 'e' + 'l' + 'l' + 'o') +Test case: CALL +Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('h' + 'e' + 'l' + 'l' + 'o') =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { function: _+_ @@ -3152,23 +4581,67 @@ CALL [1] { CALL [12] { function: matches target: { - CONSTANT [13] { value: "hello world" } + CALL [13] { + function: _+_ + args: { + IDENT [14] { + name: @index0 + } + CONSTANT [15] { value: " world" } + } + } } args: { - IDENT [14] { + IDENT [16] { name: @index0 } } } } } +Test case: CALL_ARGUMENT_NESTED_NO_COMMON_SUBEXPR +Source: 'hello world'.matches('h' + 'e' + 'l' + 'l' + 'o') +=====> +CALL [2] { + function: matches + target: { + CONSTANT [1] { value: "hello world" } + } + args: { + CALL [10] { + function: _+_ + args: { + CALL [8] { + function: _+_ + args: { + CALL [6] { + function: _+_ + args: { + CALL [4] { + function: _+_ + args: { + CONSTANT [3] { value: "h" } + CONSTANT [5] { value: "e" } + } + } + CONSTANT [7] { value: "l" } + } + } + CONSTANT [9] { value: "l" } + } + } + CONSTANT [11] { value: "o" } + } + } + } +} Test case: CALL_TARGET_NESTED_NO_COMMON_SUBEXPR Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('hello') =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { function: _+_ @@ -3222,7 +4695,7 @@ Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('w' + 'o' + 'r' + 'l' + CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { function: _+_ @@ -3236,65 +4709,62 @@ CALL [1] { CALL [6] { function: _+_ args: { - CONSTANT [7] { value: "w" } - CONSTANT [8] { value: "o" } + CALL [7] { + function: _+_ + args: { + CONSTANT [8] { value: "h" } + CONSTANT [9] { value: "e" } + } + } + CONSTANT [10] { value: "l" } } } - CONSTANT [9] { value: "r" } + CONSTANT [11] { value: "l" } } } - CONSTANT [10] { value: "l" } + CONSTANT [12] { value: "o" } } } - CONSTANT [11] { value: "d" } + CONSTANT [13] { value: " world" } } } - CALL [12] { + } + } + CALL [14] { + function: matches + target: { + IDENT [15] { + name: @index0 + } + } + args: { + CALL [16] { function: _+_ args: { - CALL [13] { + CALL [17] { function: _+_ args: { - CALL [14] { + CALL [18] { function: _+_ args: { - CALL [15] { + CALL [19] { function: _+_ args: { - CALL [16] { - function: _+_ - args: { - CONSTANT [17] { value: "h" } - CONSTANT [18] { value: "e" } - } - } - CONSTANT [19] { value: "l" } + CONSTANT [20] { value: "w" } + CONSTANT [21] { value: "o" } } } - CONSTANT [20] { value: "l" } + CONSTANT [22] { value: "r" } } } - CONSTANT [21] { value: "o" } + CONSTANT [23] { value: "l" } } } - CONSTANT [22] { value: " world" } + CONSTANT [24] { value: "d" } } } } } - CALL [23] { - function: matches - target: { - IDENT [24] { - name: @index1 - } - } - args: { - IDENT [25] { - name: @index0 - } - } - } } } Test case: CUSTOM_FUNCTION_INELIMINABLE @@ -3303,77 +4773,69 @@ Source: non_pure_custom_func(msg.oneof_type.payload.single_int64) + non_pure_cus CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 - }.single_int64 - } - SELECT [9] { - IDENT [10] { - name: msg + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload }.single_int64 } - SELECT [11] { - IDENT [12] { - name: @index1 - }.single_int32 - } } } - CALL [13] { + CALL [7] { function: _+_ args: { - CALL [14] { + CALL [8] { function: _+_ args: { - CALL [15] { + CALL [9] { function: _+_ args: { - CALL [16] { + CALL [10] { function: non_pure_custom_func args: { - IDENT [17] { - name: @index2 + IDENT [11] { + name: @index0 } } } - CALL [18] { + CALL [12] { function: non_pure_custom_func args: { - IDENT [19] { - name: @index4 + SELECT [13] { + SELECT [14] { + SELECT [15] { + IDENT [16] { + name: msg + }.oneof_type + }.payload + }.single_int32 } } } } } - CALL [20] { + CALL [17] { function: non_pure_custom_func args: { - IDENT [21] { - name: @index2 + IDENT [18] { + name: @index0 } } } } } - CALL [22] { + CALL [19] { function: non_pure_custom_func args: { - IDENT [23] { - name: @index3 + SELECT [20] { + IDENT [21] { + name: msg + }.single_int64 } } } @@ -3387,79 +4849,71 @@ Source: pure_custom_func(msg.oneof_type.payload.single_int64) + pure_custom_func CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 - }.single_int64 - } - CALL [9] { - function: pure_custom_func - args: { - IDENT [10] { - name: @index2 - } - } - } - CALL [11] { + CALL [3] { function: pure_custom_func args: { - SELECT [12] { - IDENT [13] { - name: msg + SELECT [4] { + SELECT [5] { + SELECT [6] { + IDENT [7] { + name: msg + }.oneof_type + }.payload }.single_int64 } } } - CALL [14] { + CALL [8] { function: _+_ args: { - CALL [15] { - function: _+_ + IDENT [9] { + name: @index0 + } + CALL [10] { + function: pure_custom_func args: { - IDENT [16] { - name: @index3 - } - CALL [17] { - function: pure_custom_func - args: { - SELECT [18] { - IDENT [19] { - name: @index1 - }.single_int32 - } - } + SELECT [11] { + SELECT [12] { + SELECT [13] { + IDENT [14] { + name: msg + }.oneof_type + }.payload + }.single_int32 } } } - IDENT [20] { - name: @index3 - } } } } } - CALL [21] { + CALL [15] { function: _+_ args: { - IDENT [22] { - name: @index5 + CALL [16] { + function: _+_ + args: { + IDENT [17] { + name: @index1 + } + IDENT [18] { + name: @index0 + } + } } - IDENT [23] { - name: @index4 + CALL [19] { + function: pure_custom_func + args: { + SELECT [20] { + IDENT [21] { + name: msg + }.single_int64 + } + } } } } } -} +} \ No newline at end of file diff --git a/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_6.baseline b/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_6.baseline index a2ecbc777..f13d51e99 100644 --- a/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_6.baseline +++ b/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_6.baseline @@ -4,22 +4,24 @@ Source: size([1,2]) + size([1,2]) + 1 == 5 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - CONSTANT [5] { value: 2 } - } - } - CALL [6] { + CALL [3] { function: size args: { - IDENT [7] { - name: @index0 + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + CONSTANT [6] { value: 2 } + } } } } + } + } + CALL [7] { + function: _==_ + args: { CALL [8] { function: _+_ args: { @@ -27,25 +29,17 @@ CALL [1] { function: _+_ args: { IDENT [10] { - name: @index1 + name: @index0 } IDENT [11] { - name: @index1 + name: @index0 } } } CONSTANT [12] { value: 1 } } } - } - } - CALL [13] { - function: _==_ - args: { - IDENT [14] { - name: @index2 - } - CONSTANT [15] { value: 5 } + CONSTANT [13] { value: 5 } } } } @@ -56,22 +50,24 @@ Source: 2 + size([1,2]) + size([1,2]) + 1 == 7 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - CONSTANT [5] { value: 2 } - } - } - CALL [6] { + CALL [3] { function: size args: { - IDENT [7] { - name: @index0 + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + CONSTANT [6] { value: 2 } + } } } } + } + } + CALL [7] { + function: _==_ + args: { CALL [8] { function: _+_ args: { @@ -83,27 +79,19 @@ CALL [1] { args: { CONSTANT [11] { value: 2 } IDENT [12] { - name: @index1 + name: @index0 } } } IDENT [13] { - name: @index1 + name: @index0 } } } CONSTANT [14] { value: 1 } } } - } - } - CALL [15] { - function: _==_ - args: { - IDENT [16] { - name: @index2 - } - CONSTANT [17] { value: 7 } + CONSTANT [15] { value: 7 } } } } @@ -114,71 +102,62 @@ Source: size([0]) + size([0]) + size([1,2]) + size([1,2]) == 6 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 0 } - } - } - CALL [5] { + CALL [3] { function: size args: { - IDENT [6] { - name: @index0 + LIST [4] { + elements: { + CONSTANT [5] { value: 0 } + } } } } - CREATE_LIST [7] { - elements: { - CONSTANT [8] { value: 1 } - CONSTANT [9] { value: 2 } - } - } - CALL [10] { + CALL [6] { function: size args: { - IDENT [11] { - name: @index2 + LIST [7] { + elements: { + CONSTANT [8] { value: 1 } + CONSTANT [9] { value: 2 } + } } } } - CALL [12] { + } + } + CALL [10] { + function: _==_ + args: { + CALL [11] { function: _+_ args: { - CALL [13] { + CALL [12] { function: _+_ args: { - CALL [14] { + CALL [13] { function: _+_ args: { - IDENT [15] { - name: @index1 + IDENT [14] { + name: @index0 } - IDENT [16] { - name: @index1 + IDENT [15] { + name: @index0 } } } - IDENT [17] { - name: @index3 + IDENT [16] { + name: @index1 } } } - IDENT [18] { - name: @index3 + IDENT [17] { + name: @index1 } } } - } - } - CALL [19] { - function: _==_ - args: { - IDENT [20] { - name: @index4 - } - CONSTANT [21] { value: 6 } + CONSTANT [18] { value: 6 } } } } @@ -189,108 +168,99 @@ Source: 5 + size([0]) + size([0]) + size([1,2]) + size([1,2]) + size([1,2,3]) + CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 0 } - } - } - CALL [5] { + CALL [3] { function: size args: { - IDENT [6] { - name: @index0 + LIST [4] { + elements: { + CONSTANT [5] { value: 0 } + } } } } - CREATE_LIST [7] { - elements: { - CONSTANT [8] { value: 1 } - CONSTANT [9] { value: 2 } - } - } - CALL [10] { + CALL [6] { function: size args: { - IDENT [11] { - name: @index2 + LIST [7] { + elements: { + CONSTANT [8] { value: 1 } + CONSTANT [9] { value: 2 } + } } } } - CREATE_LIST [12] { - elements: { - CONSTANT [13] { value: 1 } - CONSTANT [14] { value: 2 } - CONSTANT [15] { value: 3 } - } - } - CALL [16] { + CALL [10] { function: size args: { - IDENT [17] { - name: @index4 + LIST [11] { + elements: { + CONSTANT [12] { value: 1 } + CONSTANT [13] { value: 2 } + CONSTANT [14] { value: 3 } + } } } } - CALL [18] { + CALL [15] { function: _+_ args: { - CALL [19] { + CALL [16] { function: _+_ args: { - CALL [20] { + CALL [17] { function: _+_ args: { - CALL [21] { + CALL [18] { function: _+_ args: { - CALL [22] { + CALL [19] { function: _+_ args: { - CALL [23] { + CALL [20] { function: _+_ args: { - CONSTANT [24] { value: 5 } - IDENT [25] { - name: @index1 + CONSTANT [21] { value: 5 } + IDENT [22] { + name: @index0 } } } - IDENT [26] { - name: @index1 + IDENT [23] { + name: @index0 } } } - IDENT [27] { - name: @index3 + IDENT [24] { + name: @index1 } } } - IDENT [28] { - name: @index3 + IDENT [25] { + name: @index1 } } } - IDENT [29] { - name: @index5 + IDENT [26] { + name: @index2 } } } - IDENT [30] { - name: @index5 + IDENT [27] { + name: @index2 } } } } } - CALL [31] { + CALL [28] { function: _==_ args: { - IDENT [32] { - name: @index6 + IDENT [29] { + name: @index3 } - CONSTANT [33] { value: 17 } + CONSTANT [30] { value: 17 } } } } @@ -301,149 +271,109 @@ Source: timestamp(int(timestamp(1000000000))).getFullYear() + timestamp(int(time CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { - function: timestamp - args: { - CONSTANT [4] { value: 1000000000 } - } - } - CALL [5] { - function: int - args: { - IDENT [6] { - name: @index0 + function: getFullYear + target: { + CALL [4] { + function: timestamp + args: { + CALL [5] { + function: int + args: { + CALL [6] { + function: timestamp + args: { + CONSTANT [7] { value: 1000000000 } + } + } + } + } + } } } - } - CALL [7] { - function: timestamp args: { - IDENT [8] { - name: @index1 - } } } - CALL [9] { + CALL [8] { function: getFullYear target: { - IDENT [10] { - name: @index2 + CALL [9] { + function: timestamp + args: { + CALL [10] { + function: int + args: { + CALL [11] { + function: timestamp + args: { + CONSTANT [12] { value: 200 } + } + } + } + } + } } } args: { } } - CALL [11] { - function: timestamp - args: { - CONSTANT [12] { value: 50 } - } - } CALL [13] { - function: int - args: { - IDENT [14] { - name: @index4 - } - } - } - CALL [15] { function: timestamp args: { - IDENT [16] { - name: @index5 + CALL [14] { + function: int + args: { + CALL [15] { + function: timestamp + args: { + CONSTANT [16] { value: 50 } + } + } + } } } } CALL [17] { function: timestamp args: { - CONSTANT [18] { value: 200 } - } - } - CALL [19] { - function: int - args: { - IDENT [20] { - name: @index7 + CALL [18] { + function: int + args: { + CALL [19] { + function: timestamp + args: { + CONSTANT [20] { value: 75 } + } + } + } } } } CALL [21] { - function: timestamp - args: { - IDENT [22] { - name: @index8 - } - } - } - CALL [23] { - function: getFullYear - target: { - IDENT [24] { - name: @index9 - } - } - args: { - } - } - CALL [25] { - function: timestamp - args: { - CONSTANT [26] { value: 75 } - } - } - CALL [27] { - function: int - args: { - IDENT [28] { - name: @index11 - } - } - } - CALL [29] { - function: timestamp - args: { - IDENT [30] { - name: @index12 - } - } - } - CALL [31] { - function: getMinutes - target: { - IDENT [32] { - name: @index13 - } - } - args: { - } - } - CALL [33] { function: _+_ args: { - CALL [34] { + CALL [22] { function: _+_ args: { - CALL [35] { + CALL [23] { function: _+_ args: { - CALL [36] { + CALL [24] { function: _+_ args: { - CALL [37] { + CALL [25] { function: _+_ args: { - IDENT [38] { - name: @index3 + IDENT [26] { + name: @index0 } - CALL [39] { + CALL [27] { function: getFullYear target: { - IDENT [40] { - name: @index13 + IDENT [28] { + name: @index3 } } args: { @@ -451,11 +381,11 @@ CALL [1] { } } } - CALL [41] { + CALL [29] { function: getFullYear target: { - IDENT [42] { - name: @index6 + IDENT [30] { + name: @index2 } } args: { @@ -463,16 +393,16 @@ CALL [1] { } } } - IDENT [43] { - name: @index3 + IDENT [31] { + name: @index0 } } } - CALL [44] { + CALL [32] { function: getSeconds target: { - IDENT [45] { - name: @index6 + IDENT [33] { + name: @index2 } } args: { @@ -480,47 +410,51 @@ CALL [1] { } } } - IDENT [46] { - name: @index10 + IDENT [34] { + name: @index1 } } } - CALL [47] { - function: _+_ + } + } + CALL [35] { + function: _==_ + args: { + CALL [36] { + function: _+_ args: { - CALL [48] { + CALL [37] { function: _+_ args: { - CALL [49] { + CALL [38] { function: _+_ args: { - IDENT [50] { - name: @index15 + IDENT [39] { + name: @index4 } - IDENT [51] { - name: @index10 + IDENT [40] { + name: @index1 } } } - IDENT [52] { - name: @index14 + CALL [41] { + function: getMinutes + target: { + IDENT [42] { + name: @index3 + } + } + args: { + } } } } - IDENT [53] { - name: @index3 + IDENT [43] { + name: @index0 } } } - } - } - CALL [54] { - function: _==_ - args: { - IDENT [55] { - name: @index16 - } - CONSTANT [56] { value: 13934 } + CONSTANT [44] { value: 13934 } } } } @@ -531,55 +465,49 @@ Source: {"a": 2}["a"] + {"a": 2}["a"] * {"a": 2}["a"] == 6 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_MAP [3] { - MAP_ENTRY [4] { - key: { - CONSTANT [5] { value: "a" } - } - value: { - CONSTANT [6] { value: 2 } - } - } - } - CALL [7] { + CALL [3] { function: _[_] args: { - IDENT [8] { - name: @index0 + MAP [4] { + MAP_ENTRY [5] { + key: { + CONSTANT [6] { value: "a" } + } + value: { + CONSTANT [7] { value: 2 } + } + } } - CONSTANT [9] { value: "a" } + CONSTANT [8] { value: "a" } } } + } + } + CALL [9] { + function: _==_ + args: { CALL [10] { function: _+_ args: { IDENT [11] { - name: @index1 + name: @index0 } CALL [12] { function: _*_ args: { IDENT [13] { - name: @index1 + name: @index0 } IDENT [14] { - name: @index1 + name: @index0 } } } } } - } - } - CALL [15] { - function: _==_ - args: { - IDENT [16] { - name: @index2 - } - CONSTANT [17] { value: 6 } + CONSTANT [15] { value: 6 } } } } @@ -590,56 +518,53 @@ Source: {'a': {'b': 1}, 'c': {'b': 1}, 'd': {'e': {'b': 1}}, 'e': {'e': {'b': 1} CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_MAP [3] { + MAP [3] { MAP_ENTRY [4] { key: { - CONSTANT [5] { value: "b" } + CONSTANT [5] { value: "e" } } value: { - CONSTANT [6] { value: 1 } + MAP [6] { + MAP_ENTRY [7] { + key: { + CONSTANT [8] { value: "b" } + } + value: { + CONSTANT [9] { value: 1 } + } + } + } } } } - CREATE_MAP [7] { - MAP_ENTRY [8] { + MAP [10] { + MAP_ENTRY [11] { key: { - CONSTANT [9] { value: "e" } + CONSTANT [12] { value: "b" } } value: { - IDENT [10] { - name: @index0 - } + CONSTANT [13] { value: 1 } } } } } } - CREATE_MAP [11] { - MAP_ENTRY [12] { - key: { - CONSTANT [13] { value: "a" } - } - value: { - IDENT [14] { - name: @index0 - } - } - } + MAP [14] { MAP_ENTRY [15] { key: { - CONSTANT [16] { value: "c" } + CONSTANT [16] { value: "a" } } value: { IDENT [17] { - name: @index0 + name: @index1 } } } MAP_ENTRY [18] { key: { - CONSTANT [19] { value: "d" } + CONSTANT [19] { value: "c" } } value: { IDENT [20] { @@ -649,11 +574,21 @@ CALL [1] { } MAP_ENTRY [21] { key: { - CONSTANT [22] { value: "e" } + CONSTANT [22] { value: "d" } } value: { IDENT [23] { - name: @index1 + name: @index0 + } + } + } + MAP_ENTRY [24] { + key: { + CONSTANT [25] { value: "e" } + } + value: { + IDENT [26] { + name: @index0 } } } @@ -666,9 +601,9 @@ Source: [1, [1,2,3,4], 2, [1,2,3,4], 5, [1,2,3,4], 7, [[1,2], [1,2,3,4]], [1,2]] CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { + LIST [3] { elements: { CONSTANT [4] { value: 1 } CONSTANT [5] { value: 2 } @@ -676,43 +611,40 @@ CALL [1] { CONSTANT [7] { value: 4 } } } - CREATE_LIST [8] { + LIST [8] { elements: { CONSTANT [9] { value: 1 } CONSTANT [10] { value: 2 } } } - CREATE_LIST [11] { - elements: { - IDENT [12] { - name: @index1 - } - IDENT [13] { - name: @index0 - } - } - } } } - CREATE_LIST [14] { + LIST [11] { elements: { - CONSTANT [15] { value: 1 } - IDENT [16] { + CONSTANT [12] { value: 1 } + IDENT [13] { name: @index0 } - CONSTANT [17] { value: 2 } - IDENT [18] { + CONSTANT [14] { value: 2 } + IDENT [15] { name: @index0 } - CONSTANT [19] { value: 5 } - IDENT [20] { + CONSTANT [16] { value: 5 } + IDENT [17] { name: @index0 } - CONSTANT [21] { value: 7 } - IDENT [22] { - name: @index2 + CONSTANT [18] { value: 7 } + LIST [19] { + elements: { + IDENT [20] { + name: @index1 + } + IDENT [21] { + name: @index0 + } + } } - IDENT [23] { + IDENT [22] { name: @index1 } } @@ -725,33 +657,30 @@ Source: msg.single_int64 + msg.single_int64 == 6 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { name: msg }.single_int64 } - CALL [5] { + } + } + CALL [5] { + function: _==_ + args: { + CALL [6] { function: _+_ args: { - IDENT [6] { + IDENT [7] { name: @index0 } - IDENT [7] { + IDENT [8] { name: @index0 } } } - } - } - CALL [8] { - function: _==_ - args: { - IDENT [9] { - name: @index1 - } - CONSTANT [10] { value: 6 } + CONSTANT [9] { value: 6 } } } } @@ -762,61 +691,67 @@ Source: msg.oneof_type.payload.single_int64 + msg.oneof_type.payload.single_int3 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.single_int64 } SELECT [7] { - IDENT [8] { - name: @index1 - }.single_int64 + SELECT [8] { + IDENT [9] { + name: msg + }.oneof_type + }.payload } - CALL [9] { + } + } + CALL [10] { + function: _==_ + args: { + CALL [11] { function: _+_ args: { - CALL [10] { + CALL [12] { function: _+_ args: { - CALL [11] { + CALL [13] { function: _+_ args: { - CALL [12] { + CALL [14] { function: _+_ args: { - IDENT [13] { - name: @index2 + IDENT [15] { + name: @index0 } - SELECT [14] { - IDENT [15] { + SELECT [16] { + IDENT [17] { name: @index1 }.single_int32 } } } - IDENT [16] { - name: @index2 + IDENT [18] { + name: @index0 } } } - SELECT [17] { - IDENT [18] { + SELECT [19] { + IDENT [20] { name: msg }.single_int64 } } } - SELECT [19] { - SELECT [20] { - SELECT [21] { - IDENT [22] { + SELECT [21] { + SELECT [22] { + SELECT [23] { + IDENT [24] { name: @index1 }.oneof_type }.payload @@ -824,14 +759,6 @@ CALL [1] { } } } - } - } - CALL [23] { - function: _==_ - args: { - IDENT [24] { - name: @index3 - } CONSTANT [25] { value: 31 } } } @@ -843,54 +770,36 @@ Source: true || msg.oneof_type.payload.oneof_type.payload.oneof_type.payload.one CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 - }.oneof_type - } - SELECT [9] { - IDENT [10] { - name: @index2 - }.payload - } - SELECT [11] { - IDENT [12] { - name: @index3 - }.oneof_type - } - SELECT [13] { - SELECT [14] { - SELECT [15] { - SELECT [16] { - IDENT [17] { - name: @index4 - }.child - }.child + SELECT [4] { + SELECT [5] { + SELECT [6] { + SELECT [7] { + IDENT [8] { + name: msg + }.oneof_type + }.payload + }.oneof_type }.payload - }.single_bool + }.oneof_type } - CALL [18] { + } + } + CALL [9] { + function: _||_ + args: { + CALL [10] { function: _||_ args: { - CONSTANT [19] { value: true } - SELECT [20] { - SELECT [21] { - SELECT [22] { - SELECT [23] { - IDENT [24] { - name: @index4 + CONSTANT [11] { value: true } + SELECT [12] { + SELECT [13] { + SELECT [14] { + SELECT [15] { + IDENT [16] { + name: @index0 }.payload }.oneof_type }.payload @@ -898,16 +807,16 @@ CALL [1] { } } } - } - } - CALL [25] { - function: _||_ - args: { - IDENT [26] { - name: @index6 - } - IDENT [27] { - name: @index5 + SELECT [17] { + SELECT [18] { + SELECT [19] { + SELECT [20] { + IDENT [21] { + name: @index0 + }.child + }.child + }.payload + }.single_bool } } } @@ -919,60 +828,48 @@ Source: msg.oneof_type.payload.map_int32_int64[1] + msg.oneof_type.payload.map_i CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 - }.map_int32_int64 - } - CALL [9] { + CALL [3] { function: _[_] args: { - IDENT [10] { - name: @index2 + SELECT [4] { + SELECT [5] { + SELECT [6] { + IDENT [7] { + name: msg + }.oneof_type + }.payload + }.map_int32_int64 } - CONSTANT [11] { value: 1 } + CONSTANT [8] { value: 1 } } } - CALL [12] { + } + } + CALL [9] { + function: _==_ + args: { + CALL [10] { function: _+_ args: { - CALL [13] { + CALL [11] { function: _+_ args: { - IDENT [14] { - name: @index3 + IDENT [12] { + name: @index0 } - IDENT [15] { - name: @index3 + IDENT [13] { + name: @index0 } } } - IDENT [16] { - name: @index3 + IDENT [14] { + name: @index0 } } } - } - } - CALL [17] { - function: _==_ - args: { - IDENT [18] { - name: @index4 - } - CONSTANT [19] { value: 15 } + CONSTANT [15] { value: 15 } } } } @@ -983,69 +880,60 @@ Source: msg.oneof_type.payload.map_int32_int64[0] + msg.oneof_type.payload.map_i CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload }.map_int32_int64 } - CALL [9] { + } + } + CALL [7] { + function: _==_ + args: { + CALL [8] { function: _+_ args: { - CALL [10] { + CALL [9] { function: _+_ args: { - CALL [11] { + CALL [10] { function: _[_] args: { - IDENT [12] { - name: @index2 + IDENT [11] { + name: @index0 } - CONSTANT [13] { value: 0 } + CONSTANT [12] { value: 0 } } } - CALL [14] { + CALL [13] { function: _[_] args: { - IDENT [15] { - name: @index2 + IDENT [14] { + name: @index0 } - CONSTANT [16] { value: 1 } + CONSTANT [15] { value: 1 } } } } } - CALL [17] { + CALL [16] { function: _[_] args: { - IDENT [18] { - name: @index2 + IDENT [17] { + name: @index0 } - CONSTANT [19] { value: 2 } + CONSTANT [18] { value: 2 } } } } } - } - } - CALL [20] { - function: _==_ - args: { - IDENT [21] { - name: @index3 - } - CONSTANT [22] { value: 8 } + CONSTANT [19] { value: 8 } } } } @@ -1056,7 +944,7 @@ Source: msg.oneof_type.payload.oneof_type.payload.oneof_type.payload.oneof_type. CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { SELECT [4] { @@ -1073,18 +961,15 @@ CALL [1] { }.oneof_type }.payload } - SELECT [10] { - SELECT [11] { - IDENT [12] { - name: @index0 - }.oneof_type - }.payload - } } } - SELECT [13] { - IDENT [14] { - name: @index1 + SELECT [10] { + SELECT [11] { + SELECT [12] { + IDENT [13] { + name: @index0 + }.oneof_type + }.payload }.single_int64 } } @@ -1095,40 +980,37 @@ Source: (msg.single_int64 > 0 ? msg.single_int64 : 0) == 3 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { name: msg }.single_int64 } - CALL [5] { + } + } + CALL [5] { + function: _==_ + args: { + CALL [6] { function: _?_:_ args: { - CALL [6] { + CALL [7] { function: _>_ args: { - IDENT [7] { + IDENT [8] { name: @index0 } - CONSTANT [8] { value: 0 } + CONSTANT [9] { value: 0 } } } - IDENT [9] { + IDENT [10] { name: @index0 } - CONSTANT [10] { value: 0 } + CONSTANT [11] { value: 0 } } } - } - } - CALL [11] { - function: _==_ - args: { - IDENT [12] { - name: @index1 - } - CONSTANT [13] { value: 3 } + CONSTANT [12] { value: 3 } } } } @@ -1139,54 +1021,51 @@ Source: false ? false : (msg.single_int64) + ((msg.single_int64 + 1) * 2) == 11 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { name: msg }.single_int64 } - CALL [5] { + } + } + CALL [5] { + function: _?_:_ + args: { + CONSTANT [6] { value: false } + CONSTANT [7] { value: false } + CALL [8] { function: _==_ args: { - CALL [6] { + CALL [9] { function: _+_ args: { - IDENT [7] { + IDENT [10] { name: @index0 } - CALL [8] { + CALL [11] { function: _*_ args: { - CALL [9] { + CALL [12] { function: _+_ args: { - IDENT [10] { + IDENT [13] { name: @index0 } - CONSTANT [11] { value: 1 } + CONSTANT [14] { value: 1 } } } - CONSTANT [12] { value: 2 } + CONSTANT [15] { value: 2 } } } } } - CONSTANT [13] { value: 11 } + CONSTANT [16] { value: 11 } } } } } - CALL [14] { - function: _?_:_ - args: { - CONSTANT [15] { value: false } - CONSTANT [16] { value: false } - IDENT [17] { - name: @index1 - } - } - } } } Test case: NESTED_TERNARY @@ -1195,7 +1074,7 @@ Source: (msg.single_int64 > 0 ? (msg.single_int32 > 0 ? msg.single_int64 + msg.s CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { @@ -1207,56 +1086,53 @@ CALL [1] { name: msg }.single_int32 } - CALL [7] { + } + } + CALL [7] { + function: _==_ + args: { + CALL [8] { function: _?_:_ args: { - CALL [8] { + CALL [9] { function: _>_ args: { - IDENT [9] { + IDENT [10] { name: @index0 } - CONSTANT [10] { value: 0 } + CONSTANT [11] { value: 0 } } } - CALL [11] { + CALL [12] { function: _?_:_ args: { - CALL [12] { + CALL [13] { function: _>_ args: { - IDENT [13] { + IDENT [14] { name: @index1 } - CONSTANT [14] { value: 0 } + CONSTANT [15] { value: 0 } } } - CALL [15] { + CALL [16] { function: _+_ args: { - IDENT [16] { + IDENT [17] { name: @index0 } - IDENT [17] { + IDENT [18] { name: @index1 } } } - CONSTANT [18] { value: 0 } + CONSTANT [19] { value: 0 } } } - CONSTANT [19] { value: 0 } + CONSTANT [20] { value: 0 } } } - } - } - CALL [20] { - function: _==_ - args: { - IDENT [21] { - name: @index2 - } - CONSTANT [22] { value: 8 } + CONSTANT [21] { value: 8 } } } } @@ -1267,53 +1143,30 @@ Source: size([[1].exists(i, i > 0)]) + size([[1].exists(j, j > 0)]) + size([[2]. CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - } - } - CALL [5] { - function: _>_ - args: { - IDENT [6] { - name: @c0:0 - } - CONSTANT [7] { value: 0 } - } - } - CALL [8] { - function: _||_ - args: { - IDENT [9] { - name: @x0:0 - } - IDENT [10] { - name: @index1 - } - } - } - COMPREHENSION [11] { - iter_var: @c0:0 + COMPREHENSION [3] { + iter_var: @it:0:0 iter_range: { - IDENT [12] { - name: @index0 + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + } } } - accu_var: @x0:0 + accu_var: @ac:0:0 accu_init: { - CONSTANT [13] { value: false } + CONSTANT [6] { value: false } } loop_condition: { - CALL [14] { + CALL [7] { function: @not_strictly_false args: { - CALL [15] { + CALL [8] { function: !_ args: { - IDENT [16] { - name: @x0:0 + IDENT [9] { + name: @ac:0:0 } } } @@ -1321,76 +1174,52 @@ CALL [1] { } } loop_step: { - IDENT [17] { - name: @index2 + CALL [10] { + function: _||_ + args: { + IDENT [11] { + name: @ac:0:0 + } + CALL [12] { + function: _>_ + args: { + IDENT [13] { + name: @it:0:0 + } + CONSTANT [14] { value: 0 } + } + } + } } } result: { - IDENT [18] { - name: @x0:0 - } - } - } - CREATE_LIST [19] { - elements: { - IDENT [20] { - name: @index3 - } - } - } - CALL [21] { - function: size - args: { - IDENT [22] { - name: @index4 - } - } - } - CREATE_LIST [23] { - elements: { - CONSTANT [24] { value: 2 } - } - } - CALL [25] { - function: _>_ - args: { - IDENT [26] { - name: @c0:0 - } - CONSTANT [27] { value: 1 } - } - } - CALL [28] { - function: _||_ - args: { - IDENT [29] { - name: @x0:0 - } - IDENT [30] { - name: @index7 + IDENT [15] { + name: @ac:0:0 } } } - COMPREHENSION [31] { - iter_var: @c0:0 + COMPREHENSION [16] { + iter_var: @it:0:0 iter_range: { - IDENT [32] { - name: @index6 + LIST [17] { + elements: { + CONSTANT [18] { value: 2 } + } } } - accu_var: @x0:0 + accu_var: @ac:0:0 accu_init: { - CONSTANT [33] { value: false } + CONSTANT [19] { value: false } } loop_condition: { - CALL [34] { + CALL [20] { function: @not_strictly_false args: { - CALL [35] { + CALL [21] { function: !_ args: { - IDENT [36] { - name: @x0:0 + IDENT [22] { + name: @ac:0:0 } } } @@ -1398,67 +1227,87 @@ CALL [1] { } } loop_step: { - IDENT [37] { - name: @index8 + CALL [23] { + function: _||_ + args: { + IDENT [24] { + name: @ac:0:0 + } + CALL [25] { + function: _>_ + args: { + IDENT [26] { + name: @it:0:0 + } + CONSTANT [27] { value: 1 } + } + } + } } } result: { - IDENT [38] { - name: @x0:0 + IDENT [28] { + name: @ac:0:0 } } } - CREATE_LIST [39] { - elements: { - IDENT [40] { - name: @index9 + CALL [29] { + function: size + args: { + LIST [30] { + elements: { + IDENT [31] { + name: @index0 + } + } } } } - CALL [41] { + CALL [32] { function: size args: { - IDENT [42] { - name: @index10 + LIST [33] { + elements: { + IDENT [34] { + name: @index1 + } + } } } } - CALL [43] { + } + } + CALL [35] { + function: _==_ + args: { + CALL [36] { function: _+_ args: { - CALL [44] { + CALL [37] { function: _+_ args: { - CALL [45] { + CALL [38] { function: _+_ args: { - IDENT [46] { - name: @index5 + IDENT [39] { + name: @index2 } - IDENT [47] { - name: @index5 + IDENT [40] { + name: @index2 } } } - IDENT [48] { - name: @index11 + IDENT [41] { + name: @index3 } } } - IDENT [49] { - name: @index11 + IDENT [42] { + name: @index3 } } } - } - } - CALL [50] { - function: _==_ - args: { - IDENT [51] { - name: @index12 - } - CONSTANT [52] { value: 4 } + CONSTANT [43] { value: 4 } } } } @@ -1469,53 +1318,30 @@ Source: [[1].exists(i, i > 0)] + [[1].exists(j, j > 0)] + [['a'].exists(k, k == CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } + COMPREHENSION [3] { + iter_var: @it:0:0 + iter_range: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + } + } } - } - CALL [5] { - function: _>_ - args: { - IDENT [6] { - name: @c0:0 - } - CONSTANT [7] { value: 0 } - } - } - CALL [8] { - function: _||_ - args: { - IDENT [9] { - name: @x0:0 - } - IDENT [10] { - name: @index1 - } - } - } - COMPREHENSION [11] { - iter_var: @c0:0 - iter_range: { - IDENT [12] { - name: @index0 - } - } - accu_var: @x0:0 + accu_var: @ac:0:0 accu_init: { - CONSTANT [13] { value: false } + CONSTANT [6] { value: false } } loop_condition: { - CALL [14] { + CALL [7] { function: @not_strictly_false args: { - CALL [15] { + CALL [8] { function: !_ args: { - IDENT [16] { - name: @x0:0 + IDENT [9] { + name: @ac:0:0 } } } @@ -1523,68 +1349,52 @@ CALL [1] { } } loop_step: { - IDENT [17] { - name: @index2 + CALL [10] { + function: _||_ + args: { + IDENT [11] { + name: @ac:0:0 + } + CALL [12] { + function: _>_ + args: { + IDENT [13] { + name: @it:0:0 + } + CONSTANT [14] { value: 0 } + } + } + } } } result: { - IDENT [18] { - name: @x0:0 - } - } - } - CREATE_LIST [19] { - elements: { - IDENT [20] { - name: @index3 - } - } - } - CREATE_LIST [21] { - elements: { - CONSTANT [22] { value: "a" } - } - } - CALL [23] { - function: _==_ - args: { - IDENT [24] { - name: @c0:1 - } - CONSTANT [25] { value: "a" } - } - } - CALL [26] { - function: _||_ - args: { - IDENT [27] { - name: @x0:1 - } - IDENT [28] { - name: @index6 + IDENT [15] { + name: @ac:0:0 } } } - COMPREHENSION [29] { - iter_var: @c0:1 + COMPREHENSION [16] { + iter_var: @it:0:1 iter_range: { - IDENT [30] { - name: @index5 + LIST [17] { + elements: { + CONSTANT [18] { value: "a" } + } } } - accu_var: @x0:1 + accu_var: @ac:0:1 accu_init: { - CONSTANT [31] { value: false } + CONSTANT [19] { value: false } } loop_condition: { - CALL [32] { + CALL [20] { function: @not_strictly_false args: { - CALL [33] { + CALL [21] { function: !_ args: { - IDENT [34] { - name: @x0:1 + IDENT [22] { + name: @ac:0:1 } } } @@ -1592,594 +1402,2215 @@ CALL [1] { } } loop_step: { - IDENT [35] { - name: @index7 + CALL [23] { + function: _||_ + args: { + IDENT [24] { + name: @ac:0:1 + } + CALL [25] { + function: _==_ + args: { + IDENT [26] { + name: @it:0:1 + } + CONSTANT [27] { value: "a" } + } + } + } } } result: { - IDENT [36] { - name: @x0:1 + IDENT [28] { + name: @ac:0:1 } } } - CREATE_LIST [37] { + LIST [29] { elements: { - IDENT [38] { - name: @index8 + IDENT [30] { + name: @index0 } } } - CREATE_LIST [39] { + LIST [31] { elements: { - CONSTANT [40] { value: true } - CONSTANT [41] { value: true } - CONSTANT [42] { value: true } - CONSTANT [43] { value: true } + IDENT [32] { + name: @index1 + } } } - CALL [44] { + } + } + CALL [33] { + function: _==_ + args: { + CALL [34] { function: _+_ args: { - CALL [45] { + CALL [35] { function: _+_ args: { - CALL [46] { + CALL [36] { function: _+_ args: { - IDENT [47] { - name: @index4 + IDENT [37] { + name: @index2 } - IDENT [48] { - name: @index4 + IDENT [38] { + name: @index2 } } } - IDENT [49] { - name: @index9 + IDENT [39] { + name: @index3 } } } - IDENT [50] { - name: @index9 + IDENT [40] { + name: @index3 } } } - } - } - CALL [51] { - function: _==_ - args: { - IDENT [52] { - name: @index11 - } - IDENT [53] { - name: @index10 + LIST [41] { + elements: { + CONSTANT [42] { value: true } + CONSTANT [43] { value: true } + CONSTANT [44] { value: true } + CONSTANT [45] { value: true } + } } } } } } -Test case: NESTED_MACROS -Source: [1,2,3].map(i, [1, 2, 3].map(i, i + 1)) == [[2, 3, 4], [2, 3, 4], [2, 3, 4]] +Test case: MULTIPLE_MACROS_3 +Source: [1].exists(i, i > 0) && [1].exists(j, j > 0) && [1].exists(k, k > 1) && [2].exists(l, l > 1) =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - CONSTANT [5] { value: 2 } - CONSTANT [6] { value: 3 } + COMPREHENSION [3] { + iter_var: @it:0:0 + iter_range: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + } + } } - } - CREATE_LIST [7] { - elements: { - CONSTANT [8] { value: 2 } - CONSTANT [9] { value: 3 } - CONSTANT [10] { value: 4 } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [6] { value: false } } - } - CREATE_LIST [11] { - elements: { - IDENT [12] { - name: @index1 + loop_condition: { + CALL [7] { + function: @not_strictly_false + args: { + CALL [8] { + function: !_ + args: { + IDENT [9] { + name: @ac:0:0 + } + } + } + } } - IDENT [13] { - name: @index1 + } + loop_step: { + CALL [10] { + function: _||_ + args: { + IDENT [11] { + name: @ac:0:0 + } + CALL [12] { + function: _>_ + args: { + IDENT [13] { + name: @it:0:0 + } + CONSTANT [14] { value: 0 } + } + } + } } - IDENT [14] { - name: @index1 + } + result: { + IDENT [15] { + name: @ac:0:0 } } } - CALL [15] { - function: _+_ + } + } + CALL [16] { + function: _&&_ + args: { + CALL [17] { + function: _&&_ args: { - IDENT [16] { - name: @x0:0 + IDENT [18] { + name: @index0 } - CREATE_LIST [17] { - elements: { - COMPREHENSION [18] { - iter_var: @c1:0 - iter_range: { - IDENT [19] { - name: @index0 + IDENT [19] { + name: @index0 + } + } + } + CALL [20] { + function: _&&_ + args: { + COMPREHENSION [21] { + iter_var: @it:0:0 + iter_range: { + LIST [22] { + elements: { + CONSTANT [23] { value: 1 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [24] { value: false } + } + loop_condition: { + CALL [25] { + function: @not_strictly_false + args: { + CALL [26] { + function: !_ + args: { + IDENT [27] { + name: @ac:0:0 + } + } } } - accu_var: @x1:0 - accu_init: { - CREATE_LIST [20] { - elements: { + } + } + loop_step: { + CALL [28] { + function: _||_ + args: { + IDENT [29] { + name: @ac:0:0 + } + CALL [30] { + function: _>_ + args: { + IDENT [31] { + name: @it:0:0 + } + CONSTANT [32] { value: 1 } } } } - loop_condition: { - CONSTANT [21] { value: true } + } + } + result: { + IDENT [33] { + name: @ac:0:0 + } + } + } + COMPREHENSION [34] { + iter_var: @it:0:0 + iter_range: { + LIST [35] { + elements: { + CONSTANT [36] { value: 2 } } - loop_step: { - CALL [22] { - function: _+_ + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [37] { value: false } + } + loop_condition: { + CALL [38] { + function: @not_strictly_false + args: { + CALL [39] { + function: !_ args: { - IDENT [23] { - name: @x1:0 - } - CREATE_LIST [24] { - elements: { - CALL [25] { - function: _+_ - args: { - IDENT [26] { - name: @c1:0 - } - CONSTANT [27] { value: 1 } - } - } - } + IDENT [40] { + name: @ac:0:0 } } } } - result: { - IDENT [28] { - name: @x1:0 + } + } + loop_step: { + CALL [41] { + function: _||_ + args: { + IDENT [42] { + name: @ac:0:0 + } + CALL [43] { + function: _>_ + args: { + IDENT [44] { + name: @it:0:0 + } + CONSTANT [45] { value: 1 } + } } } } } + result: { + IDENT [46] { + name: @ac:0:0 + } + } } } } - COMPREHENSION [29] { - iter_var: @c0:0 + } + } + } +} +Test case: MULTIPLE_MACROS_COMP_V2_1 +Source: size([[1].exists(i,x,i > 0 && x >= 0)]) + size([[1].exists(j, y, j > 0 && y >= 0)]) + size([[2].exists(k,z,k > 1 && z >= 0)]) + size([[2].exists(l, w, l > 1 && w >= 0)]) == 4 +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + COMPREHENSION [3] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 iter_range: { - IDENT [30] { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [6] { value: false } + } + loop_condition: { + CALL [7] { + function: @not_strictly_false + args: { + CALL [8] { + function: !_ + args: { + IDENT [9] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [10] { + function: _||_ + args: { + IDENT [11] { + name: @ac:0:0 + } + CALL [12] { + function: _&&_ + args: { + CALL [13] { + function: _>_ + args: { + IDENT [14] { + name: @it:0:0 + } + CONSTANT [15] { value: 0 } + } + } + CALL [16] { + function: _>=_ + args: { + IDENT [17] { + name: @it2:0:0 + } + CONSTANT [18] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [19] { + name: @ac:0:0 + } + } + } + COMPREHENSION [20] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + LIST [21] { + elements: { + CONSTANT [22] { value: 2 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [23] { value: false } + } + loop_condition: { + CALL [24] { + function: @not_strictly_false + args: { + CALL [25] { + function: !_ + args: { + IDENT [26] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [27] { + function: _||_ + args: { + IDENT [28] { + name: @ac:0:0 + } + CALL [29] { + function: _&&_ + args: { + CALL [30] { + function: _>_ + args: { + IDENT [31] { + name: @it:0:0 + } + CONSTANT [32] { value: 1 } + } + } + CALL [33] { + function: _>=_ + args: { + IDENT [34] { + name: @it2:0:0 + } + CONSTANT [35] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [36] { + name: @ac:0:0 + } + } + } + CALL [37] { + function: size + args: { + LIST [38] { + elements: { + IDENT [39] { + name: @index0 + } + } + } + } + } + CALL [40] { + function: size + args: { + LIST [41] { + elements: { + IDENT [42] { + name: @index1 + } + } + } + } + } + } + } + CALL [43] { + function: _==_ + args: { + CALL [44] { + function: _+_ + args: { + CALL [45] { + function: _+_ + args: { + CALL [46] { + function: _+_ + args: { + IDENT [47] { + name: @index2 + } + IDENT [48] { + name: @index2 + } + } + } + IDENT [49] { + name: @index3 + } + } + } + IDENT [50] { + name: @index3 + } + } + } + CONSTANT [51] { value: 4 } + } + } + } +} +Test case: MULTIPLE_MACROS_COMP_V2_2 +Source: [1].exists(i, x, i > 0 && x > 0) && [1].exists(j, y, j > 0 && y > 0) && [1].exists(k, z, k > 1 && z > 0) && [2].exists(l, w, l > 1 && w > 0) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + COMPREHENSION [3] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [6] { value: false } + } + loop_condition: { + CALL [7] { + function: @not_strictly_false + args: { + CALL [8] { + function: !_ + args: { + IDENT [9] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [10] { + function: _||_ + args: { + IDENT [11] { + name: @ac:0:0 + } + CALL [12] { + function: _&&_ + args: { + CALL [13] { + function: _>_ + args: { + IDENT [14] { + name: @it:0:0 + } + CONSTANT [15] { value: 0 } + } + } + CALL [16] { + function: _>_ + args: { + IDENT [17] { + name: @it2:0:0 + } + CONSTANT [18] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [19] { + name: @ac:0:0 + } + } + } + } + } + CALL [20] { + function: _&&_ + args: { + CALL [21] { + function: _&&_ + args: { + IDENT [22] { + name: @index0 + } + IDENT [23] { + name: @index0 + } + } + } + CALL [24] { + function: _&&_ + args: { + COMPREHENSION [25] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + LIST [26] { + elements: { + CONSTANT [27] { value: 1 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [28] { value: false } + } + loop_condition: { + CALL [29] { + function: @not_strictly_false + args: { + CALL [30] { + function: !_ + args: { + IDENT [31] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [32] { + function: _||_ + args: { + IDENT [33] { + name: @ac:0:0 + } + CALL [34] { + function: _&&_ + args: { + CALL [35] { + function: _>_ + args: { + IDENT [36] { + name: @it:0:0 + } + CONSTANT [37] { value: 1 } + } + } + CALL [38] { + function: _>_ + args: { + IDENT [39] { + name: @it2:0:0 + } + CONSTANT [40] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [41] { + name: @ac:0:0 + } + } + } + COMPREHENSION [42] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + LIST [43] { + elements: { + CONSTANT [44] { value: 2 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [45] { value: false } + } + loop_condition: { + CALL [46] { + function: @not_strictly_false + args: { + CALL [47] { + function: !_ + args: { + IDENT [48] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [49] { + function: _||_ + args: { + IDENT [50] { + name: @ac:0:0 + } + CALL [51] { + function: _&&_ + args: { + CALL [52] { + function: _>_ + args: { + IDENT [53] { + name: @it:0:0 + } + CONSTANT [54] { value: 1 } + } + } + CALL [55] { + function: _>_ + args: { + IDENT [56] { + name: @it2:0:0 + } + CONSTANT [57] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [58] { + name: @ac:0:0 + } + } + } + } + } + } + } + } +} +Test case: NESTED_MACROS +Source: [1,2,3].map(i, [1, 2, 3].map(i, i + 1)) == [[2, 3, 4], [2, 3, 4], [2, 3, 4]] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + CONSTANT [5] { value: 2 } + CONSTANT [6] { value: 3 } + } + } + LIST [7] { + elements: { + CONSTANT [8] { value: 2 } + CONSTANT [9] { value: 3 } + CONSTANT [10] { value: 4 } + } + } + COMPREHENSION [11] { + iter_var: @it:0:0 + iter_range: { + IDENT [12] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [13] { + elements: { + } + } + } + loop_condition: { + CONSTANT [14] { value: true } + } + loop_step: { + CALL [15] { + function: _+_ + args: { + IDENT [16] { + name: @ac:0:0 + } + LIST [17] { + elements: { + CALL [18] { + function: _+_ + args: { + IDENT [19] { + name: @it:0:0 + } + CONSTANT [20] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [21] { + name: @ac:0:0 + } + } + } + } + } + CALL [22] { + function: _==_ + args: { + COMPREHENSION [23] { + iter_var: @it:1:0 + iter_range: { + IDENT [24] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [25] { + elements: { + } + } + } + loop_condition: { + CONSTANT [26] { value: true } + } + loop_step: { + CALL [27] { + function: _+_ + args: { + IDENT [28] { + name: @ac:1:0 + } + LIST [29] { + elements: { + IDENT [30] { + name: @index2 + } + } + } + } + } + } + result: { + IDENT [31] { + name: @ac:1:0 + } + } + } + LIST [32] { + elements: { + IDENT [33] { + name: @index1 + } + IDENT [34] { + name: @index1 + } + IDENT [35] { + name: @index1 + } + } + } + } + } + } +} +Test case: NESTED_MACROS_2 +Source: [1, 2].map(y, [1, 2, 3].filter(x, x == y)) == [[1], [2]] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + } + } + LIST [6] { + elements: { + CONSTANT [7] { value: 2 } + } + } + } + } + LIST [8] { + elements: { + CONSTANT [9] { value: 1 } + CONSTANT [10] { value: 2 } + } + } + LIST [11] { + elements: { + CONSTANT [12] { value: 1 } + CONSTANT [13] { value: 2 } + CONSTANT [14] { value: 3 } + } + } + } + } + CALL [15] { + function: _==_ + args: { + COMPREHENSION [16] { + iter_var: @it:1:0 + iter_range: { + IDENT [17] { + name: @index1 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [18] { + elements: { + } + } + } + loop_condition: { + CONSTANT [19] { value: true } + } + loop_step: { + CALL [20] { + function: _+_ + args: { + IDENT [21] { + name: @ac:1:0 + } + LIST [22] { + elements: { + COMPREHENSION [23] { + iter_var: @it:0:0 + iter_range: { + IDENT [24] { + name: @index2 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [25] { + elements: { + } + } + } + loop_condition: { + CONSTANT [26] { value: true } + } + loop_step: { + CALL [27] { + function: _?_:_ + args: { + CALL [28] { + function: _==_ + args: { + IDENT [29] { + name: @it:0:0 + } + IDENT [30] { + name: @it:1:0 + } + } + } + CALL [31] { + function: _+_ + args: { + IDENT [32] { + name: @ac:0:0 + } + LIST [33] { + elements: { + IDENT [34] { + name: @it:0:0 + } + } + } + } + } + IDENT [35] { + name: @ac:0:0 + } + } + } + } + result: { + IDENT [36] { + name: @ac:0:0 + } + } + } + } + } + } + } + } + result: { + IDENT [37] { + name: @ac:1:0 + } + } + } + IDENT [38] { + name: @index0 + } + } + } + } +} +Test case: NESTED_MACROS_3 +Source: [1,2,3].map(i, i * 2) + [1,2,3].map(i, i * 2).map(i, i * 2) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + COMPREHENSION [3] { + iter_var: @it:0:0 + iter_range: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + CONSTANT [6] { value: 2 } + CONSTANT [7] { value: 3 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [8] { + elements: { + } + } + } + loop_condition: { + CONSTANT [9] { value: true } + } + loop_step: { + CALL [10] { + function: _+_ + args: { + IDENT [11] { + name: @ac:0:0 + } + LIST [12] { + elements: { + CALL [13] { + function: _*_ + args: { + IDENT [14] { + name: @it:0:0 + } + CONSTANT [15] { value: 2 } + } + } + } + } + } + } + } + result: { + IDENT [16] { + name: @ac:0:0 + } + } + } + } + } + CALL [17] { + function: _+_ + args: { + IDENT [18] { + name: @index0 + } + COMPREHENSION [19] { + iter_var: @it:1:0 + iter_range: { + IDENT [20] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [21] { + elements: { + } + } + } + loop_condition: { + CONSTANT [22] { value: true } + } + loop_step: { + CALL [23] { + function: _+_ + args: { + IDENT [24] { + name: @ac:1:0 + } + LIST [25] { + elements: { + CALL [26] { + function: _*_ + args: { + IDENT [27] { + name: @it:1:0 + } + CONSTANT [28] { value: 2 } + } + } + } + } + } + } + } + result: { + IDENT [29] { + name: @ac:1:0 + } + } + } + } + } + } +} +Test case: NESTED_MACROS_COMP_V2_1 +Source: [1,2,3].transformList(i, v, [1, 2, 3].transformList(i, v, i + v + 1)) == [[2, 4, 6], [2, 4, 6], [2, 4, 6]] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + CONSTANT [5] { value: 2 } + CONSTANT [6] { value: 3 } + } + } + LIST [7] { + elements: { + CONSTANT [8] { value: 2 } + CONSTANT [9] { value: 4 } + CONSTANT [10] { value: 6 } + } + } + COMPREHENSION [11] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + IDENT [12] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [13] { + elements: { + } + } + } + loop_condition: { + CONSTANT [14] { value: true } + } + loop_step: { + CALL [15] { + function: _+_ + args: { + IDENT [16] { + name: @ac:0:0 + } + LIST [17] { + elements: { + CALL [18] { + function: _+_ + args: { + CALL [19] { + function: _+_ + args: { + IDENT [20] { + name: @it:0:0 + } + IDENT [21] { + name: @it2:0:0 + } + } + } + CONSTANT [22] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [23] { + name: @ac:0:0 + } + } + } + } + } + CALL [24] { + function: _==_ + args: { + COMPREHENSION [25] { + iter_var: @it:1:0 + iter_var2: @it2:1:0 + iter_range: { + IDENT [26] { name: @index0 } } - accu_var: @x0:0 + accu_var: @ac:1:0 + accu_init: { + LIST [27] { + elements: { + } + } + } + loop_condition: { + CONSTANT [28] { value: true } + } + loop_step: { + CALL [29] { + function: _+_ + args: { + IDENT [30] { + name: @ac:1:0 + } + LIST [31] { + elements: { + IDENT [32] { + name: @index2 + } + } + } + } + } + } + result: { + IDENT [33] { + name: @ac:1:0 + } + } + } + LIST [34] { + elements: { + IDENT [35] { + name: @index1 + } + IDENT [36] { + name: @index1 + } + IDENT [37] { + name: @index1 + } + } + } + } + } + } +} +Test case: NESTED_MACROS_COMP_V2_2 +Source: [1, 2].transformList(i, y, [1, 2, 3].filter(x, x == y && i < y)) == [[1], [2]] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + } + } + LIST [6] { + elements: { + CONSTANT [7] { value: 2 } + } + } + } + } + LIST [8] { + elements: { + CONSTANT [9] { value: 1 } + CONSTANT [10] { value: 2 } + } + } + LIST [11] { + elements: { + CONSTANT [12] { value: 1 } + CONSTANT [13] { value: 2 } + CONSTANT [14] { value: 3 } + } + } + } + } + CALL [15] { + function: _==_ + args: { + COMPREHENSION [16] { + iter_var: @it:1:0 + iter_var2: @it2:1:0 + iter_range: { + IDENT [17] { + name: @index1 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [18] { + elements: { + } + } + } + loop_condition: { + CONSTANT [19] { value: true } + } + loop_step: { + CALL [20] { + function: _+_ + args: { + IDENT [21] { + name: @ac:1:0 + } + LIST [22] { + elements: { + COMPREHENSION [23] { + iter_var: @it:0:0 + iter_range: { + IDENT [24] { + name: @index2 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [25] { + elements: { + } + } + } + loop_condition: { + CONSTANT [26] { value: true } + } + loop_step: { + CALL [27] { + function: _?_:_ + args: { + CALL [28] { + function: _&&_ + args: { + CALL [29] { + function: _==_ + args: { + IDENT [30] { + name: @it:0:0 + } + IDENT [31] { + name: @it2:1:0 + } + } + } + CALL [32] { + function: _<_ + args: { + IDENT [33] { + name: @it:1:0 + } + IDENT [34] { + name: @it2:1:0 + } + } + } + } + } + CALL [35] { + function: _+_ + args: { + IDENT [36] { + name: @ac:0:0 + } + LIST [37] { + elements: { + IDENT [38] { + name: @it:0:0 + } + } + } + } + } + IDENT [39] { + name: @ac:0:0 + } + } + } + } + result: { + IDENT [40] { + name: @ac:0:0 + } + } + } + } + } + } + } + } + result: { + IDENT [41] { + name: @ac:1:0 + } + } + } + IDENT [42] { + name: @index0 + } + } + } + } +} +Test case: ADJACENT_NESTED_MACROS +Source: [1,2,3].map(i, [1, 2, 3].map(i, i + 1)) == [1,2,3].map(j, [1, 2, 3].map(j, j + 1)) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + COMPREHENSION [3] { + iter_var: @it:0:0 + iter_range: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + CONSTANT [6] { value: 2 } + CONSTANT [7] { value: 3 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [8] { + elements: { + } + } + } + loop_condition: { + CONSTANT [9] { value: true } + } + loop_step: { + CALL [10] { + function: _+_ + args: { + IDENT [11] { + name: @ac:0:0 + } + LIST [12] { + elements: { + CALL [13] { + function: _+_ + args: { + IDENT [14] { + name: @it:0:0 + } + CONSTANT [15] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [16] { + name: @ac:0:0 + } + } + } + COMPREHENSION [17] { + iter_var: @it:1:0 + iter_range: { + LIST [18] { + elements: { + CONSTANT [19] { value: 1 } + CONSTANT [20] { value: 2 } + CONSTANT [21] { value: 3 } + } + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [22] { + elements: { + } + } + } + loop_condition: { + CONSTANT [23] { value: true } + } + loop_step: { + CALL [24] { + function: _+_ + args: { + IDENT [25] { + name: @ac:1:0 + } + LIST [26] { + elements: { + IDENT [27] { + name: @index0 + } + } + } + } + } + } + result: { + IDENT [28] { + name: @ac:1:0 + } + } + } + } + } + CALL [29] { + function: _==_ + args: { + IDENT [30] { + name: @index1 + } + IDENT [31] { + name: @index1 + } + } + } + } +} +Test case: ADJACENT_NESTED_MACROS_COMP_V2 +Source: [1,2,3].transformMap(i, x, [1, 2, 3].transformMap(i, x, i + x + 1)) == [1,2,3].transformMap(j, y, [1, 2, 3].transformMap(j, y, j + y + 1)) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + COMPREHENSION [3] { + iter_var: @it:1:0 + iter_var2: @it2:1:0 + iter_range: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + CONSTANT [6] { value: 2 } + CONSTANT [7] { value: 3 } + } + } + } + accu_var: @ac:1:0 accu_init: { - CREATE_LIST [31] { + MAP [8] { + + } + } + loop_condition: { + CONSTANT [9] { value: true } + } + loop_step: { + CALL [10] { + function: cel.@mapInsert + args: { + IDENT [11] { + name: @ac:1:0 + } + IDENT [12] { + name: @it:1:0 + } + COMPREHENSION [13] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + LIST [14] { + elements: { + CONSTANT [15] { value: 1 } + CONSTANT [16] { value: 2 } + CONSTANT [17] { value: 3 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + MAP [18] { + + } + } + loop_condition: { + CONSTANT [19] { value: true } + } + loop_step: { + CALL [20] { + function: cel.@mapInsert + args: { + IDENT [21] { + name: @ac:0:0 + } + IDENT [22] { + name: @it:0:0 + } + CALL [23] { + function: _+_ + args: { + CALL [24] { + function: _+_ + args: { + IDENT [25] { + name: @it:0:0 + } + IDENT [26] { + name: @it2:0:0 + } + } + } + CONSTANT [27] { value: 1 } + } + } + } + } + } + result: { + IDENT [28] { + name: @ac:0:0 + } + } + } + } + } + } + result: { + IDENT [29] { + name: @ac:1:0 + } + } + } + } + } + CALL [30] { + function: _==_ + args: { + IDENT [31] { + name: @index0 + } + IDENT [32] { + name: @index0 + } + } + } + } +} +Test case: INCLUSION_LIST +Source: 1 in [1,2,3] && 2 in [1,2,3] && 3 in [3, [1,2,3]] && 1 in [1,2,3] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + CALL [3] { + function: @in + args: { + CONSTANT [4] { value: 1 } + LIST [5] { elements: { + CONSTANT [6] { value: 1 } + CONSTANT [7] { value: 2 } + CONSTANT [8] { value: 3 } + } + } + } + } + LIST [9] { + elements: { + CONSTANT [10] { value: 1 } + CONSTANT [11] { value: 2 } + CONSTANT [12] { value: 3 } + } + } + } + } + CALL [13] { + function: _&&_ + args: { + CALL [14] { + function: _&&_ + args: { + IDENT [15] { + name: @index0 + } + CALL [16] { + function: @in + args: { + CONSTANT [17] { value: 2 } + IDENT [18] { + name: @index1 + } + } + } + } + } + CALL [19] { + function: _&&_ + args: { + CALL [20] { + function: @in + args: { + CONSTANT [21] { value: 3 } + LIST [22] { + elements: { + CONSTANT [23] { value: 3 } + IDENT [24] { + name: @index1 + } + } + } } } - } - loop_condition: { - CONSTANT [32] { value: true } - } - loop_step: { - IDENT [33] { - name: @index3 + IDENT [25] { + name: @index0 } } - result: { - IDENT [34] { - name: @x0:0 + } + } + } + } +} +Test case: INCLUSION_MAP +Source: 2 in {'a': 1, 2: {true: false}, 3: {true: false}} +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + MAP [3] { + MAP_ENTRY [4] { + key: { + CONSTANT [5] { value: true } + } + value: { + CONSTANT [6] { value: false } } } } } } - CALL [35] { - function: _==_ + CALL [7] { + function: @in args: { - IDENT [36] { - name: @index4 - } - IDENT [37] { - name: @index2 + CONSTANT [8] { value: 2 } + MAP [9] { + MAP_ENTRY [10] { + key: { + CONSTANT [11] { value: "a" } + } + value: { + CONSTANT [12] { value: 1 } + } + } + MAP_ENTRY [13] { + key: { + CONSTANT [14] { value: 2 } + } + value: { + IDENT [15] { + name: @index0 + } + } + } + MAP_ENTRY [16] { + key: { + CONSTANT [17] { value: 3 } + } + value: { + IDENT [18] { + name: @index0 + } + } + } } } } } } -Test case: NESTED_MACROS_2 -Source: [1, 2].map(y, [1, 2, 3].filter(x, x == y)) == [[1], [2]] +Test case: MACRO_ITER_VAR_NOT_REFERENCED +Source: [1,2].map(i, [1, 2].map(i, [3,4])) == [[[3, 4], [3, 4]], [[3, 4], [3, 4]]] =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { + LIST [3] { elements: { - CREATE_LIST [4] { + LIST [4] { elements: { - CONSTANT [5] { value: 1 } + CONSTANT [5] { value: 3 } + CONSTANT [6] { value: 4 } } } - CREATE_LIST [6] { + LIST [7] { elements: { - CONSTANT [7] { value: 2 } + CONSTANT [8] { value: 3 } + CONSTANT [9] { value: 4 } } } } } - CALL [8] { - function: _+_ - args: { - IDENT [9] { - name: @x0:0 + LIST [10] { + elements: { + CONSTANT [11] { value: 1 } + CONSTANT [12] { value: 2 } + } + } + COMPREHENSION [13] { + iter_var: @it:0:0 + iter_range: { + IDENT [14] { + name: @index1 } - CREATE_LIST [10] { + } + accu_var: @ac:0:0 + accu_init: { + LIST [15] { elements: { - COMPREHENSION [11] { - iter_var: @c1:0 - iter_range: { - CREATE_LIST [12] { - elements: { - CONSTANT [13] { value: 1 } - CONSTANT [14] { value: 2 } - CONSTANT [15] { value: 3 } - } - } - } - accu_var: @x1:0 - accu_init: { - CREATE_LIST [16] { + } + } + } + loop_condition: { + CONSTANT [16] { value: true } + } + loop_step: { + CALL [17] { + function: _+_ + args: { + IDENT [18] { + name: @ac:0:0 + } + LIST [19] { + elements: { + LIST [20] { elements: { + CONSTANT [21] { value: 3 } + CONSTANT [22] { value: 4 } } } } - loop_condition: { - CONSTANT [17] { value: true } - } - loop_step: { - CALL [18] { - function: _?_:_ - args: { - CALL [19] { - function: _==_ - args: { - IDENT [20] { - name: @c1:0 - } - IDENT [21] { - name: @c0:0 - } - } - } - CALL [22] { - function: _+_ - args: { - IDENT [23] { - name: @x1:0 - } - CREATE_LIST [24] { - elements: { - IDENT [25] { - name: @c1:0 - } - } - } - } - } - IDENT [26] { - name: @x1:0 - } - } - } - } - result: { - IDENT [27] { - name: @x1:0 - } - } } } } } + result: { + IDENT [23] { + name: @ac:0:0 + } + } } - COMPREHENSION [28] { - iter_var: @c0:0 + } + } + CALL [24] { + function: _==_ + args: { + COMPREHENSION [25] { + iter_var: @it:1:0 iter_range: { - CREATE_LIST [29] { - elements: { - CONSTANT [30] { value: 1 } - CONSTANT [31] { value: 2 } - } + IDENT [26] { + name: @index1 } } - accu_var: @x0:0 + accu_var: @ac:1:0 accu_init: { - CREATE_LIST [32] { + LIST [27] { elements: { } } } loop_condition: { - CONSTANT [33] { value: true } + CONSTANT [28] { value: true } } loop_step: { - IDENT [34] { - name: @index1 + CALL [29] { + function: _+_ + args: { + IDENT [30] { + name: @ac:1:0 + } + LIST [31] { + elements: { + IDENT [32] { + name: @index2 + } + } + } + } } } result: { - IDENT [35] { - name: @x0:0 + IDENT [33] { + name: @ac:1:0 } } } - } - } - CALL [36] { - function: _==_ - args: { - IDENT [37] { - name: @index2 - } - IDENT [38] { - name: @index0 + LIST [34] { + elements: { + IDENT [35] { + name: @index0 + } + IDENT [36] { + name: @index0 + } + } } } } } } -Test case: INCLUSION_LIST -Source: 1 in [1,2,3] && 2 in [1,2,3] && 3 in [3, [1,2,3]] && 1 in [1,2,3] +Test case: MACRO_ITER_VAR2_NOT_REFERENCED +Source: [1,2].transformList(i, v, [1, 2].transformList(i, v, [3,4])) == [[[3, 4], [3, 4]], [[3, 4], [3, 4]]] =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { + LIST [3] { elements: { - CONSTANT [4] { value: 1 } - CONSTANT [5] { value: 2 } - CONSTANT [6] { value: 3 } + LIST [4] { + elements: { + CONSTANT [5] { value: 3 } + CONSTANT [6] { value: 4 } + } + } + LIST [7] { + elements: { + CONSTANT [8] { value: 3 } + CONSTANT [9] { value: 4 } + } + } } } - CALL [7] { - function: @in - args: { - CONSTANT [8] { value: 1 } - IDENT [9] { - name: @index0 - } + LIST [10] { + elements: { + CONSTANT [11] { value: 1 } + CONSTANT [12] { value: 2 } } } - CALL [10] { - function: _&&_ - args: { - CALL [11] { - function: @in + COMPREHENSION [13] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + IDENT [14] { + name: @index1 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [15] { + elements: { + } + } + } + loop_condition: { + CONSTANT [16] { value: true } + } + loop_step: { + CALL [17] { + function: _+_ args: { - CONSTANT [12] { value: 3 } - CREATE_LIST [13] { + IDENT [18] { + name: @ac:0:0 + } + LIST [19] { elements: { - CONSTANT [14] { value: 3 } - IDENT [15] { - name: @index0 + LIST [20] { + elements: { + CONSTANT [21] { value: 3 } + CONSTANT [22] { value: 4 } + } } } } } } - IDENT [16] { - name: @index1 + } + result: { + IDENT [23] { + name: @ac:0:0 } } } - CALL [17] { - function: _&&_ - args: { - IDENT [18] { + } + } + CALL [24] { + function: _==_ + args: { + COMPREHENSION [25] { + iter_var: @it:1:0 + iter_var2: @it2:1:0 + iter_range: { + IDENT [26] { name: @index1 } - CALL [19] { - function: @in + } + accu_var: @ac:1:0 + accu_init: { + LIST [27] { + elements: { + } + } + } + loop_condition: { + CONSTANT [28] { value: true } + } + loop_step: { + CALL [29] { + function: _+_ args: { - CONSTANT [20] { value: 2 } - IDENT [21] { - name: @index0 + IDENT [30] { + name: @ac:1:0 + } + LIST [31] { + elements: { + IDENT [32] { + name: @index2 + } + } } } } } + result: { + IDENT [33] { + name: @ac:1:0 + } + } } - } - } - CALL [22] { - function: _&&_ - args: { - IDENT [23] { - name: @index3 - } - IDENT [24] { - name: @index2 + LIST [34] { + elements: { + IDENT [35] { + name: @index0 + } + IDENT [36] { + name: @index0 + } + } } } } } } -Test case: INCLUSION_MAP -Source: 2 in {'a': 1, 2: {true: false}, 3: {true: false}} +Test case: MACRO_SHADOWED_VARIABLE +Source: [x - 1 > 3 ? x - 1 : 5].exists(x, x - 1 > 3) || x - 1 > 3 =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_MAP [3] { - MAP_ENTRY [4] { - key: { - CONSTANT [5] { value: true } - } - value: { - CONSTANT [6] { value: false } + CALL [3] { + function: _>_ + args: { + CALL [4] { + function: _-_ + args: { + IDENT [5] { + name: x + } + CONSTANT [6] { value: 1 } + } } + CONSTANT [7] { value: 3 } } } - CREATE_MAP [7] { - MAP_ENTRY [8] { - key: { - CONSTANT [9] { value: "a" } - } - value: { - CONSTANT [10] { value: 1 } + } + } + CALL [8] { + function: _||_ + args: { + COMPREHENSION [9] { + iter_var: @it:0:0 + iter_range: { + LIST [10] { + elements: { + CALL [11] { + function: _?_:_ + args: { + IDENT [12] { + name: @index0 + } + CALL [13] { + function: _-_ + args: { + IDENT [14] { + name: x + } + CONSTANT [15] { value: 1 } + } + } + CONSTANT [16] { value: 5 } + } + } + } } } - MAP_ENTRY [11] { - key: { - CONSTANT [12] { value: 2 } - } - value: { - IDENT [13] { - name: @index0 + accu_var: @ac:0:0 + accu_init: { + CONSTANT [17] { value: false } + } + loop_condition: { + CALL [18] { + function: @not_strictly_false + args: { + CALL [19] { + function: !_ + args: { + IDENT [20] { + name: @ac:0:0 + } + } + } } } } - MAP_ENTRY [14] { - key: { - CONSTANT [15] { value: 3 } - } - value: { - IDENT [16] { - name: @index0 + loop_step: { + CALL [21] { + function: _||_ + args: { + IDENT [22] { + name: @ac:0:0 + } + CALL [23] { + function: _>_ + args: { + CALL [24] { + function: _-_ + args: { + IDENT [25] { + name: @it:0:0 + } + CONSTANT [26] { value: 1 } + } + } + CONSTANT [27] { value: 3 } + } + } } } } + result: { + IDENT [28] { + name: @ac:0:0 + } + } } - } - } - CALL [17] { - function: @in - args: { - CONSTANT [18] { value: 2 } - IDENT [19] { - name: @index1 + IDENT [29] { + name: @index0 } } } } } -Test case: MACRO_ITER_VAR_NOT_REFERENCED -Source: [1,2].map(i, [1, 2].map(i, [3,4])) == [[[3, 4], [3, 4]], [[3, 4], [3, 4]]] +Test case: MACRO_SHADOWED_VARIABLE_2 +Source: ["foo", "bar"].map(x, [x + x, x + x]).map(x, [x + x, x + x]) =====> -CALL [1] { - function: cel.@block - args: { - CREATE_LIST [2] { - elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - CONSTANT [5] { value: 2 } - } - } - CREATE_LIST [6] { - elements: { - CONSTANT [7] { value: 3 } - CONSTANT [8] { value: 4 } - } - } - CREATE_LIST [9] { +COMPREHENSION [35] { + iter_var: x + iter_range: { + COMPREHENSION [19] { + iter_var: x + iter_range: { + LIST [1] { elements: { - IDENT [10] { - name: @index1 - } - IDENT [11] { - name: @index1 - } + CONSTANT [2] { value: "foo" } + CONSTANT [3] { value: "bar" } } } - CREATE_LIST [12] { + } + accu_var: @result + accu_init: { + LIST [13] { elements: { - IDENT [13] { - name: @index2 - } - IDENT [14] { - name: @index2 - } } } - COMPREHENSION [15] { - iter_var: @c0:0 - iter_range: { - IDENT [16] { - name: @index0 + } + loop_condition: { + CONSTANT [14] { value: true } + } + loop_step: { + CALL [17] { + function: _+_ + args: { + IDENT [15] { + name: @result } - } - accu_var: @x0:0 - accu_init: { - CREATE_LIST [17] { + LIST [16] { elements: { - } - } - } - loop_condition: { - CONSTANT [18] { value: true } - } - loop_step: { - CALL [19] { - function: _+_ - args: { - IDENT [20] { - name: @x0:0 - } - CREATE_LIST [21] { + LIST [6] { elements: { - COMPREHENSION [22] { - iter_var: @c1:0 - iter_range: { - IDENT [23] { - name: @index0 + CALL [8] { + function: _+_ + args: { + IDENT [7] { + name: x } - } - accu_var: @x1:0 - accu_init: { - CREATE_LIST [24] { - elements: { - } + IDENT [9] { + name: x } } - loop_condition: { - CONSTANT [25] { value: true } - } - loop_step: { - CALL [26] { - function: _+_ - args: { - IDENT [27] { - name: @x1:0 - } - CREATE_LIST [28] { - elements: { - IDENT [29] { - name: @index1 - } - } - } - } + } + CALL [11] { + function: _+_ + args: { + IDENT [10] { + name: x } - } - result: { - IDENT [30] { - name: @x1:0 + IDENT [12] { + name: x } } } @@ -2188,86 +3619,155 @@ CALL [1] { } } } - result: { - IDENT [31] { - name: @x0:0 - } - } + } + } + result: { + IDENT [18] { + name: @result } } } - CALL [32] { - function: _==_ + } + accu_var: @result + accu_init: { + LIST [29] { + elements: { + } + } + } + loop_condition: { + CONSTANT [30] { value: true } + } + loop_step: { + CALL [33] { + function: _+_ args: { - IDENT [33] { - name: @index4 + IDENT [31] { + name: @result } - IDENT [34] { - name: @index3 + LIST [32] { + elements: { + LIST [22] { + elements: { + CALL [24] { + function: _+_ + args: { + IDENT [23] { + name: x + } + IDENT [25] { + name: x + } + } + } + CALL [27] { + function: _+_ + args: { + IDENT [26] { + name: x + } + IDENT [28] { + name: x + } + } + } + } + } + } } } } } + result: { + IDENT [34] { + name: @result + } + } } -Test case: MACRO_SHADOWED_VARIABLE -Source: [x - 1 > 3 ? x - 1 : 5].exists(x, x - 1 > 3) || x - 1 > 3 +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 =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { - function: _-_ - args: { - IDENT [4] { - name: x - } - CONSTANT [5] { value: 1 } - } - } - CALL [6] { function: _>_ args: { - IDENT [7] { - name: @index0 + CALL [4] { + function: _-_ + args: { + CALL [5] { + function: _-_ + args: { + IDENT [6] { + name: x + } + IDENT [7] { + name: y + } + } + } + CONSTANT [8] { value: 1 } + } } - CONSTANT [8] { value: 3 } + CONSTANT [9] { value: 3 } } } - COMPREHENSION [9] { - iter_var: @c0:0 + } + } + CALL [10] { + function: _||_ + args: { + COMPREHENSION [11] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 iter_range: { - CREATE_LIST [10] { + LIST [12] { elements: { - CALL [11] { + CALL [13] { function: _?_:_ args: { - IDENT [12] { - name: @index1 - } - IDENT [13] { + IDENT [14] { name: @index0 } - CONSTANT [14] { value: 5 } + CALL [15] { + function: _-_ + args: { + CALL [16] { + function: _-_ + args: { + IDENT [17] { + name: x + } + IDENT [18] { + name: y + } + } + } + CONSTANT [19] { value: 1 } + } + } + CONSTANT [20] { value: 5 } } } } } } - accu_var: @x0:0 + accu_var: @ac:0:0 accu_init: { - CONSTANT [15] { value: false } + CONSTANT [21] { value: false } } loop_condition: { - CALL [16] { + CALL [22] { function: @not_strictly_false args: { - CALL [17] { + CALL [23] { function: !_ args: { - IDENT [18] { - name: @x0:0 + IDENT [24] { + name: @ac:0:0 } } } @@ -2275,96 +3775,109 @@ CALL [1] { } } loop_step: { - CALL [19] { + CALL [25] { function: _||_ args: { - IDENT [20] { - name: @x0:0 + IDENT [26] { + name: @ac:0:0 } - CALL [21] { + CALL [27] { function: _>_ args: { - CALL [22] { + CALL [28] { function: _-_ args: { - IDENT [23] { - name: @c0:0 + CALL [29] { + function: _-_ + args: { + IDENT [30] { + name: @it:0:0 + } + IDENT [31] { + name: @it2:0:0 + } + } } - CONSTANT [24] { value: 1 } + CONSTANT [32] { value: 1 } } } - CONSTANT [25] { value: 3 } + CONSTANT [33] { value: 3 } } } } } } result: { - IDENT [26] { - name: @x0:0 + IDENT [34] { + name: @ac:0:0 } } } - } - } - CALL [27] { - function: _||_ - args: { - IDENT [28] { - name: @index2 - } - IDENT [29] { - name: @index1 + IDENT [35] { + name: @index0 } } } } } -Test case: MACRO_SHADOWED_VARIABLE_2 -Source: ["foo", "bar"].map(x, [x + x, x + x]).map(x, [x + x, x + x]) +Test case: MACRO_SHADOWED_VARIABLE_COMP_V2_2 +Source: ["foo", "bar"].transformMap(x, y, [x + x, y + y]).transformMap(x, y, [x + x, y + y]) =====> -CALL [1] { - function: cel.@block - args: { - CREATE_LIST [2] { - elements: { - CALL [3] { - function: _+_ - args: { - IDENT [4] { - name: @c1:0 - } - IDENT [5] { - name: @c1:0 - } +COMPREHENSION [35] { + iter_var: x + iter_var2: y + iter_range: { + COMPREHENSION [19] { + iter_var: x + iter_var2: y + iter_range: { + LIST [1] { + elements: { + CONSTANT [2] { value: "foo" } + CONSTANT [3] { value: "bar" } } } - CALL [6] { - function: _+_ - args: { - IDENT [7] { - name: @c0:0 - } - IDENT [8] { - name: @c0:0 - } - } + } + accu_var: @result + accu_init: { + MAP [14] { + } - CALL [9] { - function: _+_ + } + loop_condition: { + CONSTANT [15] { value: true } + } + loop_step: { + CALL [17] { + function: cel.@mapInsert args: { - IDENT [10] { - name: @x0:0 + IDENT [16] { + name: @result + } + IDENT [5] { + name: x } - CREATE_LIST [11] { + LIST [7] { elements: { - CREATE_LIST [12] { - elements: { - IDENT [13] { - name: @index1 + CALL [9] { + function: _+_ + args: { + IDENT [8] { + name: x } - IDENT [14] { - name: @index1 + IDENT [10] { + name: x + } + } + } + CALL [12] { + function: _+_ + args: { + IDENT [11] { + name: y + } + IDENT [13] { + name: y } } } @@ -2372,85 +3885,65 @@ CALL [1] { } } } - COMPREHENSION [15] { - iter_var: @c1:0 - iter_range: { - CREATE_LIST [16] { - elements: { - CONSTANT [17] { value: "foo" } - CONSTANT [18] { value: "bar" } - } - } - } - accu_var: @x1:0 - accu_init: { - CREATE_LIST [19] { - elements: { + } + result: { + IDENT [18] { + name: @result + } + } + } + } + accu_var: @result + accu_init: { + MAP [30] { + + } + } + loop_condition: { + CONSTANT [31] { value: true } + } + loop_step: { + CALL [33] { + function: cel.@mapInsert + args: { + IDENT [32] { + name: @result + } + IDENT [21] { + name: x + } + LIST [23] { + elements: { + CALL [25] { + function: _+_ + args: { + IDENT [24] { + name: x + } + IDENT [26] { + name: x + } } } - } - loop_condition: { - CONSTANT [20] { value: true } - } - loop_step: { - CALL [21] { + CALL [28] { function: _+_ args: { - IDENT [22] { - name: @x1:0 + IDENT [27] { + name: y } - CREATE_LIST [23] { - elements: { - CREATE_LIST [24] { - elements: { - IDENT [25] { - name: @index0 - } - IDENT [26] { - name: @index0 - } - } - } - } + IDENT [29] { + name: y } } } } - result: { - IDENT [27] { - name: @x1:0 - } - } } } } - COMPREHENSION [28] { - iter_var: @c0:0 - iter_range: { - IDENT [29] { - name: @index3 - } - } - accu_var: @x0:0 - accu_init: { - CREATE_LIST [30] { - elements: { - } - } - } - loop_condition: { - CONSTANT [31] { value: true } - } - loop_step: { - IDENT [32] { - name: @index2 - } - } - result: { - IDENT [33] { - name: @x0:0 - } - } + } + result: { + IDENT [34] { + name: @result } } } @@ -2460,9 +3953,9 @@ Source: has({'a': true}.a) && {'a':true}['a'] CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_MAP [3] { + MAP [3] { MAP_ENTRY [4] { key: { CONSTANT [5] { value: "a" } @@ -2472,27 +3965,59 @@ CALL [1] { } } } - CALL [7] { + } + } + CALL [7] { + function: _&&_ + args: { + SELECT [8] { + IDENT [9] { + name: @index0 + }.a~presence_test + } + CALL [10] { function: _[_] args: { - IDENT [8] { + IDENT [11] { name: @index0 } - CONSTANT [9] { value: "a" } + CONSTANT [12] { value: "a" } } } } } - CALL [10] { + } +} +Test case: PRESENCE_TEST_2 +Source: has({'a': true}.a) && has({'a': true}.a) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + SELECT [3] { + MAP [4] { + MAP_ENTRY [5] { + key: { + CONSTANT [6] { value: "a" } + } + value: { + CONSTANT [7] { value: true } + } + } + }.a~presence_test + } + } + } + CALL [8] { function: _&&_ args: { - SELECT [11] { - IDENT [12] { - name: @index0 - }.a~presence_test + IDENT [9] { + name: @index0 } - IDENT [13] { - name: @index1 + IDENT [10] { + name: @index0 } } } @@ -2504,40 +4029,37 @@ Source: (has(msg.oneof_type.payload) ? msg.oneof_type.payload.single_int64 : 0) CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { name: msg }.oneof_type } - CALL [5] { + } + } + CALL [5] { + function: _==_ + args: { + CALL [6] { function: _?_:_ args: { - SELECT [6] { - IDENT [7] { + SELECT [7] { + IDENT [8] { name: @index0 }.payload~presence_test } - SELECT [8] { - SELECT [9] { - IDENT [10] { + SELECT [9] { + SELECT [10] { + IDENT [11] { name: @index0 }.payload }.single_int64 } - CONSTANT [11] { value: 0 } + CONSTANT [12] { value: 0 } } } - } - } - CALL [12] { - function: _==_ - args: { - IDENT [13] { - name: @index1 - } - CONSTANT [14] { value: 10 } + CONSTANT [13] { value: 10 } } } } @@ -2548,54 +4070,47 @@ Source: (has(msg.oneof_type.payload) ? msg.oneof_type.payload.single_int64 : msg CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload }.single_int64 } - CALL [9] { + } + } + CALL [7] { + function: _==_ + args: { + CALL [8] { function: _?_:_ args: { - SELECT [10] { - IDENT [11] { - name: @index0 + SELECT [9] { + SELECT [10] { + IDENT [11] { + name: msg + }.oneof_type }.payload~presence_test } IDENT [12] { - name: @index2 + name: @index0 } CALL [13] { function: _*_ args: { IDENT [14] { - name: @index2 + name: @index0 } CONSTANT [15] { value: 0 } } } } } - } - } - CALL [16] { - function: _==_ - args: { - IDENT [17] { - name: @index3 - } - CONSTANT [18] { value: 10 } + CONSTANT [16] { value: 10 } } } } @@ -2606,54 +4121,49 @@ Source: (has(msg.oneof_type.payload.single_int64) ? msg.oneof_type.payload.singl CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload }.single_int64 } - CALL [9] { + } + } + CALL [7] { + function: _==_ + args: { + CALL [8] { function: _?_:_ args: { - SELECT [10] { - IDENT [11] { - name: @index1 + SELECT [9] { + SELECT [10] { + SELECT [11] { + IDENT [12] { + name: msg + }.oneof_type + }.payload }.single_int64~presence_test } - IDENT [12] { - name: @index2 + IDENT [13] { + name: @index0 } - CALL [13] { + CALL [14] { function: _*_ - args: { - IDENT [14] { - name: @index2 + args: { + IDENT [15] { + name: @index0 } - CONSTANT [15] { value: 0 } + CONSTANT [16] { value: 0 } } } } } - } - } - CALL [16] { - function: _==_ - args: { - IDENT [17] { - name: @index3 - } - CONSTANT [18] { value: 10 } + CONSTANT [17] { value: 10 } } } } @@ -2664,91 +4174,88 @@ Source: (has(msg.oneof_type) && has(msg.oneof_type.payload) && has(msg.oneof_typ CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.map_string_string } SELECT [7] { - IDENT [8] { - name: @index1 - }.map_string_string + SELECT [8] { + IDENT [9] { + name: msg + }.oneof_type + }.payload } - CALL [9] { - function: _?_:_ + } + } + CALL [10] { + function: _?_:_ + args: { + CALL [11] { + function: _&&_ args: { - CALL [10] { + CALL [12] { function: _&&_ args: { - SELECT [11] { - IDENT [12] { - name: @index1 - }.map_string_string~presence_test - } SELECT [13] { IDENT [14] { - name: @index2 - }.key~presence_test + name: msg + }.oneof_type~presence_test } - } - } - CALL [15] { - function: _==_ - args: { - SELECT [16] { - IDENT [17] { - name: @index2 - }.key + SELECT [15] { + SELECT [16] { + IDENT [17] { + name: msg + }.oneof_type + }.payload~presence_test } - CONSTANT [18] { value: "A" } } } - CONSTANT [19] { value: false } + SELECT [18] { + IDENT [19] { + name: @index1 + }.single_int64~presence_test + } } } CALL [20] { - function: _&&_ + function: _?_:_ args: { CALL [21] { function: _&&_ args: { SELECT [22] { IDENT [23] { - name: msg - }.oneof_type~presence_test + name: @index1 + }.map_string_string~presence_test } SELECT [24] { IDENT [25] { name: @index0 - }.payload~presence_test + }.key~presence_test } } } - SELECT [26] { - IDENT [27] { - name: @index1 - }.single_int64~presence_test + CALL [26] { + function: _==_ + args: { + SELECT [27] { + IDENT [28] { + name: @index0 + }.key + } + CONSTANT [29] { value: "A" } + } } + CONSTANT [30] { value: false } } } - } - } - CALL [28] { - function: _?_:_ - args: { - IDENT [29] { - name: @index4 - } - IDENT [30] { - name: @index3 - } CONSTANT [31] { value: false } } } @@ -2760,46 +4267,51 @@ Source: [10, ?optional.none(), [?optional.none(), ?opt_x], [?optional.none(), ?o CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CALL [3] { - function: optional.none - args: { - } - } - CREATE_LIST [4] { + LIST [3] { elements: { - IDENT [5] { - name: @index0 + CALL [4] { + function: optional.none + args: { + } } - IDENT [6] { + IDENT [5] { name: opt_x } } optional_indices: [0, 1] } - CREATE_LIST [7] { + LIST [6] { elements: { - CONSTANT [8] { value: 5 } + CONSTANT [7] { value: 5 } } } - CREATE_LIST [9] { + } + } + CALL [8] { + function: _==_ + args: { + LIST [9] { elements: { CONSTANT [10] { value: 10 } - IDENT [11] { - name: @index2 + CALL [11] { + function: optional.none + args: { + } } IDENT [12] { - name: @index2 + name: @index0 + } + IDENT [13] { + name: @index0 } } + optional_indices: [0] } - CREATE_LIST [13] { + LIST [14] { elements: { - CONSTANT [14] { value: 10 } - IDENT [15] { - name: @index0 - } + CONSTANT [15] { value: 10 } IDENT [16] { name: @index1 } @@ -2807,18 +4319,6 @@ CALL [1] { name: @index1 } } - optional_indices: [0] - } - } - } - CALL [18] { - function: _==_ - args: { - IDENT [19] { - name: @index4 - } - IDENT [20] { - name: @index3 } } } @@ -2830,56 +4330,47 @@ Source: {?'hello': optional.of('hello')}['hello'] + {?'hello': optional.of('hell CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { - function: optional.of - args: { - CONSTANT [4] { value: "hello" } - } - } - CREATE_MAP [5] { - MAP_ENTRY [6] { - key: { - CONSTANT [7] { value: "hello" } - } - optional_entry: true - value: { - IDENT [8] { - name: @index0 - } - } - } - } - CALL [9] { function: _[_] args: { - IDENT [10] { - name: @index1 + MAP [4] { + MAP_ENTRY [5] { + key: { + CONSTANT [6] { value: "hello" } + } + optional_entry: true + value: { + CALL [7] { + function: optional.of + args: { + CONSTANT [8] { value: "hello" } + } + } + } + } } - CONSTANT [11] { value: "hello" } + CONSTANT [9] { value: "hello" } } } - CALL [12] { + } + } + CALL [10] { + function: _==_ + args: { + CALL [11] { function: _+_ args: { - IDENT [13] { - name: @index2 + IDENT [12] { + name: @index0 } - IDENT [14] { - name: @index2 + IDENT [13] { + name: @index0 } } } - } - } - CALL [15] { - function: _==_ - args: { - IDENT [16] { - name: @index3 - } - CONSTANT [17] { value: "hellohello" } + CONSTANT [14] { value: "hellohello" } } } } @@ -2890,9 +4381,9 @@ Source: {?'key': optional.of('test')}[?'bogus'].or({'key': 'test'}[?'bogus']).or CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_MAP [3] { + MAP [3] { MAP_ENTRY [4] { key: { CONSTANT [5] { value: "key" } @@ -2902,69 +4393,66 @@ CALL [1] { } } } - CALL [7] { + } + } + CALL [7] { + function: _==_ + args: { + CALL [8] { function: orValue target: { - CALL [8] { + CALL [9] { function: or target: { - CALL [9] { + CALL [10] { function: _[?_] args: { - CREATE_MAP [10] { - MAP_ENTRY [11] { + MAP [11] { + MAP_ENTRY [12] { key: { - CONSTANT [12] { value: "key" } + CONSTANT [13] { value: "key" } } optional_entry: true value: { - CALL [13] { + CALL [14] { function: optional.of args: { - CONSTANT [14] { value: "test" } + CONSTANT [15] { value: "test" } } } } } } - CONSTANT [15] { value: "bogus" } + CONSTANT [16] { value: "bogus" } } } } args: { - CALL [16] { + CALL [17] { function: _[?_] args: { - IDENT [17] { + IDENT [18] { name: @index0 } - CONSTANT [18] { value: "bogus" } + CONSTANT [19] { value: "bogus" } } } } } } args: { - CALL [19] { + CALL [20] { function: _[_] args: { - IDENT [20] { + IDENT [21] { name: @index0 } - CONSTANT [21] { value: "key" } + CONSTANT [22] { value: "key" } } } } } - } - } - CALL [22] { - function: _==_ - args: { - IDENT [23] { - name: @index1 - } - CONSTANT [24] { value: "test" } + CONSTANT [23] { value: "test" } } } } @@ -2975,146 +4463,69 @@ Source: TestAllTypes{?single_int64: optional.ofNonZeroValue(1), ?single_int32: o CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CALL [3] { - function: optional.ofNonZeroValue - args: { - CONSTANT [4] { value: 1 } - } - } - CALL [5] { - function: optional.of - args: { - CONSTANT [6] { value: 4 } - } - } - CREATE_STRUCT [7] { - name: TestAllTypes + STRUCT [3] { + name: cel.expr.conformance.proto3.TestAllTypes entries: { - ENTRY [8] { + ENTRY [4] { field_key: single_int64 optional_entry: true value: { - IDENT [9] { - name: @index0 - } - } - } - ENTRY [10] { - field_key: single_int32 - optional_entry: true - value: { - IDENT [11] { - name: @index1 - } - } - } - } - } - CALL [12] { - function: _+_ - args: { - SELECT [13] { - IDENT [14] { - name: @index2 - }.single_int32 - } - SELECT [15] { - IDENT [16] { - name: @index2 - }.single_int64 - } - } - } - } - } - CALL [17] { - function: _==_ - args: { - IDENT [18] { - name: @index3 - } - CONSTANT [19] { value: 5 } - } - } - } -} -Test case: CALL -Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('h' + 'e' + 'l' + 'l' + 'o') -=====> -CALL [1] { - function: cel.@block - args: { - CREATE_LIST [2] { - elements: { - CALL [3] { - function: _+_ - args: { - CONSTANT [4] { value: "h" } - CONSTANT [5] { value: "e" } - } - } - CALL [6] { - function: _+_ - args: { - IDENT [7] { - name: @index0 - } - CONSTANT [8] { value: "l" } - } - } - CALL [9] { - function: _+_ - args: { - IDENT [10] { - name: @index1 + CALL [5] { + function: optional.ofNonZeroValue + args: { + CONSTANT [6] { value: 1 } + } + } + } } - CONSTANT [11] { value: "l" } - } - } - CALL [12] { - function: _+_ - args: { - IDENT [13] { - name: @index2 + ENTRY [7] { + field_key: single_int32 + optional_entry: true + value: { + CALL [8] { + function: optional.of + args: { + CONSTANT [9] { value: 4 } + } + } + } } - CONSTANT [14] { value: "o" } } } - CALL [15] { + } + } + CALL [10] { + function: _==_ + args: { + CALL [11] { function: _+_ args: { - IDENT [16] { - name: @index3 + SELECT [12] { + IDENT [13] { + name: @index0 + }.single_int32 + } + SELECT [14] { + IDENT [15] { + name: @index0 + }.single_int64 } - CONSTANT [17] { value: " world" } } } - } - } - CALL [18] { - function: matches - target: { - IDENT [19] { - name: @index4 - } - } - args: { - IDENT [20] { - name: @index3 - } + CONSTANT [16] { value: 5 } } } } } -Test case: CALL_ARGUMENT_NESTED_NO_COMMON_SUBEXPR -Source: 'hello world'.matches('h' + 'e' + 'l' + 'l' + 'o') +Test case: CALL +Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('h' + 'e' + 'l' + 'l' + 'o') =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { function: _+_ @@ -3146,147 +4557,164 @@ CALL [1] { CALL [12] { function: matches target: { - CONSTANT [13] { value: "hello world" } + CALL [13] { + function: _+_ + args: { + IDENT [14] { + name: @index0 + } + CONSTANT [15] { value: " world" } + } + } } args: { - IDENT [14] { + IDENT [16] { name: @index0 } } } } } -Test case: CALL_TARGET_NESTED_NO_COMMON_SUBEXPR -Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('hello') +Test case: CALL_ARGUMENT_NESTED_NO_COMMON_SUBEXPR +Source: 'hello world'.matches('h' + 'e' + 'l' + 'l' + 'o') =====> -CALL [1] { - function: cel.@block +CALL [2] { + function: matches + target: { + CONSTANT [1] { value: "hello world" } + } args: { - CREATE_LIST [2] { - elements: { - CALL [3] { + CALL [10] { + function: _+_ + args: { + CALL [8] { function: _+_ args: { - CALL [4] { + CALL [6] { function: _+_ args: { - CALL [5] { + CALL [4] { function: _+_ args: { - CALL [6] { - function: _+_ - args: { - CALL [7] { - function: _+_ - args: { - CONSTANT [8] { value: "h" } - CONSTANT [9] { value: "e" } - } - } - CONSTANT [10] { value: "l" } - } - } - CONSTANT [11] { value: "l" } + CONSTANT [3] { value: "h" } + CONSTANT [5] { value: "e" } } } - CONSTANT [12] { value: "o" } + CONSTANT [7] { value: "l" } } } - CONSTANT [13] { value: " world" } + CONSTANT [9] { value: "l" } } } - } - } - CALL [14] { - function: matches - target: { - IDENT [15] { - name: @index0 - } - } - args: { - CONSTANT [16] { value: "hello" } + CONSTANT [11] { value: "o" } } } } } -Test case: CALL_BOTH_ARGUMENT_TARGET_NESTED_NO_COMMON_SUBEXPR -Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('w' + 'o' + 'r' + 'l' + 'd') +Test case: CALL_TARGET_NESTED_NO_COMMON_SUBEXPR +Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('hello') =====> -CALL [1] { - function: cel.@block - args: { - CREATE_LIST [2] { - elements: { - CALL [3] { +CALL [12] { + function: matches + target: { + CALL [10] { + function: _+_ + args: { + CALL [8] { function: _+_ args: { - CALL [4] { + CALL [6] { function: _+_ args: { - CALL [5] { + CALL [4] { function: _+_ args: { - CALL [6] { + CALL [2] { function: _+_ args: { - CONSTANT [7] { value: "w" } - CONSTANT [8] { value: "o" } + CONSTANT [1] { value: "h" } + CONSTANT [3] { value: "e" } } } - CONSTANT [9] { value: "r" } + CONSTANT [5] { value: "l" } } } - CONSTANT [10] { value: "l" } + CONSTANT [7] { value: "l" } } } - CONSTANT [11] { value: "d" } + CONSTANT [9] { value: "o" } } } - CALL [12] { + CONSTANT [11] { value: " world" } + } + } + } + args: { + CONSTANT [13] { value: "hello" } + } +} +Test case: CALL_BOTH_ARGUMENT_TARGET_NESTED_NO_COMMON_SUBEXPR +Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('w' + 'o' + 'r' + 'l' + 'd') +=====> +CALL [12] { + function: matches + target: { + CALL [10] { + function: _+_ + args: { + CALL [8] { function: _+_ args: { - CALL [13] { + CALL [6] { function: _+_ args: { - CALL [14] { + CALL [4] { function: _+_ args: { - CALL [15] { + CALL [2] { function: _+_ args: { - CALL [16] { - function: _+_ - args: { - CONSTANT [17] { value: "h" } - CONSTANT [18] { value: "e" } - } - } - CONSTANT [19] { value: "l" } + CONSTANT [1] { value: "h" } + CONSTANT [3] { value: "e" } } } - CONSTANT [20] { value: "l" } + CONSTANT [5] { value: "l" } } } - CONSTANT [21] { value: "o" } + CONSTANT [7] { value: "l" } } } - CONSTANT [22] { value: " world" } + CONSTANT [9] { value: "o" } } } + CONSTANT [11] { value: " world" } } } - CALL [23] { - function: matches - target: { - IDENT [24] { - name: @index1 - } - } + } + args: { + CALL [20] { + function: _+_ args: { - IDENT [25] { - name: @index0 + CALL [18] { + function: _+_ + args: { + CALL [16] { + function: _+_ + args: { + CALL [14] { + function: _+_ + args: { + CONSTANT [13] { value: "w" } + CONSTANT [15] { value: "o" } + } + } + CONSTANT [17] { value: "r" } + } + } + CONSTANT [19] { value: "l" } + } } + CONSTANT [21] { value: "d" } } } } @@ -3297,77 +4725,69 @@ Source: non_pure_custom_func(msg.oneof_type.payload.single_int64) + non_pure_cus CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 - }.single_int64 - } - SELECT [9] { - IDENT [10] { - name: msg + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload }.single_int64 } - SELECT [11] { - IDENT [12] { - name: @index1 - }.single_int32 - } } } - CALL [13] { + CALL [7] { function: _+_ args: { - CALL [14] { + CALL [8] { function: _+_ args: { - CALL [15] { + CALL [9] { function: _+_ args: { - CALL [16] { + CALL [10] { function: non_pure_custom_func args: { - IDENT [17] { - name: @index2 + IDENT [11] { + name: @index0 } } } - CALL [18] { + CALL [12] { function: non_pure_custom_func args: { - IDENT [19] { - name: @index4 + SELECT [13] { + SELECT [14] { + SELECT [15] { + IDENT [16] { + name: msg + }.oneof_type + }.payload + }.single_int32 } } } } } - CALL [20] { + CALL [17] { function: non_pure_custom_func args: { - IDENT [21] { - name: @index2 + IDENT [18] { + name: @index0 } } } } } - CALL [22] { + CALL [19] { function: non_pure_custom_func args: { - IDENT [23] { - name: @index3 + SELECT [20] { + IDENT [21] { + name: msg + }.single_int64 } } } @@ -3381,79 +4801,71 @@ Source: pure_custom_func(msg.oneof_type.payload.single_int64) + pure_custom_func CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 - }.single_int64 - } - CALL [9] { - function: pure_custom_func - args: { - IDENT [10] { - name: @index2 - } - } - } - CALL [11] { + CALL [3] { function: pure_custom_func args: { - SELECT [12] { - IDENT [13] { - name: msg + SELECT [4] { + SELECT [5] { + SELECT [6] { + IDENT [7] { + name: msg + }.oneof_type + }.payload }.single_int64 } } } - CALL [14] { + CALL [8] { function: _+_ args: { - CALL [15] { + CALL [9] { function: _+_ args: { - IDENT [16] { - name: @index3 + IDENT [10] { + name: @index0 } - CALL [17] { + CALL [11] { function: pure_custom_func args: { - SELECT [18] { - IDENT [19] { - name: @index1 + SELECT [12] { + SELECT [13] { + SELECT [14] { + IDENT [15] { + name: msg + }.oneof_type + }.payload }.single_int32 } } } } } - IDENT [20] { - name: @index3 + IDENT [16] { + name: @index0 } } } } } - CALL [21] { + CALL [17] { function: _+_ args: { - IDENT [22] { - name: @index5 + IDENT [18] { + name: @index1 } - IDENT [23] { - name: @index4 + CALL [19] { + function: pure_custom_func + args: { + SELECT [20] { + IDENT [21] { + name: msg + }.single_int64 + } + } } } } } -} +} \ No newline at end of file diff --git a/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_7.baseline b/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_7.baseline index 973b97c3a..020447afd 100644 --- a/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_7.baseline +++ b/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_7.baseline @@ -4,22 +4,24 @@ Source: size([1,2]) + size([1,2]) + 1 == 5 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - CONSTANT [5] { value: 2 } - } - } - CALL [6] { + CALL [3] { function: size args: { - IDENT [7] { - name: @index0 + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + CONSTANT [6] { value: 2 } + } } } } + } + } + CALL [7] { + function: _==_ + args: { CALL [8] { function: _+_ args: { @@ -27,25 +29,17 @@ CALL [1] { function: _+_ args: { IDENT [10] { - name: @index1 + name: @index0 } IDENT [11] { - name: @index1 + name: @index0 } } } CONSTANT [12] { value: 1 } } } - } - } - CALL [13] { - function: _==_ - args: { - IDENT [14] { - name: @index2 - } - CONSTANT [15] { value: 5 } + CONSTANT [13] { value: 5 } } } } @@ -56,22 +50,24 @@ Source: 2 + size([1,2]) + size([1,2]) + 1 == 7 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - CONSTANT [5] { value: 2 } - } - } - CALL [6] { + CALL [3] { function: size args: { - IDENT [7] { - name: @index0 + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + CONSTANT [6] { value: 2 } + } } } } + } + } + CALL [7] { + function: _==_ + args: { CALL [8] { function: _+_ args: { @@ -83,27 +79,19 @@ CALL [1] { args: { CONSTANT [11] { value: 2 } IDENT [12] { - name: @index1 + name: @index0 } } } IDENT [13] { - name: @index1 + name: @index0 } } } CONSTANT [14] { value: 1 } } } - } - } - CALL [15] { - function: _==_ - args: { - IDENT [16] { - name: @index2 - } - CONSTANT [17] { value: 7 } + CONSTANT [15] { value: 7 } } } } @@ -114,71 +102,62 @@ Source: size([0]) + size([0]) + size([1,2]) + size([1,2]) == 6 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 0 } - } - } - CALL [5] { + CALL [3] { function: size args: { - IDENT [6] { - name: @index0 + LIST [4] { + elements: { + CONSTANT [5] { value: 0 } + } } } } - CREATE_LIST [7] { - elements: { - CONSTANT [8] { value: 1 } - CONSTANT [9] { value: 2 } - } - } - CALL [10] { + CALL [6] { function: size args: { - IDENT [11] { - name: @index2 + LIST [7] { + elements: { + CONSTANT [8] { value: 1 } + CONSTANT [9] { value: 2 } + } } } } - CALL [12] { + } + } + CALL [10] { + function: _==_ + args: { + CALL [11] { function: _+_ args: { - CALL [13] { + CALL [12] { function: _+_ args: { - CALL [14] { + CALL [13] { function: _+_ args: { - IDENT [15] { - name: @index1 + IDENT [14] { + name: @index0 } - IDENT [16] { - name: @index1 + IDENT [15] { + name: @index0 } } } - IDENT [17] { - name: @index3 + IDENT [16] { + name: @index1 } } } - IDENT [18] { - name: @index3 + IDENT [17] { + name: @index1 } } } - } - } - CALL [19] { - function: _==_ - args: { - IDENT [20] { - name: @index4 - } - CONSTANT [21] { value: 6 } + CONSTANT [18] { value: 6 } } } } @@ -189,108 +168,96 @@ Source: 5 + size([0]) + size([0]) + size([1,2]) + size([1,2]) + size([1,2,3]) + CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 0 } - } - } - CALL [5] { + CALL [3] { function: size args: { - IDENT [6] { - name: @index0 + LIST [4] { + elements: { + CONSTANT [5] { value: 0 } + } } } } - CREATE_LIST [7] { - elements: { - CONSTANT [8] { value: 1 } - CONSTANT [9] { value: 2 } - } - } - CALL [10] { + CALL [6] { function: size args: { - IDENT [11] { - name: @index2 + LIST [7] { + elements: { + CONSTANT [8] { value: 1 } + CONSTANT [9] { value: 2 } + } } } } - CREATE_LIST [12] { - elements: { - CONSTANT [13] { value: 1 } - CONSTANT [14] { value: 2 } - CONSTANT [15] { value: 3 } - } - } - CALL [16] { + CALL [10] { function: size args: { - IDENT [17] { - name: @index4 + LIST [11] { + elements: { + CONSTANT [12] { value: 1 } + CONSTANT [13] { value: 2 } + CONSTANT [14] { value: 3 } + } } } } - CALL [18] { + } + } + CALL [15] { + function: _==_ + args: { + CALL [16] { function: _+_ args: { - CALL [19] { + CALL [17] { function: _+_ args: { - CALL [20] { + CALL [18] { function: _+_ args: { - CALL [21] { + CALL [19] { function: _+_ args: { - CALL [22] { + CALL [20] { function: _+_ args: { - CALL [23] { + CALL [21] { function: _+_ args: { - CONSTANT [24] { value: 5 } - IDENT [25] { - name: @index1 + CONSTANT [22] { value: 5 } + IDENT [23] { + name: @index0 } } } - IDENT [26] { - name: @index1 + IDENT [24] { + name: @index0 } } } - IDENT [27] { - name: @index3 + IDENT [25] { + name: @index1 } } } - IDENT [28] { - name: @index3 + IDENT [26] { + name: @index1 } } } - IDENT [29] { - name: @index5 + IDENT [27] { + name: @index2 } } } - IDENT [30] { - name: @index5 + IDENT [28] { + name: @index2 } } } - } - } - CALL [31] { - function: _==_ - args: { - IDENT [32] { - name: @index6 - } - CONSTANT [33] { value: 17 } + CONSTANT [29] { value: 17 } } } } @@ -301,152 +268,112 @@ Source: timestamp(int(timestamp(1000000000))).getFullYear() + timestamp(int(time CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { - function: timestamp - args: { - CONSTANT [4] { value: 1000000000 } - } - } - CALL [5] { - function: int - args: { - IDENT [6] { - name: @index0 + function: getFullYear + target: { + CALL [4] { + function: timestamp + args: { + CALL [5] { + function: int + args: { + CALL [6] { + function: timestamp + args: { + CONSTANT [7] { value: 1000000000 } + } + } + } + } + } } } - } - CALL [7] { - function: timestamp args: { - IDENT [8] { - name: @index1 - } } } - CALL [9] { + CALL [8] { function: getFullYear target: { - IDENT [10] { - name: @index2 + CALL [9] { + function: timestamp + args: { + CALL [10] { + function: int + args: { + CALL [11] { + function: timestamp + args: { + CONSTANT [12] { value: 200 } + } + } + } + } + } } } args: { } } - CALL [11] { - function: timestamp - args: { - CONSTANT [12] { value: 50 } - } - } CALL [13] { - function: int - args: { - IDENT [14] { - name: @index4 - } - } - } - CALL [15] { function: timestamp args: { - IDENT [16] { - name: @index5 + CALL [14] { + function: int + args: { + CALL [15] { + function: timestamp + args: { + CONSTANT [16] { value: 50 } + } + } + } } } } CALL [17] { function: timestamp args: { - CONSTANT [18] { value: 200 } - } - } - CALL [19] { - function: int - args: { - IDENT [20] { - name: @index7 + CALL [18] { + function: int + args: { + CALL [19] { + function: timestamp + args: { + CONSTANT [20] { value: 75 } + } + } + } } } } CALL [21] { - function: timestamp - args: { - IDENT [22] { - name: @index8 - } - } - } - CALL [23] { - function: getFullYear - target: { - IDENT [24] { - name: @index9 - } - } - args: { - } - } - CALL [25] { - function: timestamp - args: { - CONSTANT [26] { value: 75 } - } - } - CALL [27] { - function: int - args: { - IDENT [28] { - name: @index11 - } - } - } - CALL [29] { - function: timestamp - args: { - IDENT [30] { - name: @index12 - } - } - } - CALL [31] { - function: getMinutes - target: { - IDENT [32] { - name: @index13 - } - } - args: { - } - } - CALL [33] { function: _+_ args: { - CALL [34] { + CALL [22] { function: _+_ args: { - CALL [35] { + CALL [23] { function: _+_ args: { - CALL [36] { + CALL [24] { function: _+_ args: { - CALL [37] { + CALL [25] { function: _+_ args: { - CALL [38] { + CALL [26] { function: _+_ args: { - IDENT [39] { - name: @index3 + IDENT [27] { + name: @index0 } - CALL [40] { + CALL [28] { function: getFullYear target: { - IDENT [41] { - name: @index13 + IDENT [29] { + name: @index3 } } args: { @@ -454,11 +381,11 @@ CALL [1] { } } } - CALL [42] { + CALL [30] { function: getFullYear target: { - IDENT [43] { - name: @index6 + IDENT [31] { + name: @index2 } } args: { @@ -466,16 +393,16 @@ CALL [1] { } } } - IDENT [44] { - name: @index3 + IDENT [32] { + name: @index0 } } } - CALL [45] { + CALL [33] { function: getSeconds target: { - IDENT [46] { - name: @index6 + IDENT [34] { + name: @index2 } } args: { @@ -483,44 +410,48 @@ CALL [1] { } } } - IDENT [47] { - name: @index10 + IDENT [35] { + name: @index1 } } } - IDENT [48] { - name: @index10 + IDENT [36] { + name: @index1 } } } - CALL [49] { + } + } + CALL [37] { + function: _==_ + args: { + CALL [38] { function: _+_ args: { - CALL [50] { + CALL [39] { function: _+_ args: { - IDENT [51] { - name: @index15 + IDENT [40] { + name: @index4 } - IDENT [52] { - name: @index14 + CALL [41] { + function: getMinutes + target: { + IDENT [42] { + name: @index3 + } + } + args: { + } } } } - IDENT [53] { - name: @index3 + IDENT [43] { + name: @index0 } } } - } - } - CALL [54] { - function: _==_ - args: { - IDENT [55] { - name: @index16 - } - CONSTANT [56] { value: 13934 } + CONSTANT [44] { value: 13934 } } } } @@ -531,55 +462,49 @@ Source: {"a": 2}["a"] + {"a": 2}["a"] * {"a": 2}["a"] == 6 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_MAP [3] { - MAP_ENTRY [4] { - key: { - CONSTANT [5] { value: "a" } - } - value: { - CONSTANT [6] { value: 2 } - } - } - } - CALL [7] { + CALL [3] { function: _[_] args: { - IDENT [8] { - name: @index0 + MAP [4] { + MAP_ENTRY [5] { + key: { + CONSTANT [6] { value: "a" } + } + value: { + CONSTANT [7] { value: 2 } + } + } } - CONSTANT [9] { value: "a" } + CONSTANT [8] { value: "a" } } } + } + } + CALL [9] { + function: _==_ + args: { CALL [10] { function: _+_ args: { IDENT [11] { - name: @index1 + name: @index0 } CALL [12] { function: _*_ args: { IDENT [13] { - name: @index1 + name: @index0 } IDENT [14] { - name: @index1 + name: @index0 } } } } } - } - } - CALL [15] { - function: _==_ - args: { - IDENT [16] { - name: @index2 - } - CONSTANT [17] { value: 6 } + CONSTANT [15] { value: 6 } } } } @@ -590,56 +515,53 @@ Source: {'a': {'b': 1}, 'c': {'b': 1}, 'd': {'e': {'b': 1}}, 'e': {'e': {'b': 1} CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_MAP [3] { + MAP [3] { MAP_ENTRY [4] { key: { - CONSTANT [5] { value: "b" } + CONSTANT [5] { value: "e" } } value: { - CONSTANT [6] { value: 1 } + MAP [6] { + MAP_ENTRY [7] { + key: { + CONSTANT [8] { value: "b" } + } + value: { + CONSTANT [9] { value: 1 } + } + } + } } } } - CREATE_MAP [7] { - MAP_ENTRY [8] { + MAP [10] { + MAP_ENTRY [11] { key: { - CONSTANT [9] { value: "e" } + CONSTANT [12] { value: "b" } } value: { - IDENT [10] { - name: @index0 - } + CONSTANT [13] { value: 1 } } } } } } - CREATE_MAP [11] { - MAP_ENTRY [12] { - key: { - CONSTANT [13] { value: "a" } - } - value: { - IDENT [14] { - name: @index0 - } - } - } + MAP [14] { MAP_ENTRY [15] { key: { - CONSTANT [16] { value: "c" } + CONSTANT [16] { value: "a" } } value: { IDENT [17] { - name: @index0 + name: @index1 } } } MAP_ENTRY [18] { key: { - CONSTANT [19] { value: "d" } + CONSTANT [19] { value: "c" } } value: { IDENT [20] { @@ -649,11 +571,21 @@ CALL [1] { } MAP_ENTRY [21] { key: { - CONSTANT [22] { value: "e" } + CONSTANT [22] { value: "d" } } value: { IDENT [23] { - name: @index1 + name: @index0 + } + } + } + MAP_ENTRY [24] { + key: { + CONSTANT [25] { value: "e" } + } + value: { + IDENT [26] { + name: @index0 } } } @@ -666,9 +598,9 @@ Source: [1, [1,2,3,4], 2, [1,2,3,4], 5, [1,2,3,4], 7, [[1,2], [1,2,3,4]], [1,2]] CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { + LIST [3] { elements: { CONSTANT [4] { value: 1 } CONSTANT [5] { value: 2 } @@ -676,43 +608,40 @@ CALL [1] { CONSTANT [7] { value: 4 } } } - CREATE_LIST [8] { + LIST [8] { elements: { CONSTANT [9] { value: 1 } CONSTANT [10] { value: 2 } } } - CREATE_LIST [11] { - elements: { - IDENT [12] { - name: @index1 - } - IDENT [13] { - name: @index0 - } - } - } } } - CREATE_LIST [14] { + LIST [11] { elements: { - CONSTANT [15] { value: 1 } - IDENT [16] { + CONSTANT [12] { value: 1 } + IDENT [13] { name: @index0 } - CONSTANT [17] { value: 2 } - IDENT [18] { + CONSTANT [14] { value: 2 } + IDENT [15] { name: @index0 } - CONSTANT [19] { value: 5 } - IDENT [20] { + CONSTANT [16] { value: 5 } + IDENT [17] { name: @index0 } - CONSTANT [21] { value: 7 } - IDENT [22] { - name: @index2 + CONSTANT [18] { value: 7 } + LIST [19] { + elements: { + IDENT [20] { + name: @index1 + } + IDENT [21] { + name: @index0 + } + } } - IDENT [23] { + IDENT [22] { name: @index1 } } @@ -725,33 +654,30 @@ Source: msg.single_int64 + msg.single_int64 == 6 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { name: msg }.single_int64 } - CALL [5] { + } + } + CALL [5] { + function: _==_ + args: { + CALL [6] { function: _+_ args: { - IDENT [6] { + IDENT [7] { name: @index0 } - IDENT [7] { + IDENT [8] { name: @index0 } } } - } - } - CALL [8] { - function: _==_ - args: { - IDENT [9] { - name: @index1 - } - CONSTANT [10] { value: 6 } + CONSTANT [9] { value: 6 } } } } @@ -762,61 +688,67 @@ Source: msg.oneof_type.payload.single_int64 + msg.oneof_type.payload.single_int3 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.single_int64 } SELECT [7] { - IDENT [8] { - name: @index1 - }.single_int64 + SELECT [8] { + IDENT [9] { + name: msg + }.oneof_type + }.payload } - CALL [9] { + } + } + CALL [10] { + function: _==_ + args: { + CALL [11] { function: _+_ args: { - CALL [10] { + CALL [12] { function: _+_ args: { - CALL [11] { + CALL [13] { function: _+_ args: { - CALL [12] { + CALL [14] { function: _+_ args: { - IDENT [13] { - name: @index2 + IDENT [15] { + name: @index0 } - SELECT [14] { - IDENT [15] { + SELECT [16] { + IDENT [17] { name: @index1 }.single_int32 } } } - IDENT [16] { - name: @index2 + IDENT [18] { + name: @index0 } } } - SELECT [17] { - IDENT [18] { + SELECT [19] { + IDENT [20] { name: msg }.single_int64 } } } - SELECT [19] { - SELECT [20] { - SELECT [21] { - IDENT [22] { + SELECT [21] { + SELECT [22] { + SELECT [23] { + IDENT [24] { name: @index1 }.oneof_type }.payload @@ -824,14 +756,6 @@ CALL [1] { } } } - } - } - CALL [23] { - function: _==_ - args: { - IDENT [24] { - name: @index3 - } CONSTANT [25] { value: 31 } } } @@ -843,54 +767,36 @@ Source: true || msg.oneof_type.payload.oneof_type.payload.oneof_type.payload.one CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 - }.oneof_type - } - SELECT [9] { - IDENT [10] { - name: @index2 - }.payload - } - SELECT [11] { - IDENT [12] { - name: @index3 - }.oneof_type - } - SELECT [13] { - SELECT [14] { - SELECT [15] { - SELECT [16] { - IDENT [17] { - name: @index4 - }.child - }.child + SELECT [4] { + SELECT [5] { + SELECT [6] { + SELECT [7] { + IDENT [8] { + name: msg + }.oneof_type + }.payload + }.oneof_type }.payload - }.single_bool + }.oneof_type } - CALL [18] { + } + } + CALL [9] { + function: _||_ + args: { + CALL [10] { function: _||_ args: { - CONSTANT [19] { value: true } - SELECT [20] { - SELECT [21] { - SELECT [22] { - SELECT [23] { - IDENT [24] { - name: @index4 + CONSTANT [11] { value: true } + SELECT [12] { + SELECT [13] { + SELECT [14] { + SELECT [15] { + IDENT [16] { + name: @index0 }.payload }.oneof_type }.payload @@ -898,16 +804,16 @@ CALL [1] { } } } - } - } - CALL [25] { - function: _||_ - args: { - IDENT [26] { - name: @index6 - } - IDENT [27] { - name: @index5 + SELECT [17] { + SELECT [18] { + SELECT [19] { + SELECT [20] { + IDENT [21] { + name: @index0 + }.child + }.child + }.payload + }.single_bool } } } @@ -919,60 +825,48 @@ Source: msg.oneof_type.payload.map_int32_int64[1] + msg.oneof_type.payload.map_i CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 - }.map_int32_int64 - } - CALL [9] { + CALL [3] { function: _[_] args: { - IDENT [10] { - name: @index2 + SELECT [4] { + SELECT [5] { + SELECT [6] { + IDENT [7] { + name: msg + }.oneof_type + }.payload + }.map_int32_int64 } - CONSTANT [11] { value: 1 } + CONSTANT [8] { value: 1 } } } - CALL [12] { + } + } + CALL [9] { + function: _==_ + args: { + CALL [10] { function: _+_ args: { - CALL [13] { + CALL [11] { function: _+_ args: { - IDENT [14] { - name: @index3 + IDENT [12] { + name: @index0 } - IDENT [15] { - name: @index3 + IDENT [13] { + name: @index0 } } } - IDENT [16] { - name: @index3 + IDENT [14] { + name: @index0 } } } - } - } - CALL [17] { - function: _==_ - args: { - IDENT [18] { - name: @index4 - } - CONSTANT [19] { value: 15 } + CONSTANT [15] { value: 15 } } } } @@ -983,69 +877,60 @@ Source: msg.oneof_type.payload.map_int32_int64[0] + msg.oneof_type.payload.map_i CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload }.map_int32_int64 } - CALL [9] { + } + } + CALL [7] { + function: _==_ + args: { + CALL [8] { function: _+_ args: { - CALL [10] { + CALL [9] { function: _+_ args: { - CALL [11] { + CALL [10] { function: _[_] args: { - IDENT [12] { - name: @index2 + IDENT [11] { + name: @index0 } - CONSTANT [13] { value: 0 } + CONSTANT [12] { value: 0 } } } - CALL [14] { + CALL [13] { function: _[_] args: { - IDENT [15] { - name: @index2 + IDENT [14] { + name: @index0 } - CONSTANT [16] { value: 1 } + CONSTANT [15] { value: 1 } } } } } - CALL [17] { + CALL [16] { function: _[_] args: { - IDENT [18] { - name: @index2 + IDENT [17] { + name: @index0 } - CONSTANT [19] { value: 2 } + CONSTANT [18] { value: 2 } } } } } - } - } - CALL [20] { - function: _==_ - args: { - IDENT [21] { - name: @index3 - } - CONSTANT [22] { value: 8 } + CONSTANT [19] { value: 8 } } } } @@ -1056,7 +941,7 @@ Source: msg.oneof_type.payload.oneof_type.payload.oneof_type.payload.oneof_type. CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { SELECT [4] { @@ -1075,16 +960,13 @@ CALL [1] { }.payload }.oneof_type } - SELECT [11] { - IDENT [12] { - name: @index0 - }.payload - } } } - SELECT [13] { - IDENT [14] { - name: @index1 + SELECT [11] { + SELECT [12] { + IDENT [13] { + name: @index0 + }.payload }.single_int64 } } @@ -1095,40 +977,37 @@ Source: (msg.single_int64 > 0 ? msg.single_int64 : 0) == 3 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { name: msg }.single_int64 } - CALL [5] { + } + } + CALL [5] { + function: _==_ + args: { + CALL [6] { function: _?_:_ args: { - CALL [6] { + CALL [7] { function: _>_ args: { - IDENT [7] { + IDENT [8] { name: @index0 } - CONSTANT [8] { value: 0 } + CONSTANT [9] { value: 0 } } } - IDENT [9] { + IDENT [10] { name: @index0 } - CONSTANT [10] { value: 0 } + CONSTANT [11] { value: 0 } } } - } - } - CALL [11] { - function: _==_ - args: { - IDENT [12] { - name: @index1 - } - CONSTANT [13] { value: 3 } + CONSTANT [12] { value: 3 } } } } @@ -1139,54 +1018,51 @@ Source: false ? false : (msg.single_int64) + ((msg.single_int64 + 1) * 2) == 11 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { name: msg }.single_int64 } - CALL [5] { + } + } + CALL [5] { + function: _?_:_ + args: { + CONSTANT [6] { value: false } + CONSTANT [7] { value: false } + CALL [8] { function: _==_ args: { - CALL [6] { + CALL [9] { function: _+_ args: { - IDENT [7] { + IDENT [10] { name: @index0 } - CALL [8] { + CALL [11] { function: _*_ args: { - CALL [9] { + CALL [12] { function: _+_ args: { - IDENT [10] { + IDENT [13] { name: @index0 } - CONSTANT [11] { value: 1 } + CONSTANT [14] { value: 1 } } } - CONSTANT [12] { value: 2 } + CONSTANT [15] { value: 2 } } } } } - CONSTANT [13] { value: 11 } + CONSTANT [16] { value: 11 } } } } } - CALL [14] { - function: _?_:_ - args: { - CONSTANT [15] { value: false } - CONSTANT [16] { value: false } - IDENT [17] { - name: @index1 - } - } - } } } Test case: NESTED_TERNARY @@ -1195,7 +1071,7 @@ Source: (msg.single_int64 > 0 ? (msg.single_int32 > 0 ? msg.single_int64 + msg.s CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { @@ -1207,56 +1083,53 @@ CALL [1] { name: msg }.single_int32 } - CALL [7] { + } + } + CALL [7] { + function: _==_ + args: { + CALL [8] { function: _?_:_ args: { - CALL [8] { + CALL [9] { function: _>_ args: { - IDENT [9] { + IDENT [10] { name: @index0 } - CONSTANT [10] { value: 0 } + CONSTANT [11] { value: 0 } } } - CALL [11] { + CALL [12] { function: _?_:_ args: { - CALL [12] { + CALL [13] { function: _>_ args: { - IDENT [13] { + IDENT [14] { name: @index1 } - CONSTANT [14] { value: 0 } + CONSTANT [15] { value: 0 } } } - CALL [15] { + CALL [16] { function: _+_ args: { - IDENT [16] { + IDENT [17] { name: @index0 } - IDENT [17] { + IDENT [18] { name: @index1 } } } - CONSTANT [18] { value: 0 } + CONSTANT [19] { value: 0 } } } - CONSTANT [19] { value: 0 } + CONSTANT [20] { value: 0 } } } - } - } - CALL [20] { - function: _==_ - args: { - IDENT [21] { - name: @index2 - } - CONSTANT [22] { value: 8 } + CONSTANT [21] { value: 8 } } } } @@ -1267,53 +1140,30 @@ Source: size([[1].exists(i, i > 0)]) + size([[1].exists(j, j > 0)]) + size([[2]. CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - } - } - CALL [5] { - function: _>_ - args: { - IDENT [6] { - name: @c0:0 - } - CONSTANT [7] { value: 0 } - } - } - CALL [8] { - function: _||_ - args: { - IDENT [9] { - name: @x0:0 - } - IDENT [10] { - name: @index1 - } - } - } - COMPREHENSION [11] { - iter_var: @c0:0 + COMPREHENSION [3] { + iter_var: @it:0:0 iter_range: { - IDENT [12] { - name: @index0 + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + } } } - accu_var: @x0:0 + accu_var: @ac:0:0 accu_init: { - CONSTANT [13] { value: false } + CONSTANT [6] { value: false } } loop_condition: { - CALL [14] { + CALL [7] { function: @not_strictly_false args: { - CALL [15] { + CALL [8] { function: !_ args: { - IDENT [16] { - name: @x0:0 + IDENT [9] { + name: @ac:0:0 } } } @@ -1321,76 +1171,52 @@ CALL [1] { } } loop_step: { - IDENT [17] { - name: @index2 + CALL [10] { + function: _||_ + args: { + IDENT [11] { + name: @ac:0:0 + } + CALL [12] { + function: _>_ + args: { + IDENT [13] { + name: @it:0:0 + } + CONSTANT [14] { value: 0 } + } + } + } } } result: { - IDENT [18] { - name: @x0:0 - } - } - } - CREATE_LIST [19] { - elements: { - IDENT [20] { - name: @index3 - } - } - } - CALL [21] { - function: size - args: { - IDENT [22] { - name: @index4 - } - } - } - CREATE_LIST [23] { - elements: { - CONSTANT [24] { value: 2 } - } - } - CALL [25] { - function: _>_ - args: { - IDENT [26] { - name: @c0:0 - } - CONSTANT [27] { value: 1 } - } - } - CALL [28] { - function: _||_ - args: { - IDENT [29] { - name: @x0:0 - } - IDENT [30] { - name: @index7 + IDENT [15] { + name: @ac:0:0 } } } - COMPREHENSION [31] { - iter_var: @c0:0 + COMPREHENSION [16] { + iter_var: @it:0:0 iter_range: { - IDENT [32] { - name: @index6 + LIST [17] { + elements: { + CONSTANT [18] { value: 2 } + } } } - accu_var: @x0:0 + accu_var: @ac:0:0 accu_init: { - CONSTANT [33] { value: false } + CONSTANT [19] { value: false } } loop_condition: { - CALL [34] { + CALL [20] { function: @not_strictly_false args: { - CALL [35] { + CALL [21] { function: !_ args: { - IDENT [36] { - name: @x0:0 + IDENT [22] { + name: @ac:0:0 } } } @@ -1398,67 +1224,87 @@ CALL [1] { } } loop_step: { - IDENT [37] { - name: @index8 + CALL [23] { + function: _||_ + args: { + IDENT [24] { + name: @ac:0:0 + } + CALL [25] { + function: _>_ + args: { + IDENT [26] { + name: @it:0:0 + } + CONSTANT [27] { value: 1 } + } + } + } } } result: { - IDENT [38] { - name: @x0:0 + IDENT [28] { + name: @ac:0:0 } } } - CREATE_LIST [39] { - elements: { - IDENT [40] { - name: @index9 + CALL [29] { + function: size + args: { + LIST [30] { + elements: { + IDENT [31] { + name: @index0 + } + } } } } - CALL [41] { + CALL [32] { function: size args: { - IDENT [42] { - name: @index10 + LIST [33] { + elements: { + IDENT [34] { + name: @index1 + } + } } } } - CALL [43] { + } + } + CALL [35] { + function: _==_ + args: { + CALL [36] { function: _+_ args: { - CALL [44] { + CALL [37] { function: _+_ args: { - CALL [45] { + CALL [38] { function: _+_ args: { - IDENT [46] { - name: @index5 + IDENT [39] { + name: @index2 } - IDENT [47] { - name: @index5 + IDENT [40] { + name: @index2 } } } - IDENT [48] { - name: @index11 + IDENT [41] { + name: @index3 } } } - IDENT [49] { - name: @index11 + IDENT [42] { + name: @index3 } } } - } - } - CALL [50] { - function: _==_ - args: { - IDENT [51] { - name: @index12 - } - CONSTANT [52] { value: 4 } + CONSTANT [43] { value: 4 } } } } @@ -1469,53 +1315,30 @@ Source: [[1].exists(i, i > 0)] + [[1].exists(j, j > 0)] + [['a'].exists(k, k == CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - } - } - CALL [5] { - function: _>_ - args: { - IDENT [6] { - name: @c0:0 - } - CONSTANT [7] { value: 0 } - } - } - CALL [8] { - function: _||_ - args: { - IDENT [9] { - name: @x0:0 - } - IDENT [10] { - name: @index1 - } - } - } - COMPREHENSION [11] { - iter_var: @c0:0 + COMPREHENSION [3] { + iter_var: @it:0:0 iter_range: { - IDENT [12] { - name: @index0 + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + } } } - accu_var: @x0:0 + accu_var: @ac:0:0 accu_init: { - CONSTANT [13] { value: false } + CONSTANT [6] { value: false } } loop_condition: { - CALL [14] { + CALL [7] { function: @not_strictly_false args: { - CALL [15] { + CALL [8] { function: !_ args: { - IDENT [16] { - name: @x0:0 + IDENT [9] { + name: @ac:0:0 } } } @@ -1523,68 +1346,52 @@ CALL [1] { } } loop_step: { - IDENT [17] { - name: @index2 + CALL [10] { + function: _||_ + args: { + IDENT [11] { + name: @ac:0:0 + } + CALL [12] { + function: _>_ + args: { + IDENT [13] { + name: @it:0:0 + } + CONSTANT [14] { value: 0 } + } + } + } } } result: { - IDENT [18] { - name: @x0:0 - } - } - } - CREATE_LIST [19] { - elements: { - IDENT [20] { - name: @index3 - } - } - } - CREATE_LIST [21] { - elements: { - CONSTANT [22] { value: "a" } - } - } - CALL [23] { - function: _==_ - args: { - IDENT [24] { - name: @c0:1 - } - CONSTANT [25] { value: "a" } - } - } - CALL [26] { - function: _||_ - args: { - IDENT [27] { - name: @x0:1 - } - IDENT [28] { - name: @index6 + IDENT [15] { + name: @ac:0:0 } } } - COMPREHENSION [29] { - iter_var: @c0:1 + COMPREHENSION [16] { + iter_var: @it:0:1 iter_range: { - IDENT [30] { - name: @index5 + LIST [17] { + elements: { + CONSTANT [18] { value: "a" } + } } } - accu_var: @x0:1 + accu_var: @ac:0:1 accu_init: { - CONSTANT [31] { value: false } + CONSTANT [19] { value: false } } loop_condition: { - CALL [32] { + CALL [20] { function: @not_strictly_false args: { - CALL [33] { + CALL [21] { function: !_ args: { - IDENT [34] { - name: @x0:1 + IDENT [22] { + name: @ac:0:1 } } } @@ -1592,317 +1399,1457 @@ CALL [1] { } } loop_step: { - IDENT [35] { - name: @index7 + CALL [23] { + function: _||_ + args: { + IDENT [24] { + name: @ac:0:1 + } + CALL [25] { + function: _==_ + args: { + IDENT [26] { + name: @it:0:1 + } + CONSTANT [27] { value: "a" } + } + } + } } } result: { - IDENT [36] { - name: @x0:1 + IDENT [28] { + name: @ac:0:1 } } } - CREATE_LIST [37] { + LIST [29] { elements: { - IDENT [38] { - name: @index8 + IDENT [30] { + name: @index0 } } } - CREATE_LIST [39] { + LIST [31] { elements: { - CONSTANT [40] { value: true } - CONSTANT [41] { value: true } - CONSTANT [42] { value: true } - CONSTANT [43] { value: true } + IDENT [32] { + name: @index1 + } } } - CALL [44] { + } + } + CALL [33] { + function: _==_ + args: { + CALL [34] { function: _+_ args: { - CALL [45] { + CALL [35] { function: _+_ args: { - CALL [46] { + CALL [36] { function: _+_ args: { - IDENT [47] { - name: @index4 + IDENT [37] { + name: @index2 } - IDENT [48] { - name: @index4 + IDENT [38] { + name: @index2 } } } - IDENT [49] { - name: @index9 + IDENT [39] { + name: @index3 } } } - IDENT [50] { - name: @index9 + IDENT [40] { + name: @index3 } } } - } - } - CALL [51] { - function: _==_ - args: { - IDENT [52] { - name: @index11 - } - IDENT [53] { - name: @index10 + LIST [41] { + elements: { + CONSTANT [42] { value: true } + CONSTANT [43] { value: true } + CONSTANT [44] { value: true } + CONSTANT [45] { value: true } + } } } } } } -Test case: NESTED_MACROS -Source: [1,2,3].map(i, [1, 2, 3].map(i, i + 1)) == [[2, 3, 4], [2, 3, 4], [2, 3, 4]] +Test case: MULTIPLE_MACROS_3 +Source: [1].exists(i, i > 0) && [1].exists(j, j > 0) && [1].exists(k, k > 1) && [2].exists(l, l > 1) =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - CONSTANT [5] { value: 2 } - CONSTANT [6] { value: 3 } - } - } - CREATE_LIST [7] { - elements: { - CONSTANT [8] { value: 2 } - CONSTANT [9] { value: 3 } - CONSTANT [10] { value: 4 } - } - } - CREATE_LIST [11] { - elements: { - IDENT [12] { - name: @index1 - } - IDENT [13] { - name: @index1 - } - IDENT [14] { - name: @index1 - } - } - } - COMPREHENSION [15] { - iter_var: @c0:0 + COMPREHENSION [3] { + iter_var: @it:0:0 iter_range: { - IDENT [16] { - name: @index0 - } - } - accu_var: @x0:0 - accu_init: { - CREATE_LIST [17] { + LIST [4] { elements: { + CONSTANT [5] { value: 1 } } } } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [6] { value: false } + } loop_condition: { - CONSTANT [18] { value: true } + CALL [7] { + function: @not_strictly_false + args: { + CALL [8] { + function: !_ + args: { + IDENT [9] { + name: @ac:0:0 + } + } + } + } + } } loop_step: { - CALL [19] { - function: _+_ + CALL [10] { + function: _||_ args: { - IDENT [20] { - name: @x0:0 + IDENT [11] { + name: @ac:0:0 } - CREATE_LIST [21] { - elements: { - COMPREHENSION [22] { - iter_var: @c1:0 - iter_range: { - IDENT [23] { - name: @index0 - } - } - accu_var: @x1:0 - accu_init: { - CREATE_LIST [24] { - elements: { - } - } - } - loop_condition: { - CONSTANT [25] { value: true } - } - loop_step: { - CALL [26] { - function: _+_ - args: { - IDENT [27] { - name: @x1:0 - } - CREATE_LIST [28] { - elements: { - CALL [29] { - function: _+_ - args: { - IDENT [30] { - name: @c1:0 - } - CONSTANT [31] { value: 1 } - } - } - } - } - } - } - } - result: { - IDENT [32] { - name: @x1:0 - } - } + CALL [12] { + function: _>_ + args: { + IDENT [13] { + name: @it:0:0 } + CONSTANT [14] { value: 0 } } } } } } result: { - IDENT [33] { - name: @x0:0 + IDENT [15] { + name: @ac:0:0 } } } } } - CALL [34] { - function: _==_ + CALL [16] { + function: _&&_ args: { - IDENT [35] { - name: @index3 - } - IDENT [36] { - name: @index2 - } - } - } - } -} -Test case: NESTED_MACROS_2 -Source: [1, 2].map(y, [1, 2, 3].filter(x, x == y)) == [[1], [2]] -=====> -CALL [1] { - function: cel.@block - args: { - CREATE_LIST [2] { - elements: { - CREATE_LIST [3] { - elements: { - CREATE_LIST [4] { - elements: { - CONSTANT [5] { value: 1 } - } + CALL [17] { + function: _&&_ + args: { + IDENT [18] { + name: @index0 } - CREATE_LIST [6] { - elements: { - CONSTANT [7] { value: 2 } - } + IDENT [19] { + name: @index0 } } } - COMPREHENSION [8] { - iter_var: @c0:0 - iter_range: { - CREATE_LIST [9] { - elements: { - CONSTANT [10] { value: 1 } - CONSTANT [11] { value: 2 } + CALL [20] { + function: _&&_ + args: { + COMPREHENSION [21] { + iter_var: @it:0:0 + iter_range: { + LIST [22] { + elements: { + CONSTANT [23] { value: 1 } + } + } } - } - } - accu_var: @x0:0 - accu_init: { - CREATE_LIST [12] { - elements: { + accu_var: @ac:0:0 + accu_init: { + CONSTANT [24] { value: false } } - } - } - loop_condition: { - CONSTANT [13] { value: true } - } - loop_step: { - CALL [14] { - function: _+_ - args: { - IDENT [15] { - name: @x0:0 + loop_condition: { + CALL [25] { + function: @not_strictly_false + args: { + CALL [26] { + function: !_ + args: { + IDENT [27] { + name: @ac:0:0 + } + } + } + } } - CREATE_LIST [16] { + } + loop_step: { + CALL [28] { + function: _||_ + args: { + IDENT [29] { + name: @ac:0:0 + } + CALL [30] { + function: _>_ + args: { + IDENT [31] { + name: @it:0:0 + } + CONSTANT [32] { value: 1 } + } + } + } + } + } + result: { + IDENT [33] { + name: @ac:0:0 + } + } + } + COMPREHENSION [34] { + iter_var: @it:0:0 + iter_range: { + LIST [35] { + elements: { + CONSTANT [36] { value: 2 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [37] { value: false } + } + loop_condition: { + CALL [38] { + function: @not_strictly_false + args: { + CALL [39] { + function: !_ + args: { + IDENT [40] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [41] { + function: _||_ + args: { + IDENT [42] { + name: @ac:0:0 + } + CALL [43] { + function: _>_ + args: { + IDENT [44] { + name: @it:0:0 + } + CONSTANT [45] { value: 1 } + } + } + } + } + } + result: { + IDENT [46] { + name: @ac:0:0 + } + } + } + } + } + } + } + } +} +Test case: MULTIPLE_MACROS_COMP_V2_1 +Source: size([[1].exists(i,x,i > 0 && x >= 0)]) + size([[1].exists(j, y, j > 0 && y >= 0)]) + size([[2].exists(k,z,k > 1 && z >= 0)]) + size([[2].exists(l, w, l > 1 && w >= 0)]) == 4 +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + COMPREHENSION [3] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [6] { value: false } + } + loop_condition: { + CALL [7] { + function: @not_strictly_false + args: { + CALL [8] { + function: !_ + args: { + IDENT [9] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [10] { + function: _||_ + args: { + IDENT [11] { + name: @ac:0:0 + } + CALL [12] { + function: _&&_ + args: { + CALL [13] { + function: _>_ + args: { + IDENT [14] { + name: @it:0:0 + } + CONSTANT [15] { value: 0 } + } + } + CALL [16] { + function: _>=_ + args: { + IDENT [17] { + name: @it2:0:0 + } + CONSTANT [18] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [19] { + name: @ac:0:0 + } + } + } + COMPREHENSION [20] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + LIST [21] { + elements: { + CONSTANT [22] { value: 2 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [23] { value: false } + } + loop_condition: { + CALL [24] { + function: @not_strictly_false + args: { + CALL [25] { + function: !_ + args: { + IDENT [26] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [27] { + function: _||_ + args: { + IDENT [28] { + name: @ac:0:0 + } + CALL [29] { + function: _&&_ + args: { + CALL [30] { + function: _>_ + args: { + IDENT [31] { + name: @it:0:0 + } + CONSTANT [32] { value: 1 } + } + } + CALL [33] { + function: _>=_ + args: { + IDENT [34] { + name: @it2:0:0 + } + CONSTANT [35] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [36] { + name: @ac:0:0 + } + } + } + CALL [37] { + function: size + args: { + LIST [38] { + elements: { + IDENT [39] { + name: @index0 + } + } + } + } + } + CALL [40] { + function: size + args: { + LIST [41] { + elements: { + IDENT [42] { + name: @index1 + } + } + } + } + } + } + } + CALL [43] { + function: _==_ + args: { + CALL [44] { + function: _+_ + args: { + CALL [45] { + function: _+_ + args: { + CALL [46] { + function: _+_ + args: { + IDENT [47] { + name: @index2 + } + IDENT [48] { + name: @index2 + } + } + } + IDENT [49] { + name: @index3 + } + } + } + IDENT [50] { + name: @index3 + } + } + } + CONSTANT [51] { value: 4 } + } + } + } +} +Test case: MULTIPLE_MACROS_COMP_V2_2 +Source: [1].exists(i, x, i > 0 && x > 0) && [1].exists(j, y, j > 0 && y > 0) && [1].exists(k, z, k > 1 && z > 0) && [2].exists(l, w, l > 1 && w > 0) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + COMPREHENSION [3] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [6] { value: false } + } + loop_condition: { + CALL [7] { + function: @not_strictly_false + args: { + CALL [8] { + function: !_ + args: { + IDENT [9] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [10] { + function: _||_ + args: { + IDENT [11] { + name: @ac:0:0 + } + CALL [12] { + function: _&&_ + args: { + CALL [13] { + function: _>_ + args: { + IDENT [14] { + name: @it:0:0 + } + CONSTANT [15] { value: 0 } + } + } + CALL [16] { + function: _>_ + args: { + IDENT [17] { + name: @it2:0:0 + } + CONSTANT [18] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [19] { + name: @ac:0:0 + } + } + } + } + } + CALL [20] { + function: _&&_ + args: { + CALL [21] { + function: _&&_ + args: { + IDENT [22] { + name: @index0 + } + IDENT [23] { + name: @index0 + } + } + } + CALL [24] { + function: _&&_ + args: { + COMPREHENSION [25] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + LIST [26] { + elements: { + CONSTANT [27] { value: 1 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [28] { value: false } + } + loop_condition: { + CALL [29] { + function: @not_strictly_false + args: { + CALL [30] { + function: !_ + args: { + IDENT [31] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [32] { + function: _||_ + args: { + IDENT [33] { + name: @ac:0:0 + } + CALL [34] { + function: _&&_ + args: { + CALL [35] { + function: _>_ + args: { + IDENT [36] { + name: @it:0:0 + } + CONSTANT [37] { value: 1 } + } + } + CALL [38] { + function: _>_ + args: { + IDENT [39] { + name: @it2:0:0 + } + CONSTANT [40] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [41] { + name: @ac:0:0 + } + } + } + COMPREHENSION [42] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + LIST [43] { + elements: { + CONSTANT [44] { value: 2 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [45] { value: false } + } + loop_condition: { + CALL [46] { + function: @not_strictly_false + args: { + CALL [47] { + function: !_ + args: { + IDENT [48] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [49] { + function: _||_ + args: { + IDENT [50] { + name: @ac:0:0 + } + CALL [51] { + function: _&&_ + args: { + CALL [52] { + function: _>_ + args: { + IDENT [53] { + name: @it:0:0 + } + CONSTANT [54] { value: 1 } + } + } + CALL [55] { + function: _>_ + args: { + IDENT [56] { + name: @it2:0:0 + } + CONSTANT [57] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [58] { + name: @ac:0:0 + } + } + } + } + } + } + } + } +} +Test case: NESTED_MACROS +Source: [1,2,3].map(i, [1, 2, 3].map(i, i + 1)) == [[2, 3, 4], [2, 3, 4], [2, 3, 4]] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + CONSTANT [5] { value: 2 } + CONSTANT [6] { value: 3 } + } + } + LIST [7] { + elements: { + CONSTANT [8] { value: 2 } + CONSTANT [9] { value: 3 } + CONSTANT [10] { value: 4 } + } + } + } + } + CALL [11] { + function: _==_ + args: { + COMPREHENSION [12] { + iter_var: @it:1:0 + iter_range: { + IDENT [13] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [14] { + elements: { + } + } + } + loop_condition: { + CONSTANT [15] { value: true } + } + loop_step: { + CALL [16] { + function: _+_ + args: { + IDENT [17] { + name: @ac:1:0 + } + LIST [18] { + elements: { + COMPREHENSION [19] { + iter_var: @it:0:0 + iter_range: { + IDENT [20] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [21] { + elements: { + } + } + } + loop_condition: { + CONSTANT [22] { value: true } + } + loop_step: { + CALL [23] { + function: _+_ + args: { + IDENT [24] { + name: @ac:0:0 + } + LIST [25] { + elements: { + CALL [26] { + function: _+_ + args: { + IDENT [27] { + name: @it:0:0 + } + CONSTANT [28] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [29] { + name: @ac:0:0 + } + } + } + } + } + } + } + } + result: { + IDENT [30] { + name: @ac:1:0 + } + } + } + LIST [31] { + elements: { + IDENT [32] { + name: @index1 + } + IDENT [33] { + name: @index1 + } + IDENT [34] { + name: @index1 + } + } + } + } + } + } +} +Test case: NESTED_MACROS_2 +Source: [1, 2].map(y, [1, 2, 3].filter(x, x == y)) == [[1], [2]] +=====> +CALL [31] { + function: _==_ + args: { + COMPREHENSION [30] { + iter_var: y + iter_range: { + LIST [1] { + elements: { + CONSTANT [2] { value: 1 } + CONSTANT [3] { value: 2 } + } + } + } + accu_var: @result + accu_init: { + LIST [24] { + elements: { + } + } + } + loop_condition: { + CONSTANT [25] { value: true } + } + loop_step: { + CALL [28] { + function: _+_ + args: { + IDENT [26] { + name: @result + } + LIST [27] { + elements: { + COMPREHENSION [23] { + iter_var: x + iter_range: { + LIST [6] { + elements: { + CONSTANT [7] { value: 1 } + CONSTANT [8] { value: 2 } + CONSTANT [9] { value: 3 } + } + } + } + accu_var: @result + accu_init: { + LIST [15] { + elements: { + } + } + } + loop_condition: { + CONSTANT [16] { value: true } + } + loop_step: { + CALL [21] { + function: _?_:_ + args: { + CALL [13] { + function: _==_ + args: { + IDENT [12] { + name: x + } + IDENT [14] { + name: y + } + } + } + CALL [19] { + function: _+_ + args: { + IDENT [17] { + name: @result + } + LIST [18] { + elements: { + IDENT [11] { + name: x + } + } + } + } + } + IDENT [20] { + name: @result + } + } + } + } + result: { + IDENT [22] { + name: @result + } + } + } + } + } + } + } + } + result: { + IDENT [29] { + name: @result + } + } + } + LIST [32] { + elements: { + LIST [33] { + elements: { + CONSTANT [34] { value: 1 } + } + } + LIST [35] { + elements: { + CONSTANT [36] { value: 2 } + } + } + } + } + } +} +Test case: NESTED_MACROS_3 +Source: [1,2,3].map(i, i * 2) + [1,2,3].map(i, i * 2).map(i, i * 2) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + COMPREHENSION [3] { + iter_var: @it:0:0 + iter_range: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + CONSTANT [6] { value: 2 } + CONSTANT [7] { value: 3 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [8] { + elements: { + } + } + } + loop_condition: { + CONSTANT [9] { value: true } + } + loop_step: { + CALL [10] { + function: _+_ + args: { + IDENT [11] { + name: @ac:0:0 + } + LIST [12] { + elements: { + CALL [13] { + function: _*_ + args: { + IDENT [14] { + name: @it:0:0 + } + CONSTANT [15] { value: 2 } + } + } + } + } + } + } + } + result: { + IDENT [16] { + name: @ac:0:0 + } + } + } + } + } + CALL [17] { + function: _+_ + args: { + IDENT [18] { + name: @index0 + } + COMPREHENSION [19] { + iter_var: @it:1:0 + iter_range: { + IDENT [20] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [21] { + elements: { + } + } + } + loop_condition: { + CONSTANT [22] { value: true } + } + loop_step: { + CALL [23] { + function: _+_ + args: { + IDENT [24] { + name: @ac:1:0 + } + LIST [25] { + elements: { + CALL [26] { + function: _*_ + args: { + IDENT [27] { + name: @it:1:0 + } + CONSTANT [28] { value: 2 } + } + } + } + } + } + } + } + result: { + IDENT [29] { + name: @ac:1:0 + } + } + } + } + } + } +} +Test case: NESTED_MACROS_COMP_V2_1 +Source: [1,2,3].transformList(i, v, [1, 2, 3].transformList(i, v, i + v + 1)) == [[2, 4, 6], [2, 4, 6], [2, 4, 6]] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + CONSTANT [5] { value: 2 } + CONSTANT [6] { value: 3 } + } + } + LIST [7] { + elements: { + CONSTANT [8] { value: 2 } + CONSTANT [9] { value: 4 } + CONSTANT [10] { value: 6 } + } + } + COMPREHENSION [11] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + IDENT [12] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [13] { + elements: { + } + } + } + loop_condition: { + CONSTANT [14] { value: true } + } + loop_step: { + CALL [15] { + function: _+_ + args: { + IDENT [16] { + name: @ac:0:0 + } + LIST [17] { + elements: { + CALL [18] { + function: _+_ + args: { + CALL [19] { + function: _+_ + args: { + IDENT [20] { + name: @it:0:0 + } + IDENT [21] { + name: @it2:0:0 + } + } + } + CONSTANT [22] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [23] { + name: @ac:0:0 + } + } + } + } + } + CALL [24] { + function: _==_ + args: { + COMPREHENSION [25] { + iter_var: @it:1:0 + iter_var2: @it2:1:0 + iter_range: { + IDENT [26] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [27] { + elements: { + } + } + } + loop_condition: { + CONSTANT [28] { value: true } + } + loop_step: { + CALL [29] { + function: _+_ + args: { + IDENT [30] { + name: @ac:1:0 + } + LIST [31] { + elements: { + IDENT [32] { + name: @index2 + } + } + } + } + } + } + result: { + IDENT [33] { + name: @ac:1:0 + } + } + } + LIST [34] { + elements: { + IDENT [35] { + name: @index1 + } + IDENT [36] { + name: @index1 + } + IDENT [37] { + name: @index1 + } + } + } + } + } + } +} +Test case: NESTED_MACROS_COMP_V2_2 +Source: [1, 2].transformList(i, y, [1, 2, 3].filter(x, x == y && i < y)) == [[1], [2]] +=====> +CALL [36] { + function: _==_ + args: { + COMPREHENSION [35] { + iter_var: i + iter_var2: y + iter_range: { + LIST [1] { + elements: { + CONSTANT [2] { value: 1 } + CONSTANT [3] { value: 2 } + } + } + } + accu_var: @result + accu_init: { + LIST [29] { + elements: { + } + } + } + loop_condition: { + CONSTANT [30] { value: true } + } + loop_step: { + CALL [33] { + function: _+_ + args: { + IDENT [31] { + name: @result + } + LIST [32] { + elements: { + COMPREHENSION [28] { + iter_var: x + iter_range: { + LIST [7] { + elements: { + CONSTANT [8] { value: 1 } + CONSTANT [9] { value: 2 } + CONSTANT [10] { value: 3 } + } + } + } + accu_var: @result + accu_init: { + LIST [20] { + elements: { + } + } + } + loop_condition: { + CONSTANT [21] { value: true } + } + loop_step: { + CALL [26] { + function: _?_:_ + args: { + CALL [16] { + function: _&&_ + args: { + CALL [14] { + function: _==_ + args: { + IDENT [13] { + name: x + } + IDENT [15] { + name: y + } + } + } + CALL [18] { + function: _<_ + args: { + IDENT [17] { + name: i + } + IDENT [19] { + name: y + } + } + } + } + } + CALL [24] { + function: _+_ + args: { + IDENT [22] { + name: @result + } + LIST [23] { + elements: { + IDENT [12] { + name: x + } + } + } + } + } + IDENT [25] { + name: @result + } + } + } + } + result: { + IDENT [27] { + name: @result + } + } + } + } + } + } + } + } + result: { + IDENT [34] { + name: @result + } + } + } + LIST [37] { + elements: { + LIST [38] { + elements: { + CONSTANT [39] { value: 1 } + } + } + LIST [40] { + elements: { + CONSTANT [41] { value: 2 } + } + } + } + } + } +} +Test case: ADJACENT_NESTED_MACROS +Source: [1,2,3].map(i, [1, 2, 3].map(i, i + 1)) == [1,2,3].map(j, [1, 2, 3].map(j, j + 1)) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + COMPREHENSION [3] { + iter_var: @it:1:0 + iter_range: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + CONSTANT [6] { value: 2 } + CONSTANT [7] { value: 3 } + } + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [8] { + elements: { + } + } + } + loop_condition: { + CONSTANT [9] { value: true } + } + loop_step: { + CALL [10] { + function: _+_ + args: { + IDENT [11] { + name: @ac:1:0 + } + LIST [12] { elements: { - COMPREHENSION [17] { - iter_var: @c1:0 + COMPREHENSION [13] { + iter_var: @it:0:0 iter_range: { - CREATE_LIST [18] { + LIST [14] { elements: { - CONSTANT [19] { value: 1 } - CONSTANT [20] { value: 2 } - CONSTANT [21] { value: 3 } + CONSTANT [15] { value: 1 } + CONSTANT [16] { value: 2 } + CONSTANT [17] { value: 3 } } } } - accu_var: @x1:0 + accu_var: @ac:0:0 accu_init: { - CREATE_LIST [22] { + LIST [18] { elements: { } } } loop_condition: { - CONSTANT [23] { value: true } + CONSTANT [19] { value: true } } loop_step: { - CALL [24] { - function: _?_:_ + CALL [20] { + function: _+_ args: { - CALL [25] { - function: _==_ - args: { - IDENT [26] { - name: @c1:0 - } - IDENT [27] { - name: @c0:0 - } - } + IDENT [21] { + name: @ac:0:0 } - CALL [28] { - function: _+_ - args: { - IDENT [29] { - name: @x1:0 - } - CREATE_LIST [30] { - elements: { - IDENT [31] { - name: @c1:0 + LIST [22] { + elements: { + CALL [23] { + function: _+_ + args: { + IDENT [24] { + name: @it:0:0 } + CONSTANT [25] { value: 1 } } } } } - IDENT [32] { - name: @x1:0 - } } } } result: { - IDENT [33] { - name: @x1:0 + IDENT [26] { + name: @ac:0:0 } } } @@ -1912,102 +2859,220 @@ CALL [1] { } } result: { - IDENT [34] { - name: @x0:0 + IDENT [27] { + name: @ac:1:0 } } } } } - CALL [35] { + CALL [28] { function: _==_ args: { - IDENT [36] { - name: @index1 + IDENT [29] { + name: @index0 } - IDENT [37] { + IDENT [30] { name: @index0 } } } } } -Test case: INCLUSION_LIST -Source: 1 in [1,2,3] && 2 in [1,2,3] && 3 in [3, [1,2,3]] && 1 in [1,2,3] +Test case: ADJACENT_NESTED_MACROS_COMP_V2 +Source: [1,2,3].transformMap(i, x, [1, 2, 3].transformMap(i, x, i + x + 1)) == [1,2,3].transformMap(j, y, [1, 2, 3].transformMap(j, y, j + y + 1)) =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - CONSTANT [5] { value: 2 } - CONSTANT [6] { value: 3 } + COMPREHENSION [3] { + iter_var: @it:1:0 + iter_var2: @it2:1:0 + iter_range: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + CONSTANT [6] { value: 2 } + CONSTANT [7] { value: 3 } + } + } + } + accu_var: @ac:1:0 + accu_init: { + MAP [8] { + + } + } + loop_condition: { + CONSTANT [9] { value: true } + } + loop_step: { + CALL [10] { + function: cel.@mapInsert + args: { + IDENT [11] { + name: @ac:1:0 + } + IDENT [12] { + name: @it:1:0 + } + COMPREHENSION [13] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + LIST [14] { + elements: { + CONSTANT [15] { value: 1 } + CONSTANT [16] { value: 2 } + CONSTANT [17] { value: 3 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + MAP [18] { + + } + } + loop_condition: { + CONSTANT [19] { value: true } + } + loop_step: { + CALL [20] { + function: cel.@mapInsert + args: { + IDENT [21] { + name: @ac:0:0 + } + IDENT [22] { + name: @it:0:0 + } + CALL [23] { + function: _+_ + args: { + CALL [24] { + function: _+_ + args: { + IDENT [25] { + name: @it:0:0 + } + IDENT [26] { + name: @it2:0:0 + } + } + } + CONSTANT [27] { value: 1 } + } + } + } + } + } + result: { + IDENT [28] { + name: @ac:0:0 + } + } + } + } + } + } + result: { + IDENT [29] { + name: @ac:1:0 + } } } - CALL [7] { + } + } + CALL [30] { + function: _==_ + args: { + IDENT [31] { + name: @index0 + } + IDENT [32] { + name: @index0 + } + } + } + } +} +Test case: INCLUSION_LIST +Source: 1 in [1,2,3] && 2 in [1,2,3] && 3 in [3, [1,2,3]] && 1 in [1,2,3] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + CALL [3] { function: @in args: { - CONSTANT [8] { value: 1 } - IDENT [9] { - name: @index0 + CONSTANT [4] { value: 1 } + LIST [5] { + elements: { + CONSTANT [6] { value: 1 } + CONSTANT [7] { value: 2 } + CONSTANT [8] { value: 3 } + } } } } - CALL [10] { + LIST [9] { + elements: { + CONSTANT [10] { value: 1 } + CONSTANT [11] { value: 2 } + CONSTANT [12] { value: 3 } + } + } + } + } + CALL [13] { + function: _&&_ + args: { + CALL [14] { function: _&&_ args: { - CALL [11] { + IDENT [15] { + name: @index0 + } + CALL [16] { function: @in args: { - CONSTANT [12] { value: 3 } - CREATE_LIST [13] { - elements: { - CONSTANT [14] { value: 3 } - IDENT [15] { - name: @index0 - } - } + CONSTANT [17] { value: 2 } + IDENT [18] { + name: @index1 } } } - IDENT [16] { - name: @index1 - } } } - CALL [17] { + CALL [19] { function: _&&_ args: { - IDENT [18] { - name: @index1 - } - CALL [19] { + CALL [20] { function: @in args: { - CONSTANT [20] { value: 2 } - IDENT [21] { - name: @index0 + CONSTANT [21] { value: 3 } + LIST [22] { + elements: { + CONSTANT [23] { value: 3 } + IDENT [24] { + name: @index1 + } + } } } } + IDENT [25] { + name: @index0 + } } } } } - CALL [22] { - function: _&&_ - args: { - IDENT [23] { - name: @index3 - } - IDENT [24] { - name: @index2 - } - } - } } } Test case: INCLUSION_MAP @@ -2016,9 +3081,9 @@ Source: 2 in {'a': 1, 2: {true: false}, 3: {true: false}} CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_MAP [3] { + MAP [3] { MAP_ENTRY [4] { key: { CONSTANT [5] { value: true } @@ -2028,31 +3093,37 @@ CALL [1] { } } } - CREATE_MAP [7] { - MAP_ENTRY [8] { + } + } + CALL [7] { + function: @in + args: { + CONSTANT [8] { value: 2 } + MAP [9] { + MAP_ENTRY [10] { key: { - CONSTANT [9] { value: "a" } + CONSTANT [11] { value: "a" } } value: { - CONSTANT [10] { value: 1 } + CONSTANT [12] { value: 1 } } } - MAP_ENTRY [11] { + MAP_ENTRY [13] { key: { - CONSTANT [12] { value: 2 } + CONSTANT [14] { value: 2 } } value: { - IDENT [13] { + IDENT [15] { name: @index0 } } } - MAP_ENTRY [14] { + MAP_ENTRY [16] { key: { - CONSTANT [15] { value: 3 } + CONSTANT [17] { value: 3 } } value: { - IDENT [16] { + IDENT [18] { name: @index0 } } @@ -2060,111 +3131,231 @@ CALL [1] { } } } - CALL [17] { - function: @in + } +} +Test case: MACRO_ITER_VAR_NOT_REFERENCED +Source: [1,2].map(i, [1, 2].map(i, [3,4])) == [[[3, 4], [3, 4]], [[3, 4], [3, 4]]] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + LIST [4] { + elements: { + CONSTANT [5] { value: 3 } + CONSTANT [6] { value: 4 } + } + } + LIST [7] { + elements: { + CONSTANT [8] { value: 3 } + CONSTANT [9] { value: 4 } + } + } + } + } + LIST [10] { + elements: { + CONSTANT [11] { value: 1 } + CONSTANT [12] { value: 2 } + } + } + } + } + CALL [13] { + function: _==_ args: { - CONSTANT [18] { value: 2 } - IDENT [19] { - name: @index1 + COMPREHENSION [14] { + iter_var: @it:1:0 + iter_range: { + IDENT [15] { + name: @index1 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [16] { + elements: { + } + } + } + loop_condition: { + CONSTANT [17] { value: true } + } + loop_step: { + CALL [18] { + function: _+_ + args: { + IDENT [19] { + name: @ac:1:0 + } + LIST [20] { + elements: { + COMPREHENSION [21] { + iter_var: @it:0:0 + iter_range: { + IDENT [22] { + name: @index1 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [23] { + elements: { + } + } + } + loop_condition: { + CONSTANT [24] { value: true } + } + loop_step: { + CALL [25] { + function: _+_ + args: { + IDENT [26] { + name: @ac:0:0 + } + LIST [27] { + elements: { + LIST [28] { + elements: { + CONSTANT [29] { value: 3 } + CONSTANT [30] { value: 4 } + } + } + } + } + } + } + } + result: { + IDENT [31] { + name: @ac:0:0 + } + } + } + } + } + } + } + } + result: { + IDENT [32] { + name: @ac:1:0 + } + } + } + LIST [33] { + elements: { + IDENT [34] { + name: @index0 + } + IDENT [35] { + name: @index0 + } + } } } } } } -Test case: MACRO_ITER_VAR_NOT_REFERENCED -Source: [1,2].map(i, [1, 2].map(i, [3,4])) == [[[3, 4], [3, 4]], [[3, 4], [3, 4]]] +Test case: MACRO_ITER_VAR2_NOT_REFERENCED +Source: [1,2].transformList(i, v, [1, 2].transformList(i, v, [3,4])) == [[[3, 4], [3, 4]], [[3, 4], [3, 4]]] =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - CONSTANT [5] { value: 2 } - } - } - CREATE_LIST [6] { - elements: { - CONSTANT [7] { value: 3 } - CONSTANT [8] { value: 4 } - } - } - CREATE_LIST [9] { + LIST [3] { elements: { - IDENT [10] { - name: @index1 + LIST [4] { + elements: { + CONSTANT [5] { value: 3 } + CONSTANT [6] { value: 4 } + } } - IDENT [11] { - name: @index1 + LIST [7] { + elements: { + CONSTANT [8] { value: 3 } + CONSTANT [9] { value: 4 } + } } } } - CREATE_LIST [12] { + LIST [10] { elements: { - IDENT [13] { - name: @index2 - } - IDENT [14] { - name: @index2 - } + CONSTANT [11] { value: 1 } + CONSTANT [12] { value: 2 } } } - COMPREHENSION [15] { - iter_var: @c0:0 + } + } + CALL [13] { + function: _==_ + args: { + COMPREHENSION [14] { + iter_var: @it:1:0 + iter_var2: @it2:1:0 iter_range: { - IDENT [16] { - name: @index0 + IDENT [15] { + name: @index1 } } - accu_var: @x0:0 + accu_var: @ac:1:0 accu_init: { - CREATE_LIST [17] { + LIST [16] { elements: { } } } loop_condition: { - CONSTANT [18] { value: true } + CONSTANT [17] { value: true } } loop_step: { - CALL [19] { + CALL [18] { function: _+_ args: { - IDENT [20] { - name: @x0:0 + IDENT [19] { + name: @ac:1:0 } - CREATE_LIST [21] { + LIST [20] { elements: { - COMPREHENSION [22] { - iter_var: @c1:0 + COMPREHENSION [21] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 iter_range: { - IDENT [23] { - name: @index0 + IDENT [22] { + name: @index1 } } - accu_var: @x1:0 + accu_var: @ac:0:0 accu_init: { - CREATE_LIST [24] { + LIST [23] { elements: { } } } loop_condition: { - CONSTANT [25] { value: true } + CONSTANT [24] { value: true } } loop_step: { - CALL [26] { + CALL [25] { function: _+_ args: { - IDENT [27] { - name: @x1:0 + IDENT [26] { + name: @ac:0:0 } - CREATE_LIST [28] { + LIST [27] { elements: { - IDENT [29] { - name: @index1 + LIST [28] { + elements: { + CONSTANT [29] { value: 3 } + CONSTANT [30] { value: 4 } + } } } } @@ -2172,8 +3363,8 @@ CALL [1] { } } result: { - IDENT [30] { - name: @x1:0 + IDENT [31] { + name: @ac:0:0 } } } @@ -2183,21 +3374,20 @@ CALL [1] { } } result: { - IDENT [31] { - name: @x0:0 + IDENT [32] { + name: @ac:1:0 } } } - } - } - CALL [32] { - function: _==_ - args: { - IDENT [33] { - name: @index4 - } - IDENT [34] { - name: @index3 + LIST [33] { + elements: { + IDENT [34] { + name: @index0 + } + IDENT [35] { + name: @index0 + } + } } } } @@ -2209,59 +3399,324 @@ Source: [x - 1 > 3 ? x - 1 : 5].exists(x, x - 1 > 3) || x - 1 > 3 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { - function: _-_ - args: { - IDENT [4] { - name: x - } - CONSTANT [5] { value: 1 } - } - } - CALL [6] { function: _>_ args: { - IDENT [7] { - name: @index0 + CALL [4] { + function: _-_ + args: { + IDENT [5] { + name: x + } + CONSTANT [6] { value: 1 } + } } - CONSTANT [8] { value: 3 } + CONSTANT [7] { value: 3 } } } + } + } + CALL [8] { + function: _||_ + args: { COMPREHENSION [9] { - iter_var: @c0:0 + iter_var: @it:0:0 iter_range: { - CREATE_LIST [10] { + LIST [10] { elements: { CALL [11] { function: _?_:_ args: { IDENT [12] { - name: @index1 + name: @index0 } - IDENT [13] { + CALL [13] { + function: _-_ + args: { + IDENT [14] { + name: x + } + CONSTANT [15] { value: 1 } + } + } + CONSTANT [16] { value: 5 } + } + } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [17] { value: false } + } + loop_condition: { + CALL [18] { + function: @not_strictly_false + args: { + CALL [19] { + function: !_ + args: { + IDENT [20] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [21] { + function: _||_ + args: { + IDENT [22] { + name: @ac:0:0 + } + CALL [23] { + function: _>_ + args: { + CALL [24] { + function: _-_ + args: { + IDENT [25] { + name: @it:0:0 + } + CONSTANT [26] { value: 1 } + } + } + CONSTANT [27] { value: 3 } + } + } + } + } + } + result: { + IDENT [28] { + name: @ac:0:0 + } + } + } + IDENT [29] { + name: @index0 + } + } + } + } +} +Test case: MACRO_SHADOWED_VARIABLE_2 +Source: ["foo", "bar"].map(x, [x + x, x + x]).map(x, [x + x, x + x]) +=====> +COMPREHENSION [35] { + iter_var: x + iter_range: { + COMPREHENSION [19] { + iter_var: x + iter_range: { + LIST [1] { + elements: { + CONSTANT [2] { value: "foo" } + CONSTANT [3] { value: "bar" } + } + } + } + accu_var: @result + accu_init: { + LIST [13] { + elements: { + } + } + } + loop_condition: { + CONSTANT [14] { value: true } + } + loop_step: { + CALL [17] { + function: _+_ + args: { + IDENT [15] { + name: @result + } + LIST [16] { + elements: { + LIST [6] { + elements: { + CALL [8] { + function: _+_ + args: { + IDENT [7] { + name: x + } + IDENT [9] { + name: x + } + } + } + CALL [11] { + function: _+_ + args: { + IDENT [10] { + name: x + } + IDENT [12] { + name: x + } + } + } + } + } + } + } + } + } + } + result: { + IDENT [18] { + name: @result + } + } + } + } + accu_var: @result + accu_init: { + LIST [29] { + elements: { + } + } + } + loop_condition: { + CONSTANT [30] { value: true } + } + loop_step: { + CALL [33] { + function: _+_ + args: { + IDENT [31] { + name: @result + } + LIST [32] { + elements: { + LIST [22] { + elements: { + CALL [24] { + function: _+_ + args: { + IDENT [23] { + name: x + } + IDENT [25] { + name: x + } + } + } + CALL [27] { + function: _+_ + args: { + IDENT [26] { + name: x + } + IDENT [28] { + name: x + } + } + } + } + } + } + } + } + } + } + result: { + IDENT [34] { + name: @result + } + } +} +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 +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + CALL [3] { + function: _>_ + args: { + CALL [4] { + function: _-_ + args: { + CALL [5] { + function: _-_ + args: { + IDENT [6] { + name: x + } + IDENT [7] { + name: y + } + } + } + CONSTANT [8] { value: 1 } + } + } + CONSTANT [9] { value: 3 } + } + } + } + } + CALL [10] { + function: _||_ + args: { + COMPREHENSION [11] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + LIST [12] { + elements: { + CALL [13] { + function: _?_:_ + args: { + IDENT [14] { name: @index0 } - CONSTANT [14] { value: 5 } + CALL [15] { + function: _-_ + args: { + CALL [16] { + function: _-_ + args: { + IDENT [17] { + name: x + } + IDENT [18] { + name: y + } + } + } + CONSTANT [19] { value: 1 } + } + } + CONSTANT [20] { value: 5 } } } } } } - accu_var: @x0:0 + accu_var: @ac:0:0 accu_init: { - CONSTANT [15] { value: false } + CONSTANT [21] { value: false } } loop_condition: { - CALL [16] { + CALL [22] { function: @not_strictly_false args: { - CALL [17] { + CALL [23] { function: !_ args: { - IDENT [18] { - name: @x0:0 + IDENT [24] { + name: @ac:0:0 } } } @@ -2269,96 +3724,109 @@ CALL [1] { } } loop_step: { - CALL [19] { + CALL [25] { function: _||_ args: { - IDENT [20] { - name: @x0:0 + IDENT [26] { + name: @ac:0:0 } - CALL [21] { + CALL [27] { function: _>_ args: { - CALL [22] { + CALL [28] { function: _-_ args: { - IDENT [23] { - name: @c0:0 + CALL [29] { + function: _-_ + args: { + IDENT [30] { + name: @it:0:0 + } + IDENT [31] { + name: @it2:0:0 + } + } } - CONSTANT [24] { value: 1 } + CONSTANT [32] { value: 1 } } } - CONSTANT [25] { value: 3 } + CONSTANT [33] { value: 3 } } } } } } result: { - IDENT [26] { - name: @x0:0 + IDENT [34] { + name: @ac:0:0 } } } - } - } - CALL [27] { - function: _||_ - args: { - IDENT [28] { - name: @index2 - } - IDENT [29] { - name: @index1 + IDENT [35] { + name: @index0 } } } } } -Test case: MACRO_SHADOWED_VARIABLE_2 -Source: ["foo", "bar"].map(x, [x + x, x + x]).map(x, [x + x, x + x]) +Test case: MACRO_SHADOWED_VARIABLE_COMP_V2_2 +Source: ["foo", "bar"].transformMap(x, y, [x + x, y + y]).transformMap(x, y, [x + x, y + y]) =====> -CALL [1] { - function: cel.@block - args: { - CREATE_LIST [2] { - elements: { - CALL [3] { - function: _+_ - args: { - IDENT [4] { - name: @c1:0 - } - IDENT [5] { - name: @c1:0 - } +COMPREHENSION [35] { + iter_var: x + iter_var2: y + iter_range: { + COMPREHENSION [19] { + iter_var: x + iter_var2: y + iter_range: { + LIST [1] { + elements: { + CONSTANT [2] { value: "foo" } + CONSTANT [3] { value: "bar" } } } - CALL [6] { - function: _+_ - args: { - IDENT [7] { - name: @c0:0 - } - IDENT [8] { - name: @c0:0 - } - } + } + accu_var: @result + accu_init: { + MAP [14] { + } - CALL [9] { - function: _+_ + } + loop_condition: { + CONSTANT [15] { value: true } + } + loop_step: { + CALL [17] { + function: cel.@mapInsert args: { - IDENT [10] { - name: @x0:0 + IDENT [16] { + name: @result + } + IDENT [5] { + name: x } - CREATE_LIST [11] { + LIST [7] { elements: { - CREATE_LIST [12] { - elements: { - IDENT [13] { - name: @index1 + CALL [9] { + function: _+_ + args: { + IDENT [8] { + name: x } - IDENT [14] { - name: @index1 + IDENT [10] { + name: x + } + } + } + CALL [12] { + function: _+_ + args: { + IDENT [11] { + name: y + } + IDENT [13] { + name: y } } } @@ -2366,85 +3834,65 @@ CALL [1] { } } } - COMPREHENSION [15] { - iter_var: @c1:0 - iter_range: { - CREATE_LIST [16] { - elements: { - CONSTANT [17] { value: "foo" } - CONSTANT [18] { value: "bar" } - } - } - } - accu_var: @x1:0 - accu_init: { - CREATE_LIST [19] { - elements: { + } + result: { + IDENT [18] { + name: @result + } + } + } + } + accu_var: @result + accu_init: { + MAP [30] { + + } + } + loop_condition: { + CONSTANT [31] { value: true } + } + loop_step: { + CALL [33] { + function: cel.@mapInsert + args: { + IDENT [32] { + name: @result + } + IDENT [21] { + name: x + } + LIST [23] { + elements: { + CALL [25] { + function: _+_ + args: { + IDENT [24] { + name: x + } + IDENT [26] { + name: x + } } } - } - loop_condition: { - CONSTANT [20] { value: true } - } - loop_step: { - CALL [21] { + CALL [28] { function: _+_ args: { - IDENT [22] { - name: @x1:0 + IDENT [27] { + name: y } - CREATE_LIST [23] { - elements: { - CREATE_LIST [24] { - elements: { - IDENT [25] { - name: @index0 - } - IDENT [26] { - name: @index0 - } - } - } - } + IDENT [29] { + name: y } } } } - result: { - IDENT [27] { - name: @x1:0 - } - } } } } - COMPREHENSION [28] { - iter_var: @c0:0 - iter_range: { - IDENT [29] { - name: @index3 - } - } - accu_var: @x0:0 - accu_init: { - CREATE_LIST [30] { - elements: { - } - } - } - loop_condition: { - CONSTANT [31] { value: true } - } - loop_step: { - IDENT [32] { - name: @index2 - } - } - result: { - IDENT [33] { - name: @x0:0 - } - } + } + result: { + IDENT [34] { + name: @result } } } @@ -2454,9 +3902,9 @@ Source: has({'a': true}.a) && {'a':true}['a'] CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_MAP [3] { + MAP [3] { MAP_ENTRY [4] { key: { CONSTANT [5] { value: "a" } @@ -2466,27 +3914,59 @@ CALL [1] { } } } - CALL [7] { + } + } + CALL [7] { + function: _&&_ + args: { + SELECT [8] { + IDENT [9] { + name: @index0 + }.a~presence_test + } + CALL [10] { function: _[_] args: { - IDENT [8] { + IDENT [11] { name: @index0 } - CONSTANT [9] { value: "a" } + CONSTANT [12] { value: "a" } } } } } - CALL [10] { + } +} +Test case: PRESENCE_TEST_2 +Source: has({'a': true}.a) && has({'a': true}.a) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + SELECT [3] { + MAP [4] { + MAP_ENTRY [5] { + key: { + CONSTANT [6] { value: "a" } + } + value: { + CONSTANT [7] { value: true } + } + } + }.a~presence_test + } + } + } + CALL [8] { function: _&&_ args: { - SELECT [11] { - IDENT [12] { - name: @index0 - }.a~presence_test + IDENT [9] { + name: @index0 } - IDENT [13] { - name: @index1 + IDENT [10] { + name: @index0 } } } @@ -2498,40 +3978,37 @@ Source: (has(msg.oneof_type.payload) ? msg.oneof_type.payload.single_int64 : 0) CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { name: msg }.oneof_type } - CALL [5] { + } + } + CALL [5] { + function: _==_ + args: { + CALL [6] { function: _?_:_ args: { - SELECT [6] { - IDENT [7] { + SELECT [7] { + IDENT [8] { name: @index0 }.payload~presence_test } - SELECT [8] { - SELECT [9] { - IDENT [10] { + SELECT [9] { + SELECT [10] { + IDENT [11] { name: @index0 }.payload }.single_int64 } - CONSTANT [11] { value: 0 } + CONSTANT [12] { value: 0 } } } - } - } - CALL [12] { - function: _==_ - args: { - IDENT [13] { - name: @index1 - } - CONSTANT [14] { value: 10 } + CONSTANT [13] { value: 10 } } } } @@ -2542,54 +4019,47 @@ Source: (has(msg.oneof_type.payload) ? msg.oneof_type.payload.single_int64 : msg CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload }.single_int64 } - CALL [9] { + } + } + CALL [7] { + function: _==_ + args: { + CALL [8] { function: _?_:_ args: { - SELECT [10] { - IDENT [11] { - name: @index0 + SELECT [9] { + SELECT [10] { + IDENT [11] { + name: msg + }.oneof_type }.payload~presence_test } IDENT [12] { - name: @index2 + name: @index0 } CALL [13] { function: _*_ args: { IDENT [14] { - name: @index2 + name: @index0 } CONSTANT [15] { value: 0 } } } } } - } - } - CALL [16] { - function: _==_ - args: { - IDENT [17] { - name: @index3 - } - CONSTANT [18] { value: 10 } + CONSTANT [16] { value: 10 } } } } @@ -2600,54 +4070,49 @@ Source: (has(msg.oneof_type.payload.single_int64) ? msg.oneof_type.payload.singl CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload }.single_int64 } - CALL [9] { + } + } + CALL [7] { + function: _==_ + args: { + CALL [8] { function: _?_:_ args: { - SELECT [10] { - IDENT [11] { - name: @index1 + SELECT [9] { + SELECT [10] { + SELECT [11] { + IDENT [12] { + name: msg + }.oneof_type + }.payload }.single_int64~presence_test } - IDENT [12] { - name: @index2 + IDENT [13] { + name: @index0 } - CALL [13] { + CALL [14] { function: _*_ args: { - IDENT [14] { - name: @index2 + IDENT [15] { + name: @index0 } - CONSTANT [15] { value: 0 } + CONSTANT [16] { value: 0 } } } } } - } - } - CALL [16] { - function: _==_ - args: { - IDENT [17] { - name: @index3 - } - CONSTANT [18] { value: 10 } + CONSTANT [17] { value: 10 } } } } @@ -2658,91 +4123,88 @@ Source: (has(msg.oneof_type) && has(msg.oneof_type.payload) && has(msg.oneof_typ CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.map_string_string } SELECT [7] { - IDENT [8] { - name: @index1 - }.map_string_string + SELECT [8] { + IDENT [9] { + name: msg + }.oneof_type + }.payload } - CALL [9] { - function: _?_:_ + } + } + CALL [10] { + function: _?_:_ + args: { + CALL [11] { + function: _&&_ args: { - CALL [10] { + CALL [12] { function: _&&_ args: { - SELECT [11] { - IDENT [12] { - name: @index1 - }.map_string_string~presence_test - } SELECT [13] { IDENT [14] { - name: @index2 - }.key~presence_test + name: msg + }.oneof_type~presence_test } - } - } - CALL [15] { - function: _==_ - args: { - SELECT [16] { - IDENT [17] { - name: @index2 - }.key + SELECT [15] { + SELECT [16] { + IDENT [17] { + name: msg + }.oneof_type + }.payload~presence_test } - CONSTANT [18] { value: "A" } } } - CONSTANT [19] { value: false } + SELECT [18] { + IDENT [19] { + name: @index1 + }.single_int64~presence_test + } } } CALL [20] { - function: _&&_ + function: _?_:_ args: { CALL [21] { function: _&&_ args: { SELECT [22] { IDENT [23] { - name: msg - }.oneof_type~presence_test + name: @index1 + }.map_string_string~presence_test } SELECT [24] { IDENT [25] { name: @index0 - }.payload~presence_test + }.key~presence_test } } } - SELECT [26] { - IDENT [27] { - name: @index1 - }.single_int64~presence_test + CALL [26] { + function: _==_ + args: { + SELECT [27] { + IDENT [28] { + name: @index0 + }.key + } + CONSTANT [29] { value: "A" } + } } + CONSTANT [30] { value: false } } } - } - } - CALL [28] { - function: _?_:_ - args: { - IDENT [29] { - name: @index4 - } - IDENT [30] { - name: @index3 - } CONSTANT [31] { value: false } } } @@ -2754,46 +4216,51 @@ Source: [10, ?optional.none(), [?optional.none(), ?opt_x], [?optional.none(), ?o CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CALL [3] { - function: optional.none - args: { - } - } - CREATE_LIST [4] { + LIST [3] { elements: { - IDENT [5] { - name: @index0 + CALL [4] { + function: optional.none + args: { + } } - IDENT [6] { + IDENT [5] { name: opt_x } } optional_indices: [0, 1] } - CREATE_LIST [7] { + LIST [6] { elements: { - CONSTANT [8] { value: 5 } + CONSTANT [7] { value: 5 } } } - CREATE_LIST [9] { + } + } + CALL [8] { + function: _==_ + args: { + LIST [9] { elements: { CONSTANT [10] { value: 10 } - IDENT [11] { - name: @index2 + CALL [11] { + function: optional.none + args: { + } } IDENT [12] { - name: @index2 + name: @index0 + } + IDENT [13] { + name: @index0 } } + optional_indices: [0] } - CREATE_LIST [13] { + LIST [14] { elements: { - CONSTANT [14] { value: 10 } - IDENT [15] { - name: @index0 - } + CONSTANT [15] { value: 10 } IDENT [16] { name: @index1 } @@ -2801,18 +4268,6 @@ CALL [1] { name: @index1 } } - optional_indices: [0] - } - } - } - CALL [18] { - function: _==_ - args: { - IDENT [19] { - name: @index4 - } - IDENT [20] { - name: @index3 } } } @@ -2824,56 +4279,47 @@ Source: {?'hello': optional.of('hello')}['hello'] + {?'hello': optional.of('hell CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { - function: optional.of - args: { - CONSTANT [4] { value: "hello" } - } - } - CREATE_MAP [5] { - MAP_ENTRY [6] { - key: { - CONSTANT [7] { value: "hello" } - } - optional_entry: true - value: { - IDENT [8] { - name: @index0 - } - } - } - } - CALL [9] { function: _[_] args: { - IDENT [10] { - name: @index1 + MAP [4] { + MAP_ENTRY [5] { + key: { + CONSTANT [6] { value: "hello" } + } + optional_entry: true + value: { + CALL [7] { + function: optional.of + args: { + CONSTANT [8] { value: "hello" } + } + } + } + } } - CONSTANT [11] { value: "hello" } + CONSTANT [9] { value: "hello" } } } - CALL [12] { + } + } + CALL [10] { + function: _==_ + args: { + CALL [11] { function: _+_ args: { - IDENT [13] { - name: @index2 + IDENT [12] { + name: @index0 } - IDENT [14] { - name: @index2 + IDENT [13] { + name: @index0 } } } - } - } - CALL [15] { - function: _==_ - args: { - IDENT [16] { - name: @index3 - } - CONSTANT [17] { value: "hellohello" } + CONSTANT [14] { value: "hellohello" } } } } @@ -2884,9 +4330,9 @@ Source: {?'key': optional.of('test')}[?'bogus'].or({'key': 'test'}[?'bogus']).or CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_MAP [3] { + MAP [3] { MAP_ENTRY [4] { key: { CONSTANT [5] { value: "key" } @@ -2896,69 +4342,66 @@ CALL [1] { } } } - CALL [7] { + } + } + CALL [7] { + function: _==_ + args: { + CALL [8] { function: orValue target: { - CALL [8] { + CALL [9] { function: or target: { - CALL [9] { + CALL [10] { function: _[?_] args: { - CREATE_MAP [10] { - MAP_ENTRY [11] { + MAP [11] { + MAP_ENTRY [12] { key: { - CONSTANT [12] { value: "key" } + CONSTANT [13] { value: "key" } } optional_entry: true value: { - CALL [13] { + CALL [14] { function: optional.of args: { - CONSTANT [14] { value: "test" } + CONSTANT [15] { value: "test" } } } } } } - CONSTANT [15] { value: "bogus" } + CONSTANT [16] { value: "bogus" } } } } args: { - CALL [16] { + CALL [17] { function: _[?_] args: { - IDENT [17] { + IDENT [18] { name: @index0 } - CONSTANT [18] { value: "bogus" } + CONSTANT [19] { value: "bogus" } } } } } } args: { - CALL [19] { + CALL [20] { function: _[_] args: { - IDENT [20] { + IDENT [21] { name: @index0 } - CONSTANT [21] { value: "key" } + CONSTANT [22] { value: "key" } } } } } - } - } - CALL [22] { - function: _==_ - args: { - IDENT [23] { - name: @index1 - } - CONSTANT [24] { value: "test" } + CONSTANT [23] { value: "test" } } } } @@ -2969,146 +4412,69 @@ Source: TestAllTypes{?single_int64: optional.ofNonZeroValue(1), ?single_int32: o CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CALL [3] { - function: optional.ofNonZeroValue - args: { - CONSTANT [4] { value: 1 } - } - } - CALL [5] { - function: optional.of - args: { - CONSTANT [6] { value: 4 } - } - } - CREATE_STRUCT [7] { - name: TestAllTypes + STRUCT [3] { + name: cel.expr.conformance.proto3.TestAllTypes entries: { - ENTRY [8] { + ENTRY [4] { field_key: single_int64 optional_entry: true value: { - IDENT [9] { - name: @index0 + CALL [5] { + function: optional.ofNonZeroValue + args: { + CONSTANT [6] { value: 1 } + } } } } - ENTRY [10] { + ENTRY [7] { field_key: single_int32 optional_entry: true - value: { - IDENT [11] { - name: @index1 - } - } - } - } - } - CALL [12] { - function: _+_ - args: { - SELECT [13] { - IDENT [14] { - name: @index2 - }.single_int32 - } - SELECT [15] { - IDENT [16] { - name: @index2 - }.single_int64 - } - } - } - } - } - CALL [17] { - function: _==_ - args: { - IDENT [18] { - name: @index3 - } - CONSTANT [19] { value: 5 } - } - } - } -} -Test case: CALL -Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('h' + 'e' + 'l' + 'l' + 'o') -=====> -CALL [1] { - function: cel.@block - args: { - CREATE_LIST [2] { - elements: { - CALL [3] { - function: _+_ - args: { - CONSTANT [4] { value: "h" } - CONSTANT [5] { value: "e" } - } - } - CALL [6] { - function: _+_ - args: { - IDENT [7] { - name: @index0 - } - CONSTANT [8] { value: "l" } - } - } - CALL [9] { - function: _+_ - args: { - IDENT [10] { - name: @index1 + value: { + CALL [8] { + function: optional.of + args: { + CONSTANT [9] { value: 4 } + } + } + } } - CONSTANT [11] { value: "l" } } } - CALL [12] { + } + } + CALL [10] { + function: _==_ + args: { + CALL [11] { function: _+_ args: { - IDENT [13] { - name: @index2 + SELECT [12] { + IDENT [13] { + name: @index0 + }.single_int32 } - CONSTANT [14] { value: "o" } - } - } - CALL [15] { - function: _+_ - args: { - IDENT [16] { - name: @index3 + SELECT [14] { + IDENT [15] { + name: @index0 + }.single_int64 } - CONSTANT [17] { value: " world" } } } - } - } - CALL [18] { - function: matches - target: { - IDENT [19] { - name: @index4 - } - } - args: { - IDENT [20] { - name: @index3 - } + CONSTANT [16] { value: 5 } } } } } -Test case: CALL_ARGUMENT_NESTED_NO_COMMON_SUBEXPR -Source: 'hello world'.matches('h' + 'e' + 'l' + 'l' + 'o') +Test case: CALL +Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('h' + 'e' + 'l' + 'l' + 'o') =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { function: _+_ @@ -3140,147 +4506,164 @@ CALL [1] { CALL [12] { function: matches target: { - CONSTANT [13] { value: "hello world" } + CALL [13] { + function: _+_ + args: { + IDENT [14] { + name: @index0 + } + CONSTANT [15] { value: " world" } + } + } } args: { - IDENT [14] { + IDENT [16] { name: @index0 } } } } } -Test case: CALL_TARGET_NESTED_NO_COMMON_SUBEXPR -Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('hello') +Test case: CALL_ARGUMENT_NESTED_NO_COMMON_SUBEXPR +Source: 'hello world'.matches('h' + 'e' + 'l' + 'l' + 'o') =====> -CALL [1] { - function: cel.@block +CALL [2] { + function: matches + target: { + CONSTANT [1] { value: "hello world" } + } args: { - CREATE_LIST [2] { - elements: { - CALL [3] { + CALL [10] { + function: _+_ + args: { + CALL [8] { function: _+_ args: { - CALL [4] { + CALL [6] { function: _+_ args: { - CALL [5] { + CALL [4] { function: _+_ args: { - CALL [6] { - function: _+_ - args: { - CALL [7] { - function: _+_ - args: { - CONSTANT [8] { value: "h" } - CONSTANT [9] { value: "e" } - } - } - CONSTANT [10] { value: "l" } - } - } - CONSTANT [11] { value: "l" } + CONSTANT [3] { value: "h" } + CONSTANT [5] { value: "e" } } } - CONSTANT [12] { value: "o" } + CONSTANT [7] { value: "l" } } } - CONSTANT [13] { value: " world" } + CONSTANT [9] { value: "l" } } } - } - } - CALL [14] { - function: matches - target: { - IDENT [15] { - name: @index0 - } - } - args: { - CONSTANT [16] { value: "hello" } + CONSTANT [11] { value: "o" } } } } } -Test case: CALL_BOTH_ARGUMENT_TARGET_NESTED_NO_COMMON_SUBEXPR -Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('w' + 'o' + 'r' + 'l' + 'd') +Test case: CALL_TARGET_NESTED_NO_COMMON_SUBEXPR +Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('hello') =====> -CALL [1] { - function: cel.@block - args: { - CREATE_LIST [2] { - elements: { - CALL [3] { +CALL [12] { + function: matches + target: { + CALL [10] { + function: _+_ + args: { + CALL [8] { function: _+_ args: { - CALL [4] { + CALL [6] { function: _+_ args: { - CALL [5] { + CALL [4] { function: _+_ args: { - CALL [6] { + CALL [2] { function: _+_ args: { - CONSTANT [7] { value: "w" } - CONSTANT [8] { value: "o" } + CONSTANT [1] { value: "h" } + CONSTANT [3] { value: "e" } } } - CONSTANT [9] { value: "r" } + CONSTANT [5] { value: "l" } } } - CONSTANT [10] { value: "l" } + CONSTANT [7] { value: "l" } } } - CONSTANT [11] { value: "d" } + CONSTANT [9] { value: "o" } } } - CALL [12] { + CONSTANT [11] { value: " world" } + } + } + } + args: { + CONSTANT [13] { value: "hello" } + } +} +Test case: CALL_BOTH_ARGUMENT_TARGET_NESTED_NO_COMMON_SUBEXPR +Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('w' + 'o' + 'r' + 'l' + 'd') +=====> +CALL [12] { + function: matches + target: { + CALL [10] { + function: _+_ + args: { + CALL [8] { function: _+_ args: { - CALL [13] { + CALL [6] { function: _+_ args: { - CALL [14] { + CALL [4] { function: _+_ args: { - CALL [15] { + CALL [2] { function: _+_ args: { - CALL [16] { - function: _+_ - args: { - CONSTANT [17] { value: "h" } - CONSTANT [18] { value: "e" } - } - } - CONSTANT [19] { value: "l" } + CONSTANT [1] { value: "h" } + CONSTANT [3] { value: "e" } } } - CONSTANT [20] { value: "l" } + CONSTANT [5] { value: "l" } } } - CONSTANT [21] { value: "o" } + CONSTANT [7] { value: "l" } } } - CONSTANT [22] { value: " world" } + CONSTANT [9] { value: "o" } } } + CONSTANT [11] { value: " world" } } } - CALL [23] { - function: matches - target: { - IDENT [24] { - name: @index1 - } - } + } + args: { + CALL [20] { + function: _+_ args: { - IDENT [25] { - name: @index0 + CALL [18] { + function: _+_ + args: { + CALL [16] { + function: _+_ + args: { + CALL [14] { + function: _+_ + args: { + CONSTANT [13] { value: "w" } + CONSTANT [15] { value: "o" } + } + } + CONSTANT [17] { value: "r" } + } + } + CONSTANT [19] { value: "l" } + } } + CONSTANT [21] { value: "d" } } } } @@ -3291,77 +4674,69 @@ Source: non_pure_custom_func(msg.oneof_type.payload.single_int64) + non_pure_cus CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 - }.single_int64 - } - SELECT [9] { - IDENT [10] { - name: msg + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload }.single_int64 } - SELECT [11] { - IDENT [12] { - name: @index1 - }.single_int32 - } } } - CALL [13] { + CALL [7] { function: _+_ args: { - CALL [14] { + CALL [8] { function: _+_ args: { - CALL [15] { + CALL [9] { function: _+_ args: { - CALL [16] { + CALL [10] { function: non_pure_custom_func args: { - IDENT [17] { - name: @index2 + IDENT [11] { + name: @index0 } } } - CALL [18] { + CALL [12] { function: non_pure_custom_func args: { - IDENT [19] { - name: @index4 + SELECT [13] { + SELECT [14] { + SELECT [15] { + IDENT [16] { + name: msg + }.oneof_type + }.payload + }.single_int32 } } } } } - CALL [20] { + CALL [17] { function: non_pure_custom_func args: { - IDENT [21] { - name: @index2 + IDENT [18] { + name: @index0 } } } } } - CALL [22] { + CALL [19] { function: non_pure_custom_func args: { - IDENT [23] { - name: @index3 + SELECT [20] { + IDENT [21] { + name: msg + }.single_int64 } } } @@ -3375,79 +4750,68 @@ Source: pure_custom_func(msg.oneof_type.payload.single_int64) + pure_custom_func CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 - }.single_int64 - } - CALL [9] { - function: pure_custom_func - args: { - IDENT [10] { - name: @index2 - } - } - } - CALL [11] { + CALL [3] { function: pure_custom_func args: { - SELECT [12] { - IDENT [13] { - name: msg + SELECT [4] { + SELECT [5] { + SELECT [6] { + IDENT [7] { + name: msg + }.oneof_type + }.payload }.single_int64 } } } - CALL [14] { + } + } + CALL [8] { + function: _+_ + args: { + CALL [9] { function: _+_ args: { - CALL [15] { + CALL [10] { function: _+_ args: { - IDENT [16] { - name: @index3 + IDENT [11] { + name: @index0 } - CALL [17] { + CALL [12] { function: pure_custom_func args: { - SELECT [18] { - IDENT [19] { - name: @index1 + SELECT [13] { + SELECT [14] { + SELECT [15] { + IDENT [16] { + name: msg + }.oneof_type + }.payload }.single_int32 } } } } } - IDENT [20] { - name: @index3 + IDENT [17] { + name: @index0 } } } - } - } - CALL [21] { - function: _+_ - args: { - IDENT [22] { - name: @index5 - } - IDENT [23] { - name: @index4 + CALL [18] { + function: pure_custom_func + args: { + SELECT [19] { + IDENT [20] { + name: msg + }.single_int64 + } + } } } } } -} +} \ No newline at end of file diff --git a/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_8.baseline b/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_8.baseline index 67f1b1308..bd1fa8d45 100644 --- a/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_8.baseline +++ b/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_8.baseline @@ -4,22 +4,24 @@ Source: size([1,2]) + size([1,2]) + 1 == 5 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - CONSTANT [5] { value: 2 } - } - } - CALL [6] { + CALL [3] { function: size args: { - IDENT [7] { - name: @index0 + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + CONSTANT [6] { value: 2 } + } } } } + } + } + CALL [7] { + function: _==_ + args: { CALL [8] { function: _+_ args: { @@ -27,25 +29,17 @@ CALL [1] { function: _+_ args: { IDENT [10] { - name: @index1 + name: @index0 } IDENT [11] { - name: @index1 + name: @index0 } } } CONSTANT [12] { value: 1 } } } - } - } - CALL [13] { - function: _==_ - args: { - IDENT [14] { - name: @index2 - } - CONSTANT [15] { value: 5 } + CONSTANT [13] { value: 5 } } } } @@ -56,22 +50,24 @@ Source: 2 + size([1,2]) + size([1,2]) + 1 == 7 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - CONSTANT [5] { value: 2 } - } - } - CALL [6] { + CALL [3] { function: size args: { - IDENT [7] { - name: @index0 + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + CONSTANT [6] { value: 2 } + } } } } + } + } + CALL [7] { + function: _==_ + args: { CALL [8] { function: _+_ args: { @@ -83,27 +79,19 @@ CALL [1] { args: { CONSTANT [11] { value: 2 } IDENT [12] { - name: @index1 + name: @index0 } } } IDENT [13] { - name: @index1 + name: @index0 } } } CONSTANT [14] { value: 1 } } } - } - } - CALL [15] { - function: _==_ - args: { - IDENT [16] { - name: @index2 - } - CONSTANT [17] { value: 7 } + CONSTANT [15] { value: 7 } } } } @@ -114,71 +102,62 @@ Source: size([0]) + size([0]) + size([1,2]) + size([1,2]) == 6 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 0 } - } - } - CALL [5] { + CALL [3] { function: size args: { - IDENT [6] { - name: @index0 + LIST [4] { + elements: { + CONSTANT [5] { value: 0 } + } } } } - CREATE_LIST [7] { - elements: { - CONSTANT [8] { value: 1 } - CONSTANT [9] { value: 2 } - } - } - CALL [10] { + CALL [6] { function: size args: { - IDENT [11] { - name: @index2 + LIST [7] { + elements: { + CONSTANT [8] { value: 1 } + CONSTANT [9] { value: 2 } + } } } } - CALL [12] { + } + } + CALL [10] { + function: _==_ + args: { + CALL [11] { function: _+_ args: { - CALL [13] { + CALL [12] { function: _+_ args: { - CALL [14] { + CALL [13] { function: _+_ args: { - IDENT [15] { - name: @index1 + IDENT [14] { + name: @index0 } - IDENT [16] { - name: @index1 + IDENT [15] { + name: @index0 } } } - IDENT [17] { - name: @index3 + IDENT [16] { + name: @index1 } } } - IDENT [18] { - name: @index3 + IDENT [17] { + name: @index1 } } } - } - } - CALL [19] { - function: _==_ - args: { - IDENT [20] { - name: @index4 - } - CONSTANT [21] { value: 6 } + CONSTANT [18] { value: 6 } } } } @@ -189,108 +168,96 @@ Source: 5 + size([0]) + size([0]) + size([1,2]) + size([1,2]) + size([1,2,3]) + CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 0 } - } - } - CALL [5] { + CALL [3] { function: size args: { - IDENT [6] { - name: @index0 + LIST [4] { + elements: { + CONSTANT [5] { value: 0 } + } } } } - CREATE_LIST [7] { - elements: { - CONSTANT [8] { value: 1 } - CONSTANT [9] { value: 2 } - } - } - CALL [10] { + CALL [6] { function: size args: { - IDENT [11] { - name: @index2 + LIST [7] { + elements: { + CONSTANT [8] { value: 1 } + CONSTANT [9] { value: 2 } + } } } } - CREATE_LIST [12] { - elements: { - CONSTANT [13] { value: 1 } - CONSTANT [14] { value: 2 } - CONSTANT [15] { value: 3 } - } - } - CALL [16] { + CALL [10] { function: size args: { - IDENT [17] { - name: @index4 + LIST [11] { + elements: { + CONSTANT [12] { value: 1 } + CONSTANT [13] { value: 2 } + CONSTANT [14] { value: 3 } + } } } } - CALL [18] { + } + } + CALL [15] { + function: _==_ + args: { + CALL [16] { function: _+_ args: { - CALL [19] { + CALL [17] { function: _+_ args: { - CALL [20] { + CALL [18] { function: _+_ args: { - CALL [21] { + CALL [19] { function: _+_ args: { - CALL [22] { + CALL [20] { function: _+_ args: { - CALL [23] { + CALL [21] { function: _+_ args: { - CONSTANT [24] { value: 5 } - IDENT [25] { - name: @index1 + CONSTANT [22] { value: 5 } + IDENT [23] { + name: @index0 } } } - IDENT [26] { - name: @index1 + IDENT [24] { + name: @index0 } } } - IDENT [27] { - name: @index3 + IDENT [25] { + name: @index1 } } } - IDENT [28] { - name: @index3 + IDENT [26] { + name: @index1 } } } - IDENT [29] { - name: @index5 + IDENT [27] { + name: @index2 } } } - IDENT [30] { - name: @index5 + IDENT [28] { + name: @index2 } } } - } - } - CALL [31] { - function: _==_ - args: { - IDENT [32] { - name: @index6 - } - CONSTANT [33] { value: 17 } + CONSTANT [29] { value: 17 } } } } @@ -301,145 +268,115 @@ Source: timestamp(int(timestamp(1000000000))).getFullYear() + timestamp(int(time CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { - function: timestamp - args: { - CONSTANT [4] { value: 1000000000 } - } - } - CALL [5] { - function: int - args: { - IDENT [6] { - name: @index0 + function: getFullYear + target: { + CALL [4] { + function: timestamp + args: { + CALL [5] { + function: int + args: { + CALL [6] { + function: timestamp + args: { + CONSTANT [7] { value: 1000000000 } + } + } + } + } + } } } - } - CALL [7] { - function: timestamp args: { - IDENT [8] { - name: @index1 - } } } - CALL [9] { + CALL [8] { function: getFullYear target: { - IDENT [10] { - name: @index2 + CALL [9] { + function: timestamp + args: { + CALL [10] { + function: int + args: { + CALL [11] { + function: timestamp + args: { + CONSTANT [12] { value: 200 } + } + } + } + } + } } } args: { } } - CALL [11] { - function: timestamp - args: { - CONSTANT [12] { value: 50 } - } - } CALL [13] { - function: int - args: { - IDENT [14] { - name: @index4 - } - } - } - CALL [15] { function: timestamp args: { - IDENT [16] { - name: @index5 + CALL [14] { + function: int + args: { + CALL [15] { + function: timestamp + args: { + CONSTANT [16] { value: 50 } + } + } + } } } } CALL [17] { function: timestamp args: { - CONSTANT [18] { value: 200 } - } - } - CALL [19] { - function: int - args: { - IDENT [20] { - name: @index7 + CALL [18] { + function: int + args: { + CALL [19] { + function: timestamp + args: { + CONSTANT [20] { value: 75 } + } + } + } } } } CALL [21] { - function: timestamp - args: { - IDENT [22] { - name: @index8 - } - } - } - CALL [23] { - function: getFullYear - target: { - IDENT [24] { - name: @index9 - } - } - args: { - } - } - CALL [25] { - function: timestamp - args: { - CONSTANT [26] { value: 75 } - } - } - CALL [27] { - function: int - args: { - IDENT [28] { - name: @index11 - } - } - } - CALL [29] { - function: timestamp - args: { - IDENT [30] { - name: @index12 - } - } - } - CALL [31] { function: _+_ args: { - CALL [32] { + CALL [22] { function: _+_ args: { - CALL [33] { + CALL [23] { function: _+_ args: { - CALL [34] { + CALL [24] { function: _+_ args: { - CALL [35] { + CALL [25] { function: _+_ args: { - CALL [36] { + CALL [26] { function: _+_ args: { - CALL [37] { + CALL [27] { function: _+_ args: { - IDENT [38] { - name: @index3 + IDENT [28] { + name: @index0 } - CALL [39] { + CALL [29] { function: getFullYear target: { - IDENT [40] { - name: @index13 + IDENT [30] { + name: @index3 } } args: { @@ -447,11 +384,11 @@ CALL [1] { } } } - CALL [41] { + CALL [31] { function: getFullYear target: { - IDENT [42] { - name: @index6 + IDENT [32] { + name: @index2 } } args: { @@ -459,16 +396,16 @@ CALL [1] { } } } - IDENT [43] { - name: @index3 + IDENT [33] { + name: @index0 } } } - CALL [44] { + CALL [34] { function: getSeconds target: { - IDENT [45] { - name: @index6 + IDENT [35] { + name: @index2 } } args: { @@ -476,21 +413,21 @@ CALL [1] { } } } - IDENT [46] { - name: @index10 + IDENT [36] { + name: @index1 } } } - IDENT [47] { - name: @index10 + IDENT [37] { + name: @index1 } } } - CALL [48] { + CALL [38] { function: getMinutes target: { - IDENT [49] { - name: @index13 + IDENT [39] { + name: @index3 } } args: { @@ -498,26 +435,23 @@ CALL [1] { } } } - CALL [50] { + } + } + CALL [40] { + function: _==_ + args: { + CALL [41] { function: _+_ args: { - IDENT [51] { - name: @index14 + IDENT [42] { + name: @index4 } - IDENT [52] { - name: @index3 + IDENT [43] { + name: @index0 } } } - } - } - CALL [53] { - function: _==_ - args: { - IDENT [54] { - name: @index15 - } - CONSTANT [55] { value: 13934 } + CONSTANT [44] { value: 13934 } } } } @@ -528,55 +462,49 @@ Source: {"a": 2}["a"] + {"a": 2}["a"] * {"a": 2}["a"] == 6 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_MAP [3] { - MAP_ENTRY [4] { - key: { - CONSTANT [5] { value: "a" } - } - value: { - CONSTANT [6] { value: 2 } - } - } - } - CALL [7] { + CALL [3] { function: _[_] args: { - IDENT [8] { - name: @index0 + MAP [4] { + MAP_ENTRY [5] { + key: { + CONSTANT [6] { value: "a" } + } + value: { + CONSTANT [7] { value: 2 } + } + } } - CONSTANT [9] { value: "a" } + CONSTANT [8] { value: "a" } } } + } + } + CALL [9] { + function: _==_ + args: { CALL [10] { function: _+_ args: { IDENT [11] { - name: @index1 + name: @index0 } CALL [12] { function: _*_ args: { IDENT [13] { - name: @index1 + name: @index0 } IDENT [14] { - name: @index1 + name: @index0 } } } } } - } - } - CALL [15] { - function: _==_ - args: { - IDENT [16] { - name: @index2 - } - CONSTANT [17] { value: 6 } + CONSTANT [15] { value: 6 } } } } @@ -587,56 +515,53 @@ Source: {'a': {'b': 1}, 'c': {'b': 1}, 'd': {'e': {'b': 1}}, 'e': {'e': {'b': 1} CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_MAP [3] { + MAP [3] { MAP_ENTRY [4] { key: { - CONSTANT [5] { value: "b" } + CONSTANT [5] { value: "e" } } value: { - CONSTANT [6] { value: 1 } + MAP [6] { + MAP_ENTRY [7] { + key: { + CONSTANT [8] { value: "b" } + } + value: { + CONSTANT [9] { value: 1 } + } + } + } } } } - CREATE_MAP [7] { - MAP_ENTRY [8] { + MAP [10] { + MAP_ENTRY [11] { key: { - CONSTANT [9] { value: "e" } + CONSTANT [12] { value: "b" } } value: { - IDENT [10] { - name: @index0 - } + CONSTANT [13] { value: 1 } } } } } } - CREATE_MAP [11] { - MAP_ENTRY [12] { - key: { - CONSTANT [13] { value: "a" } - } - value: { - IDENT [14] { - name: @index0 - } - } - } + MAP [14] { MAP_ENTRY [15] { key: { - CONSTANT [16] { value: "c" } + CONSTANT [16] { value: "a" } } value: { IDENT [17] { - name: @index0 + name: @index1 } } } MAP_ENTRY [18] { key: { - CONSTANT [19] { value: "d" } + CONSTANT [19] { value: "c" } } value: { IDENT [20] { @@ -646,11 +571,21 @@ CALL [1] { } MAP_ENTRY [21] { key: { - CONSTANT [22] { value: "e" } + CONSTANT [22] { value: "d" } } value: { IDENT [23] { - name: @index1 + name: @index0 + } + } + } + MAP_ENTRY [24] { + key: { + CONSTANT [25] { value: "e" } + } + value: { + IDENT [26] { + name: @index0 } } } @@ -663,9 +598,9 @@ Source: [1, [1,2,3,4], 2, [1,2,3,4], 5, [1,2,3,4], 7, [[1,2], [1,2,3,4]], [1,2]] CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { + LIST [3] { elements: { CONSTANT [4] { value: 1 } CONSTANT [5] { value: 2 } @@ -673,43 +608,40 @@ CALL [1] { CONSTANT [7] { value: 4 } } } - CREATE_LIST [8] { + LIST [8] { elements: { CONSTANT [9] { value: 1 } CONSTANT [10] { value: 2 } } } - CREATE_LIST [11] { - elements: { - IDENT [12] { - name: @index1 - } - IDENT [13] { - name: @index0 - } - } - } } } - CREATE_LIST [14] { + LIST [11] { elements: { - CONSTANT [15] { value: 1 } - IDENT [16] { + CONSTANT [12] { value: 1 } + IDENT [13] { name: @index0 } - CONSTANT [17] { value: 2 } - IDENT [18] { + CONSTANT [14] { value: 2 } + IDENT [15] { name: @index0 } - CONSTANT [19] { value: 5 } - IDENT [20] { + CONSTANT [16] { value: 5 } + IDENT [17] { name: @index0 } - CONSTANT [21] { value: 7 } - IDENT [22] { - name: @index2 + CONSTANT [18] { value: 7 } + LIST [19] { + elements: { + IDENT [20] { + name: @index1 + } + IDENT [21] { + name: @index0 + } + } } - IDENT [23] { + IDENT [22] { name: @index1 } } @@ -722,33 +654,30 @@ Source: msg.single_int64 + msg.single_int64 == 6 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { name: msg }.single_int64 } - CALL [5] { + } + } + CALL [5] { + function: _==_ + args: { + CALL [6] { function: _+_ args: { - IDENT [6] { + IDENT [7] { name: @index0 } - IDENT [7] { + IDENT [8] { name: @index0 } } } - } - } - CALL [8] { - function: _==_ - args: { - IDENT [9] { - name: @index1 - } - CONSTANT [10] { value: 6 } + CONSTANT [9] { value: 6 } } } } @@ -759,61 +688,67 @@ Source: msg.oneof_type.payload.single_int64 + msg.oneof_type.payload.single_int3 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.single_int64 } SELECT [7] { - IDENT [8] { - name: @index1 - }.single_int64 + SELECT [8] { + IDENT [9] { + name: msg + }.oneof_type + }.payload } - CALL [9] { + } + } + CALL [10] { + function: _==_ + args: { + CALL [11] { function: _+_ args: { - CALL [10] { + CALL [12] { function: _+_ args: { - CALL [11] { + CALL [13] { function: _+_ args: { - CALL [12] { + CALL [14] { function: _+_ args: { - IDENT [13] { - name: @index2 + IDENT [15] { + name: @index0 } - SELECT [14] { - IDENT [15] { + SELECT [16] { + IDENT [17] { name: @index1 }.single_int32 } } } - IDENT [16] { - name: @index2 + IDENT [18] { + name: @index0 } } } - SELECT [17] { - IDENT [18] { + SELECT [19] { + IDENT [20] { name: msg }.single_int64 } } } - SELECT [19] { - SELECT [20] { - SELECT [21] { - IDENT [22] { + SELECT [21] { + SELECT [22] { + SELECT [23] { + IDENT [24] { name: @index1 }.oneof_type }.payload @@ -821,14 +756,6 @@ CALL [1] { } } } - } - } - CALL [23] { - function: _==_ - args: { - IDENT [24] { - name: @index3 - } CONSTANT [25] { value: 31 } } } @@ -840,54 +767,36 @@ Source: true || msg.oneof_type.payload.oneof_type.payload.oneof_type.payload.one CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 - }.oneof_type - } - SELECT [9] { - IDENT [10] { - name: @index2 - }.payload - } - SELECT [11] { - IDENT [12] { - name: @index3 - }.oneof_type - } - SELECT [13] { - SELECT [14] { - SELECT [15] { - SELECT [16] { - IDENT [17] { - name: @index4 - }.child - }.child + SELECT [4] { + SELECT [5] { + SELECT [6] { + SELECT [7] { + IDENT [8] { + name: msg + }.oneof_type + }.payload + }.oneof_type }.payload - }.single_bool + }.oneof_type } - CALL [18] { + } + } + CALL [9] { + function: _||_ + args: { + CALL [10] { function: _||_ args: { - CONSTANT [19] { value: true } - SELECT [20] { - SELECT [21] { - SELECT [22] { - SELECT [23] { - IDENT [24] { - name: @index4 + CONSTANT [11] { value: true } + SELECT [12] { + SELECT [13] { + SELECT [14] { + SELECT [15] { + IDENT [16] { + name: @index0 }.payload }.oneof_type }.payload @@ -895,16 +804,16 @@ CALL [1] { } } } - } - } - CALL [25] { - function: _||_ - args: { - IDENT [26] { - name: @index6 - } - IDENT [27] { - name: @index5 + SELECT [17] { + SELECT [18] { + SELECT [19] { + SELECT [20] { + IDENT [21] { + name: @index0 + }.child + }.child + }.payload + }.single_bool } } } @@ -916,60 +825,48 @@ Source: msg.oneof_type.payload.map_int32_int64[1] + msg.oneof_type.payload.map_i CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 - }.map_int32_int64 - } - CALL [9] { + CALL [3] { function: _[_] args: { - IDENT [10] { - name: @index2 + SELECT [4] { + SELECT [5] { + SELECT [6] { + IDENT [7] { + name: msg + }.oneof_type + }.payload + }.map_int32_int64 } - CONSTANT [11] { value: 1 } + CONSTANT [8] { value: 1 } } } - CALL [12] { + } + } + CALL [9] { + function: _==_ + args: { + CALL [10] { function: _+_ args: { - CALL [13] { + CALL [11] { function: _+_ args: { - IDENT [14] { - name: @index3 + IDENT [12] { + name: @index0 } - IDENT [15] { - name: @index3 + IDENT [13] { + name: @index0 } } } - IDENT [16] { - name: @index3 + IDENT [14] { + name: @index0 } } } - } - } - CALL [17] { - function: _==_ - args: { - IDENT [18] { - name: @index4 - } - CONSTANT [19] { value: 15 } + CONSTANT [15] { value: 15 } } } } @@ -980,69 +877,60 @@ Source: msg.oneof_type.payload.map_int32_int64[0] + msg.oneof_type.payload.map_i CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload }.map_int32_int64 } - CALL [9] { + } + } + CALL [7] { + function: _==_ + args: { + CALL [8] { function: _+_ args: { - CALL [10] { + CALL [9] { function: _+_ args: { - CALL [11] { + CALL [10] { function: _[_] args: { - IDENT [12] { - name: @index2 + IDENT [11] { + name: @index0 } - CONSTANT [13] { value: 0 } + CONSTANT [12] { value: 0 } } } - CALL [14] { + CALL [13] { function: _[_] args: { - IDENT [15] { - name: @index2 + IDENT [14] { + name: @index0 } - CONSTANT [16] { value: 1 } + CONSTANT [15] { value: 1 } } } } } - CALL [17] { + CALL [16] { function: _[_] args: { - IDENT [18] { - name: @index2 + IDENT [17] { + name: @index0 } - CONSTANT [19] { value: 2 } + CONSTANT [18] { value: 2 } } } } } - } - } - CALL [20] { - function: _==_ - args: { - IDENT [21] { - name: @index3 - } - CONSTANT [22] { value: 8 } + CONSTANT [19] { value: 8 } } } } @@ -1053,7 +941,7 @@ Source: msg.oneof_type.payload.oneof_type.payload.oneof_type.payload.oneof_type. CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { SELECT [4] { @@ -1089,40 +977,37 @@ Source: (msg.single_int64 > 0 ? msg.single_int64 : 0) == 3 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { name: msg }.single_int64 } - CALL [5] { + } + } + CALL [5] { + function: _==_ + args: { + CALL [6] { function: _?_:_ args: { - CALL [6] { + CALL [7] { function: _>_ args: { - IDENT [7] { + IDENT [8] { name: @index0 } - CONSTANT [8] { value: 0 } + CONSTANT [9] { value: 0 } } } - IDENT [9] { + IDENT [10] { name: @index0 } - CONSTANT [10] { value: 0 } + CONSTANT [11] { value: 0 } } } - } - } - CALL [11] { - function: _==_ - args: { - IDENT [12] { - name: @index1 - } - CONSTANT [13] { value: 3 } + CONSTANT [12] { value: 3 } } } } @@ -1133,54 +1018,51 @@ Source: false ? false : (msg.single_int64) + ((msg.single_int64 + 1) * 2) == 11 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { name: msg }.single_int64 } - CALL [5] { + } + } + CALL [5] { + function: _?_:_ + args: { + CONSTANT [6] { value: false } + CONSTANT [7] { value: false } + CALL [8] { function: _==_ args: { - CALL [6] { + CALL [9] { function: _+_ args: { - IDENT [7] { + IDENT [10] { name: @index0 } - CALL [8] { + CALL [11] { function: _*_ args: { - CALL [9] { + CALL [12] { function: _+_ args: { - IDENT [10] { + IDENT [13] { name: @index0 } - CONSTANT [11] { value: 1 } + CONSTANT [14] { value: 1 } } } - CONSTANT [12] { value: 2 } + CONSTANT [15] { value: 2 } } } } } - CONSTANT [13] { value: 11 } + CONSTANT [16] { value: 11 } } } } } - CALL [14] { - function: _?_:_ - args: { - CONSTANT [15] { value: false } - CONSTANT [16] { value: false } - IDENT [17] { - name: @index1 - } - } - } } } Test case: NESTED_TERNARY @@ -1189,7 +1071,7 @@ Source: (msg.single_int64 > 0 ? (msg.single_int32 > 0 ? msg.single_int64 + msg.s CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { @@ -1201,56 +1083,53 @@ CALL [1] { name: msg }.single_int32 } - CALL [7] { + } + } + CALL [7] { + function: _==_ + args: { + CALL [8] { function: _?_:_ args: { - CALL [8] { + CALL [9] { function: _>_ args: { - IDENT [9] { + IDENT [10] { name: @index0 } - CONSTANT [10] { value: 0 } + CONSTANT [11] { value: 0 } } } - CALL [11] { + CALL [12] { function: _?_:_ args: { - CALL [12] { + CALL [13] { function: _>_ args: { - IDENT [13] { + IDENT [14] { name: @index1 } - CONSTANT [14] { value: 0 } + CONSTANT [15] { value: 0 } } } - CALL [15] { + CALL [16] { function: _+_ args: { - IDENT [16] { + IDENT [17] { name: @index0 } - IDENT [17] { + IDENT [18] { name: @index1 } } } - CONSTANT [18] { value: 0 } + CONSTANT [19] { value: 0 } } } - CONSTANT [19] { value: 0 } + CONSTANT [20] { value: 0 } } } - } - } - CALL [20] { - function: _==_ - args: { - IDENT [21] { - name: @index2 - } - CONSTANT [22] { value: 8 } + CONSTANT [21] { value: 8 } } } } @@ -1261,53 +1140,30 @@ Source: size([[1].exists(i, i > 0)]) + size([[1].exists(j, j > 0)]) + size([[2]. CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - } - } - CALL [5] { - function: _>_ - args: { - IDENT [6] { - name: @c0:0 - } - CONSTANT [7] { value: 0 } - } - } - CALL [8] { - function: _||_ - args: { - IDENT [9] { - name: @x0:0 - } - IDENT [10] { - name: @index1 - } - } - } - COMPREHENSION [11] { - iter_var: @c0:0 + COMPREHENSION [3] { + iter_var: @it:0:0 iter_range: { - IDENT [12] { - name: @index0 + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + } } } - accu_var: @x0:0 + accu_var: @ac:0:0 accu_init: { - CONSTANT [13] { value: false } + CONSTANT [6] { value: false } } loop_condition: { - CALL [14] { + CALL [7] { function: @not_strictly_false args: { - CALL [15] { + CALL [8] { function: !_ args: { - IDENT [16] { - name: @x0:0 + IDENT [9] { + name: @ac:0:0 } } } @@ -1315,76 +1171,52 @@ CALL [1] { } } loop_step: { - IDENT [17] { - name: @index2 + CALL [10] { + function: _||_ + args: { + IDENT [11] { + name: @ac:0:0 + } + CALL [12] { + function: _>_ + args: { + IDENT [13] { + name: @it:0:0 + } + CONSTANT [14] { value: 0 } + } + } + } } } result: { - IDENT [18] { - name: @x0:0 - } - } - } - CREATE_LIST [19] { - elements: { - IDENT [20] { - name: @index3 - } - } - } - CALL [21] { - function: size - args: { - IDENT [22] { - name: @index4 - } - } - } - CREATE_LIST [23] { - elements: { - CONSTANT [24] { value: 2 } - } - } - CALL [25] { - function: _>_ - args: { - IDENT [26] { - name: @c0:0 - } - CONSTANT [27] { value: 1 } - } - } - CALL [28] { - function: _||_ - args: { - IDENT [29] { - name: @x0:0 - } - IDENT [30] { - name: @index7 + IDENT [15] { + name: @ac:0:0 } } } - COMPREHENSION [31] { - iter_var: @c0:0 + COMPREHENSION [16] { + iter_var: @it:0:0 iter_range: { - IDENT [32] { - name: @index6 + LIST [17] { + elements: { + CONSTANT [18] { value: 2 } + } } } - accu_var: @x0:0 + accu_var: @ac:0:0 accu_init: { - CONSTANT [33] { value: false } + CONSTANT [19] { value: false } } loop_condition: { - CALL [34] { + CALL [20] { function: @not_strictly_false args: { - CALL [35] { + CALL [21] { function: !_ args: { - IDENT [36] { - name: @x0:0 + IDENT [22] { + name: @ac:0:0 } } } @@ -1392,67 +1224,87 @@ CALL [1] { } } loop_step: { - IDENT [37] { - name: @index8 + CALL [23] { + function: _||_ + args: { + IDENT [24] { + name: @ac:0:0 + } + CALL [25] { + function: _>_ + args: { + IDENT [26] { + name: @it:0:0 + } + CONSTANT [27] { value: 1 } + } + } + } } } result: { - IDENT [38] { - name: @x0:0 + IDENT [28] { + name: @ac:0:0 } } } - CREATE_LIST [39] { - elements: { - IDENT [40] { - name: @index9 + CALL [29] { + function: size + args: { + LIST [30] { + elements: { + IDENT [31] { + name: @index0 + } + } } } } - CALL [41] { + CALL [32] { function: size args: { - IDENT [42] { - name: @index10 + LIST [33] { + elements: { + IDENT [34] { + name: @index1 + } + } } } } - CALL [43] { + } + } + CALL [35] { + function: _==_ + args: { + CALL [36] { function: _+_ args: { - CALL [44] { + CALL [37] { function: _+_ args: { - CALL [45] { + CALL [38] { function: _+_ args: { - IDENT [46] { - name: @index5 + IDENT [39] { + name: @index2 } - IDENT [47] { - name: @index5 + IDENT [40] { + name: @index2 } } } - IDENT [48] { - name: @index11 + IDENT [41] { + name: @index3 } } } - IDENT [49] { - name: @index11 + IDENT [42] { + name: @index3 } } } - } - } - CALL [50] { - function: _==_ - args: { - IDENT [51] { - name: @index12 - } - CONSTANT [52] { value: 4 } + CONSTANT [43] { value: 4 } } } } @@ -1463,53 +1315,30 @@ Source: [[1].exists(i, i > 0)] + [[1].exists(j, j > 0)] + [['a'].exists(k, k == CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - } - } - CALL [5] { - function: _>_ - args: { - IDENT [6] { - name: @c0:0 + COMPREHENSION [3] { + iter_var: @it:0:0 + iter_range: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + } } - CONSTANT [7] { value: 0 } } - } - CALL [8] { - function: _||_ - args: { - IDENT [9] { - name: @x0:0 - } - IDENT [10] { - name: @index1 - } - } - } - COMPREHENSION [11] { - iter_var: @c0:0 - iter_range: { - IDENT [12] { - name: @index0 - } - } - accu_var: @x0:0 - accu_init: { - CONSTANT [13] { value: false } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [6] { value: false } } loop_condition: { - CALL [14] { + CALL [7] { function: @not_strictly_false args: { - CALL [15] { + CALL [8] { function: !_ args: { - IDENT [16] { - name: @x0:0 + IDENT [9] { + name: @ac:0:0 } } } @@ -1517,68 +1346,52 @@ CALL [1] { } } loop_step: { - IDENT [17] { - name: @index2 + CALL [10] { + function: _||_ + args: { + IDENT [11] { + name: @ac:0:0 + } + CALL [12] { + function: _>_ + args: { + IDENT [13] { + name: @it:0:0 + } + CONSTANT [14] { value: 0 } + } + } + } } } result: { - IDENT [18] { - name: @x0:0 - } - } - } - CREATE_LIST [19] { - elements: { - IDENT [20] { - name: @index3 - } - } - } - CREATE_LIST [21] { - elements: { - CONSTANT [22] { value: "a" } - } - } - CALL [23] { - function: _==_ - args: { - IDENT [24] { - name: @c0:1 - } - CONSTANT [25] { value: "a" } - } - } - CALL [26] { - function: _||_ - args: { - IDENT [27] { - name: @x0:1 - } - IDENT [28] { - name: @index6 + IDENT [15] { + name: @ac:0:0 } } } - COMPREHENSION [29] { - iter_var: @c0:1 + COMPREHENSION [16] { + iter_var: @it:0:1 iter_range: { - IDENT [30] { - name: @index5 + LIST [17] { + elements: { + CONSTANT [18] { value: "a" } + } } } - accu_var: @x0:1 + accu_var: @ac:0:1 accu_init: { - CONSTANT [31] { value: false } + CONSTANT [19] { value: false } } loop_condition: { - CALL [32] { + CALL [20] { function: @not_strictly_false args: { - CALL [33] { + CALL [21] { function: !_ args: { - IDENT [34] { - name: @x0:1 + IDENT [22] { + name: @ac:0:1 } } } @@ -1586,317 +1399,1454 @@ CALL [1] { } } loop_step: { - IDENT [35] { - name: @index7 + CALL [23] { + function: _||_ + args: { + IDENT [24] { + name: @ac:0:1 + } + CALL [25] { + function: _==_ + args: { + IDENT [26] { + name: @it:0:1 + } + CONSTANT [27] { value: "a" } + } + } + } } } result: { - IDENT [36] { - name: @x0:1 + IDENT [28] { + name: @ac:0:1 } } } - CREATE_LIST [37] { + LIST [29] { elements: { - IDENT [38] { - name: @index8 + IDENT [30] { + name: @index0 } } } - CREATE_LIST [39] { + LIST [31] { elements: { - CONSTANT [40] { value: true } - CONSTANT [41] { value: true } - CONSTANT [42] { value: true } - CONSTANT [43] { value: true } + IDENT [32] { + name: @index1 + } } } - CALL [44] { + } + } + CALL [33] { + function: _==_ + args: { + CALL [34] { function: _+_ args: { - CALL [45] { + CALL [35] { function: _+_ args: { - CALL [46] { + CALL [36] { function: _+_ args: { - IDENT [47] { - name: @index4 + IDENT [37] { + name: @index2 } - IDENT [48] { - name: @index4 + IDENT [38] { + name: @index2 } } } - IDENT [49] { - name: @index9 + IDENT [39] { + name: @index3 } } } - IDENT [50] { - name: @index9 + IDENT [40] { + name: @index3 } } } - } - } - CALL [51] { - function: _==_ - args: { - IDENT [52] { - name: @index11 - } - IDENT [53] { - name: @index10 + LIST [41] { + elements: { + CONSTANT [42] { value: true } + CONSTANT [43] { value: true } + CONSTANT [44] { value: true } + CONSTANT [45] { value: true } + } } } } } } -Test case: NESTED_MACROS -Source: [1,2,3].map(i, [1, 2, 3].map(i, i + 1)) == [[2, 3, 4], [2, 3, 4], [2, 3, 4]] +Test case: MULTIPLE_MACROS_3 +Source: [1].exists(i, i > 0) && [1].exists(j, j > 0) && [1].exists(k, k > 1) && [2].exists(l, l > 1) =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - CONSTANT [5] { value: 2 } - CONSTANT [6] { value: 3 } + COMPREHENSION [3] { + iter_var: @it:0:0 + iter_range: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + } + } } - } - CREATE_LIST [7] { - elements: { - CONSTANT [8] { value: 2 } - CONSTANT [9] { value: 3 } - CONSTANT [10] { value: 4 } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [6] { value: false } } - } - CREATE_LIST [11] { - elements: { - IDENT [12] { - name: @index1 + loop_condition: { + CALL [7] { + function: @not_strictly_false + args: { + CALL [8] { + function: !_ + args: { + IDENT [9] { + name: @ac:0:0 + } + } + } + } } - IDENT [13] { - name: @index1 + } + loop_step: { + CALL [10] { + function: _||_ + args: { + IDENT [11] { + name: @ac:0:0 + } + CALL [12] { + function: _>_ + args: { + IDENT [13] { + name: @it:0:0 + } + CONSTANT [14] { value: 0 } + } + } + } } - IDENT [14] { - name: @index1 + } + result: { + IDENT [15] { + name: @ac:0:0 } } } - COMPREHENSION [15] { - iter_var: @c0:0 - iter_range: { - IDENT [16] { + } + } + CALL [16] { + function: _&&_ + args: { + CALL [17] { + function: _&&_ + args: { + IDENT [18] { name: @index0 } - } - accu_var: @x0:0 - accu_init: { - CREATE_LIST [17] { - elements: { - } + IDENT [19] { + name: @index0 } } - loop_condition: { - CONSTANT [18] { value: true } - } - loop_step: { - CALL [19] { - function: _+_ - args: { - IDENT [20] { - name: @x0:0 - } - CREATE_LIST [21] { + } + CALL [20] { + function: _&&_ + args: { + COMPREHENSION [21] { + iter_var: @it:0:0 + iter_range: { + LIST [22] { elements: { - COMPREHENSION [22] { - iter_var: @c1:0 - iter_range: { - IDENT [23] { - name: @index0 + CONSTANT [23] { value: 1 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [24] { value: false } + } + loop_condition: { + CALL [25] { + function: @not_strictly_false + args: { + CALL [26] { + function: !_ + args: { + IDENT [27] { + name: @ac:0:0 } } - accu_var: @x1:0 - accu_init: { - CREATE_LIST [24] { - elements: { - } + } + } + } + } + loop_step: { + CALL [28] { + function: _||_ + args: { + IDENT [29] { + name: @ac:0:0 + } + CALL [30] { + function: _>_ + args: { + IDENT [31] { + name: @it:0:0 } + CONSTANT [32] { value: 1 } } - loop_condition: { - CONSTANT [25] { value: true } - } - loop_step: { - CALL [26] { - function: _+_ - args: { - IDENT [27] { - name: @x1:0 - } - CREATE_LIST [28] { - elements: { - CALL [29] { - function: _+_ - args: { - IDENT [30] { - name: @c1:0 - } - CONSTANT [31] { value: 1 } - } - } - } - } - } + } + } + } + } + result: { + IDENT [33] { + name: @ac:0:0 + } + } + } + COMPREHENSION [34] { + iter_var: @it:0:0 + iter_range: { + LIST [35] { + elements: { + CONSTANT [36] { value: 2 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [37] { value: false } + } + loop_condition: { + CALL [38] { + function: @not_strictly_false + args: { + CALL [39] { + function: !_ + args: { + IDENT [40] { + name: @ac:0:0 } } - result: { - IDENT [32] { - name: @x1:0 + } + } + } + } + loop_step: { + CALL [41] { + function: _||_ + args: { + IDENT [42] { + name: @ac:0:0 + } + CALL [43] { + function: _>_ + args: { + IDENT [44] { + name: @it:0:0 } + CONSTANT [45] { value: 1 } } } } } } - } - } - result: { - IDENT [33] { - name: @x0:0 + result: { + IDENT [46] { + name: @ac:0:0 + } + } } } } } } - CALL [34] { - function: _==_ - args: { - IDENT [35] { - name: @index3 - } - IDENT [36] { - name: @index2 - } - } - } } } -Test case: NESTED_MACROS_2 -Source: [1, 2].map(y, [1, 2, 3].filter(x, x == y)) == [[1], [2]] +Test case: MULTIPLE_MACROS_COMP_V2_1 +Source: size([[1].exists(i,x,i > 0 && x >= 0)]) + size([[1].exists(j, y, j > 0 && y >= 0)]) + size([[2].exists(k,z,k > 1 && z >= 0)]) + size([[2].exists(l, w, l > 1 && w >= 0)]) == 4 =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CREATE_LIST [4] { + COMPREHENSION [3] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + LIST [4] { elements: { CONSTANT [5] { value: 1 } } } - CREATE_LIST [6] { - elements: { - CONSTANT [7] { value: 2 } - } - } } - } - COMPREHENSION [8] { - iter_var: @c0:0 + accu_var: @ac:0:0 + accu_init: { + CONSTANT [6] { value: false } + } + loop_condition: { + CALL [7] { + function: @not_strictly_false + args: { + CALL [8] { + function: !_ + args: { + IDENT [9] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [10] { + function: _||_ + args: { + IDENT [11] { + name: @ac:0:0 + } + CALL [12] { + function: _&&_ + args: { + CALL [13] { + function: _>_ + args: { + IDENT [14] { + name: @it:0:0 + } + CONSTANT [15] { value: 0 } + } + } + CALL [16] { + function: _>=_ + args: { + IDENT [17] { + name: @it2:0:0 + } + CONSTANT [18] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [19] { + name: @ac:0:0 + } + } + } + COMPREHENSION [20] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + LIST [21] { + elements: { + CONSTANT [22] { value: 2 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [23] { value: false } + } + loop_condition: { + CALL [24] { + function: @not_strictly_false + args: { + CALL [25] { + function: !_ + args: { + IDENT [26] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [27] { + function: _||_ + args: { + IDENT [28] { + name: @ac:0:0 + } + CALL [29] { + function: _&&_ + args: { + CALL [30] { + function: _>_ + args: { + IDENT [31] { + name: @it:0:0 + } + CONSTANT [32] { value: 1 } + } + } + CALL [33] { + function: _>=_ + args: { + IDENT [34] { + name: @it2:0:0 + } + CONSTANT [35] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [36] { + name: @ac:0:0 + } + } + } + CALL [37] { + function: size + args: { + LIST [38] { + elements: { + IDENT [39] { + name: @index0 + } + } + } + } + } + CALL [40] { + function: size + args: { + LIST [41] { + elements: { + IDENT [42] { + name: @index1 + } + } + } + } + } + } + } + CALL [43] { + function: _==_ + args: { + CALL [44] { + function: _+_ + args: { + CALL [45] { + function: _+_ + args: { + CALL [46] { + function: _+_ + args: { + IDENT [47] { + name: @index2 + } + IDENT [48] { + name: @index2 + } + } + } + IDENT [49] { + name: @index3 + } + } + } + IDENT [50] { + name: @index3 + } + } + } + CONSTANT [51] { value: 4 } + } + } + } +} +Test case: MULTIPLE_MACROS_COMP_V2_2 +Source: [1].exists(i, x, i > 0 && x > 0) && [1].exists(j, y, j > 0 && y > 0) && [1].exists(k, z, k > 1 && z > 0) && [2].exists(l, w, l > 1 && w > 0) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + COMPREHENSION [3] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [6] { value: false } + } + loop_condition: { + CALL [7] { + function: @not_strictly_false + args: { + CALL [8] { + function: !_ + args: { + IDENT [9] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [10] { + function: _||_ + args: { + IDENT [11] { + name: @ac:0:0 + } + CALL [12] { + function: _&&_ + args: { + CALL [13] { + function: _>_ + args: { + IDENT [14] { + name: @it:0:0 + } + CONSTANT [15] { value: 0 } + } + } + CALL [16] { + function: _>_ + args: { + IDENT [17] { + name: @it2:0:0 + } + CONSTANT [18] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [19] { + name: @ac:0:0 + } + } + } + } + } + CALL [20] { + function: _&&_ + args: { + CALL [21] { + function: _&&_ + args: { + IDENT [22] { + name: @index0 + } + IDENT [23] { + name: @index0 + } + } + } + CALL [24] { + function: _&&_ + args: { + COMPREHENSION [25] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + LIST [26] { + elements: { + CONSTANT [27] { value: 1 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [28] { value: false } + } + loop_condition: { + CALL [29] { + function: @not_strictly_false + args: { + CALL [30] { + function: !_ + args: { + IDENT [31] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [32] { + function: _||_ + args: { + IDENT [33] { + name: @ac:0:0 + } + CALL [34] { + function: _&&_ + args: { + CALL [35] { + function: _>_ + args: { + IDENT [36] { + name: @it:0:0 + } + CONSTANT [37] { value: 1 } + } + } + CALL [38] { + function: _>_ + args: { + IDENT [39] { + name: @it2:0:0 + } + CONSTANT [40] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [41] { + name: @ac:0:0 + } + } + } + COMPREHENSION [42] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + LIST [43] { + elements: { + CONSTANT [44] { value: 2 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [45] { value: false } + } + loop_condition: { + CALL [46] { + function: @not_strictly_false + args: { + CALL [47] { + function: !_ + args: { + IDENT [48] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [49] { + function: _||_ + args: { + IDENT [50] { + name: @ac:0:0 + } + CALL [51] { + function: _&&_ + args: { + CALL [52] { + function: _>_ + args: { + IDENT [53] { + name: @it:0:0 + } + CONSTANT [54] { value: 1 } + } + } + CALL [55] { + function: _>_ + args: { + IDENT [56] { + name: @it2:0:0 + } + CONSTANT [57] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [58] { + name: @ac:0:0 + } + } + } + } + } + } + } + } +} +Test case: NESTED_MACROS +Source: [1,2,3].map(i, [1, 2, 3].map(i, i + 1)) == [[2, 3, 4], [2, 3, 4], [2, 3, 4]] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + CONSTANT [5] { value: 2 } + CONSTANT [6] { value: 3 } + } + } + LIST [7] { + elements: { + CONSTANT [8] { value: 2 } + CONSTANT [9] { value: 3 } + CONSTANT [10] { value: 4 } + } + } + } + } + CALL [11] { + function: _==_ + args: { + COMPREHENSION [12] { + iter_var: @it:1:0 + iter_range: { + IDENT [13] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [14] { + elements: { + } + } + } + loop_condition: { + CONSTANT [15] { value: true } + } + loop_step: { + CALL [16] { + function: _+_ + args: { + IDENT [17] { + name: @ac:1:0 + } + LIST [18] { + elements: { + COMPREHENSION [19] { + iter_var: @it:0:0 + iter_range: { + IDENT [20] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [21] { + elements: { + } + } + } + loop_condition: { + CONSTANT [22] { value: true } + } + loop_step: { + CALL [23] { + function: _+_ + args: { + IDENT [24] { + name: @ac:0:0 + } + LIST [25] { + elements: { + CALL [26] { + function: _+_ + args: { + IDENT [27] { + name: @it:0:0 + } + CONSTANT [28] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [29] { + name: @ac:0:0 + } + } + } + } + } + } + } + } + result: { + IDENT [30] { + name: @ac:1:0 + } + } + } + LIST [31] { + elements: { + IDENT [32] { + name: @index1 + } + IDENT [33] { + name: @index1 + } + IDENT [34] { + name: @index1 + } + } + } + } + } + } +} +Test case: NESTED_MACROS_2 +Source: [1, 2].map(y, [1, 2, 3].filter(x, x == y)) == [[1], [2]] +=====> +CALL [31] { + function: _==_ + args: { + COMPREHENSION [30] { + iter_var: y + iter_range: { + LIST [1] { + elements: { + CONSTANT [2] { value: 1 } + CONSTANT [3] { value: 2 } + } + } + } + accu_var: @result + accu_init: { + LIST [24] { + elements: { + } + } + } + loop_condition: { + CONSTANT [25] { value: true } + } + loop_step: { + CALL [28] { + function: _+_ + args: { + IDENT [26] { + name: @result + } + LIST [27] { + elements: { + COMPREHENSION [23] { + iter_var: x + iter_range: { + LIST [6] { + elements: { + CONSTANT [7] { value: 1 } + CONSTANT [8] { value: 2 } + CONSTANT [9] { value: 3 } + } + } + } + accu_var: @result + accu_init: { + LIST [15] { + elements: { + } + } + } + loop_condition: { + CONSTANT [16] { value: true } + } + loop_step: { + CALL [21] { + function: _?_:_ + args: { + CALL [13] { + function: _==_ + args: { + IDENT [12] { + name: x + } + IDENT [14] { + name: y + } + } + } + CALL [19] { + function: _+_ + args: { + IDENT [17] { + name: @result + } + LIST [18] { + elements: { + IDENT [11] { + name: x + } + } + } + } + } + IDENT [20] { + name: @result + } + } + } + } + result: { + IDENT [22] { + name: @result + } + } + } + } + } + } + } + } + result: { + IDENT [29] { + name: @result + } + } + } + LIST [32] { + elements: { + LIST [33] { + elements: { + CONSTANT [34] { value: 1 } + } + } + LIST [35] { + elements: { + CONSTANT [36] { value: 2 } + } + } + } + } + } +} +Test case: NESTED_MACROS_3 +Source: [1,2,3].map(i, i * 2) + [1,2,3].map(i, i * 2).map(i, i * 2) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + COMPREHENSION [3] { + iter_var: @it:0:0 + iter_range: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + CONSTANT [6] { value: 2 } + CONSTANT [7] { value: 3 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [8] { + elements: { + } + } + } + loop_condition: { + CONSTANT [9] { value: true } + } + loop_step: { + CALL [10] { + function: _+_ + args: { + IDENT [11] { + name: @ac:0:0 + } + LIST [12] { + elements: { + CALL [13] { + function: _*_ + args: { + IDENT [14] { + name: @it:0:0 + } + CONSTANT [15] { value: 2 } + } + } + } + } + } + } + } + result: { + IDENT [16] { + name: @ac:0:0 + } + } + } + } + } + CALL [17] { + function: _+_ + args: { + IDENT [18] { + name: @index0 + } + COMPREHENSION [19] { + iter_var: @it:1:0 + iter_range: { + IDENT [20] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [21] { + elements: { + } + } + } + loop_condition: { + CONSTANT [22] { value: true } + } + loop_step: { + CALL [23] { + function: _+_ + args: { + IDENT [24] { + name: @ac:1:0 + } + LIST [25] { + elements: { + CALL [26] { + function: _*_ + args: { + IDENT [27] { + name: @it:1:0 + } + CONSTANT [28] { value: 2 } + } + } + } + } + } + } + } + result: { + IDENT [29] { + name: @ac:1:0 + } + } + } + } + } + } +} +Test case: NESTED_MACROS_COMP_V2_1 +Source: [1,2,3].transformList(i, v, [1, 2, 3].transformList(i, v, i + v + 1)) == [[2, 4, 6], [2, 4, 6], [2, 4, 6]] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + CONSTANT [5] { value: 2 } + CONSTANT [6] { value: 3 } + } + } + LIST [7] { + elements: { + CONSTANT [8] { value: 2 } + CONSTANT [9] { value: 4 } + CONSTANT [10] { value: 6 } + } + } + } + } + CALL [11] { + function: _==_ + args: { + COMPREHENSION [12] { + iter_var: @it:1:0 + iter_var2: @it2:1:0 + iter_range: { + IDENT [13] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [14] { + elements: { + } + } + } + loop_condition: { + CONSTANT [15] { value: true } + } + loop_step: { + CALL [16] { + function: _+_ + args: { + IDENT [17] { + name: @ac:1:0 + } + LIST [18] { + elements: { + COMPREHENSION [19] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + IDENT [20] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [21] { + elements: { + } + } + } + loop_condition: { + CONSTANT [22] { value: true } + } + loop_step: { + CALL [23] { + function: _+_ + args: { + IDENT [24] { + name: @ac:0:0 + } + LIST [25] { + elements: { + CALL [26] { + function: _+_ + args: { + CALL [27] { + function: _+_ + args: { + IDENT [28] { + name: @it:0:0 + } + IDENT [29] { + name: @it2:0:0 + } + } + } + CONSTANT [30] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [31] { + name: @ac:0:0 + } + } + } + } + } + } + } + } + result: { + IDENT [32] { + name: @ac:1:0 + } + } + } + LIST [33] { + elements: { + IDENT [34] { + name: @index1 + } + IDENT [35] { + name: @index1 + } + IDENT [36] { + name: @index1 + } + } + } + } + } + } +} +Test case: NESTED_MACROS_COMP_V2_2 +Source: [1, 2].transformList(i, y, [1, 2, 3].filter(x, x == y && i < y)) == [[1], [2]] +=====> +CALL [36] { + function: _==_ + args: { + COMPREHENSION [35] { + iter_var: i + iter_var2: y + iter_range: { + LIST [1] { + elements: { + CONSTANT [2] { value: 1 } + CONSTANT [3] { value: 2 } + } + } + } + accu_var: @result + accu_init: { + LIST [29] { + elements: { + } + } + } + loop_condition: { + CONSTANT [30] { value: true } + } + loop_step: { + CALL [33] { + function: _+_ + args: { + IDENT [31] { + name: @result + } + LIST [32] { + elements: { + COMPREHENSION [28] { + iter_var: x + iter_range: { + LIST [7] { + elements: { + CONSTANT [8] { value: 1 } + CONSTANT [9] { value: 2 } + CONSTANT [10] { value: 3 } + } + } + } + accu_var: @result + accu_init: { + LIST [20] { + elements: { + } + } + } + loop_condition: { + CONSTANT [21] { value: true } + } + loop_step: { + CALL [26] { + function: _?_:_ + args: { + CALL [16] { + function: _&&_ + args: { + CALL [14] { + function: _==_ + args: { + IDENT [13] { + name: x + } + IDENT [15] { + name: y + } + } + } + CALL [18] { + function: _<_ + args: { + IDENT [17] { + name: i + } + IDENT [19] { + name: y + } + } + } + } + } + CALL [24] { + function: _+_ + args: { + IDENT [22] { + name: @result + } + LIST [23] { + elements: { + IDENT [12] { + name: x + } + } + } + } + } + IDENT [25] { + name: @result + } + } + } + } + result: { + IDENT [27] { + name: @result + } + } + } + } + } + } + } + } + result: { + IDENT [34] { + name: @result + } + } + } + LIST [37] { + elements: { + LIST [38] { + elements: { + CONSTANT [39] { value: 1 } + } + } + LIST [40] { + elements: { + CONSTANT [41] { value: 2 } + } + } + } + } + } +} +Test case: ADJACENT_NESTED_MACROS +Source: [1,2,3].map(i, [1, 2, 3].map(i, i + 1)) == [1,2,3].map(j, [1, 2, 3].map(j, j + 1)) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + COMPREHENSION [3] { + iter_var: @it:1:0 iter_range: { - CREATE_LIST [9] { + LIST [4] { elements: { - CONSTANT [10] { value: 1 } - CONSTANT [11] { value: 2 } + CONSTANT [5] { value: 1 } + CONSTANT [6] { value: 2 } + CONSTANT [7] { value: 3 } } } } - accu_var: @x0:0 + accu_var: @ac:1:0 accu_init: { - CREATE_LIST [12] { + LIST [8] { elements: { } } } loop_condition: { - CONSTANT [13] { value: true } + CONSTANT [9] { value: true } } loop_step: { - CALL [14] { + CALL [10] { function: _+_ args: { - IDENT [15] { - name: @x0:0 + IDENT [11] { + name: @ac:1:0 } - CREATE_LIST [16] { + LIST [12] { elements: { - COMPREHENSION [17] { - iter_var: @c1:0 + COMPREHENSION [13] { + iter_var: @it:0:0 iter_range: { - CREATE_LIST [18] { + LIST [14] { elements: { - CONSTANT [19] { value: 1 } - CONSTANT [20] { value: 2 } - CONSTANT [21] { value: 3 } + CONSTANT [15] { value: 1 } + CONSTANT [16] { value: 2 } + CONSTANT [17] { value: 3 } } } } - accu_var: @x1:0 + accu_var: @ac:0:0 accu_init: { - CREATE_LIST [22] { + LIST [18] { elements: { } } } loop_condition: { - CONSTANT [23] { value: true } + CONSTANT [19] { value: true } } loop_step: { - CALL [24] { - function: _?_:_ + CALL [20] { + function: _+_ args: { - CALL [25] { - function: _==_ - args: { - IDENT [26] { - name: @c1:0 - } - IDENT [27] { - name: @c0:0 - } - } + IDENT [21] { + name: @ac:0:0 } - CALL [28] { - function: _+_ - args: { - IDENT [29] { - name: @x1:0 - } - CREATE_LIST [30] { - elements: { - IDENT [31] { - name: @c1:0 + LIST [22] { + elements: { + CALL [23] { + function: _+_ + args: { + IDENT [24] { + name: @it:0:0 } + CONSTANT [25] { value: 1 } } } } } - IDENT [32] { - name: @x1:0 - } } } } result: { - IDENT [33] { - name: @x1:0 + IDENT [26] { + name: @ac:0:0 } } } @@ -1906,102 +2856,220 @@ CALL [1] { } } result: { - IDENT [34] { - name: @x0:0 + IDENT [27] { + name: @ac:1:0 } } } } } - CALL [35] { + CALL [28] { function: _==_ args: { - IDENT [36] { - name: @index1 + IDENT [29] { + name: @index0 } - IDENT [37] { + IDENT [30] { name: @index0 } } } } } -Test case: INCLUSION_LIST -Source: 1 in [1,2,3] && 2 in [1,2,3] && 3 in [3, [1,2,3]] && 1 in [1,2,3] +Test case: ADJACENT_NESTED_MACROS_COMP_V2 +Source: [1,2,3].transformMap(i, x, [1, 2, 3].transformMap(i, x, i + x + 1)) == [1,2,3].transformMap(j, y, [1, 2, 3].transformMap(j, y, j + y + 1)) =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - CONSTANT [5] { value: 2 } - CONSTANT [6] { value: 3 } + COMPREHENSION [3] { + iter_var: @it:1:0 + iter_var2: @it2:1:0 + iter_range: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + CONSTANT [6] { value: 2 } + CONSTANT [7] { value: 3 } + } + } + } + accu_var: @ac:1:0 + accu_init: { + MAP [8] { + + } + } + loop_condition: { + CONSTANT [9] { value: true } + } + loop_step: { + CALL [10] { + function: cel.@mapInsert + args: { + IDENT [11] { + name: @ac:1:0 + } + IDENT [12] { + name: @it:1:0 + } + COMPREHENSION [13] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + LIST [14] { + elements: { + CONSTANT [15] { value: 1 } + CONSTANT [16] { value: 2 } + CONSTANT [17] { value: 3 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + MAP [18] { + + } + } + loop_condition: { + CONSTANT [19] { value: true } + } + loop_step: { + CALL [20] { + function: cel.@mapInsert + args: { + IDENT [21] { + name: @ac:0:0 + } + IDENT [22] { + name: @it:0:0 + } + CALL [23] { + function: _+_ + args: { + CALL [24] { + function: _+_ + args: { + IDENT [25] { + name: @it:0:0 + } + IDENT [26] { + name: @it2:0:0 + } + } + } + CONSTANT [27] { value: 1 } + } + } + } + } + } + result: { + IDENT [28] { + name: @ac:0:0 + } + } + } + } + } + } + result: { + IDENT [29] { + name: @ac:1:0 + } } } - CALL [7] { + } + } + CALL [30] { + function: _==_ + args: { + IDENT [31] { + name: @index0 + } + IDENT [32] { + name: @index0 + } + } + } + } +} +Test case: INCLUSION_LIST +Source: 1 in [1,2,3] && 2 in [1,2,3] && 3 in [3, [1,2,3]] && 1 in [1,2,3] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + CALL [3] { function: @in args: { - CONSTANT [8] { value: 1 } - IDENT [9] { - name: @index0 + CONSTANT [4] { value: 1 } + LIST [5] { + elements: { + CONSTANT [6] { value: 1 } + CONSTANT [7] { value: 2 } + CONSTANT [8] { value: 3 } + } } } } - CALL [10] { + LIST [9] { + elements: { + CONSTANT [10] { value: 1 } + CONSTANT [11] { value: 2 } + CONSTANT [12] { value: 3 } + } + } + } + } + CALL [13] { + function: _&&_ + args: { + CALL [14] { function: _&&_ args: { - CALL [11] { + IDENT [15] { + name: @index0 + } + CALL [16] { function: @in args: { - CONSTANT [12] { value: 3 } - CREATE_LIST [13] { - elements: { - CONSTANT [14] { value: 3 } - IDENT [15] { - name: @index0 - } - } + CONSTANT [17] { value: 2 } + IDENT [18] { + name: @index1 } } } - IDENT [16] { - name: @index1 - } } } - CALL [17] { + CALL [19] { function: _&&_ args: { - IDENT [18] { - name: @index1 - } - CALL [19] { + CALL [20] { function: @in args: { - CONSTANT [20] { value: 2 } - IDENT [21] { - name: @index0 + CONSTANT [21] { value: 3 } + LIST [22] { + elements: { + CONSTANT [23] { value: 3 } + IDENT [24] { + name: @index1 + } + } } } } + IDENT [25] { + name: @index0 + } } } } } - CALL [22] { - function: _&&_ - args: { - IDENT [23] { - name: @index3 - } - IDENT [24] { - name: @index2 - } - } - } } } Test case: INCLUSION_MAP @@ -2010,9 +3078,9 @@ Source: 2 in {'a': 1, 2: {true: false}, 3: {true: false}} CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_MAP [3] { + MAP [3] { MAP_ENTRY [4] { key: { CONSTANT [5] { value: true } @@ -2022,31 +3090,37 @@ CALL [1] { } } } - CREATE_MAP [7] { - MAP_ENTRY [8] { + } + } + CALL [7] { + function: @in + args: { + CONSTANT [8] { value: 2 } + MAP [9] { + MAP_ENTRY [10] { key: { - CONSTANT [9] { value: "a" } + CONSTANT [11] { value: "a" } } value: { - CONSTANT [10] { value: 1 } + CONSTANT [12] { value: 1 } } } - MAP_ENTRY [11] { + MAP_ENTRY [13] { key: { - CONSTANT [12] { value: 2 } + CONSTANT [14] { value: 2 } } value: { - IDENT [13] { + IDENT [15] { name: @index0 } } } - MAP_ENTRY [14] { + MAP_ENTRY [16] { key: { - CONSTANT [15] { value: 3 } + CONSTANT [17] { value: 3 } } value: { - IDENT [16] { + IDENT [18] { name: @index0 } } @@ -2054,111 +3128,231 @@ CALL [1] { } } } - CALL [17] { - function: @in + } +} +Test case: MACRO_ITER_VAR_NOT_REFERENCED +Source: [1,2].map(i, [1, 2].map(i, [3,4])) == [[[3, 4], [3, 4]], [[3, 4], [3, 4]]] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + LIST [4] { + elements: { + CONSTANT [5] { value: 3 } + CONSTANT [6] { value: 4 } + } + } + LIST [7] { + elements: { + CONSTANT [8] { value: 3 } + CONSTANT [9] { value: 4 } + } + } + } + } + LIST [10] { + elements: { + CONSTANT [11] { value: 1 } + CONSTANT [12] { value: 2 } + } + } + } + } + CALL [13] { + function: _==_ args: { - CONSTANT [18] { value: 2 } - IDENT [19] { - name: @index1 + COMPREHENSION [14] { + iter_var: @it:1:0 + iter_range: { + IDENT [15] { + name: @index1 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [16] { + elements: { + } + } + } + loop_condition: { + CONSTANT [17] { value: true } + } + loop_step: { + CALL [18] { + function: _+_ + args: { + IDENT [19] { + name: @ac:1:0 + } + LIST [20] { + elements: { + COMPREHENSION [21] { + iter_var: @it:0:0 + iter_range: { + IDENT [22] { + name: @index1 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [23] { + elements: { + } + } + } + loop_condition: { + CONSTANT [24] { value: true } + } + loop_step: { + CALL [25] { + function: _+_ + args: { + IDENT [26] { + name: @ac:0:0 + } + LIST [27] { + elements: { + LIST [28] { + elements: { + CONSTANT [29] { value: 3 } + CONSTANT [30] { value: 4 } + } + } + } + } + } + } + } + result: { + IDENT [31] { + name: @ac:0:0 + } + } + } + } + } + } + } + } + result: { + IDENT [32] { + name: @ac:1:0 + } + } + } + LIST [33] { + elements: { + IDENT [34] { + name: @index0 + } + IDENT [35] { + name: @index0 + } + } } } } } } -Test case: MACRO_ITER_VAR_NOT_REFERENCED -Source: [1,2].map(i, [1, 2].map(i, [3,4])) == [[[3, 4], [3, 4]], [[3, 4], [3, 4]]] +Test case: MACRO_ITER_VAR2_NOT_REFERENCED +Source: [1,2].transformList(i, v, [1, 2].transformList(i, v, [3,4])) == [[[3, 4], [3, 4]], [[3, 4], [3, 4]]] =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - CONSTANT [5] { value: 2 } - } - } - CREATE_LIST [6] { - elements: { - CONSTANT [7] { value: 3 } - CONSTANT [8] { value: 4 } - } - } - CREATE_LIST [9] { + LIST [3] { elements: { - IDENT [10] { - name: @index1 + LIST [4] { + elements: { + CONSTANT [5] { value: 3 } + CONSTANT [6] { value: 4 } + } } - IDENT [11] { - name: @index1 + LIST [7] { + elements: { + CONSTANT [8] { value: 3 } + CONSTANT [9] { value: 4 } + } } } } - CREATE_LIST [12] { + LIST [10] { elements: { - IDENT [13] { - name: @index2 - } - IDENT [14] { - name: @index2 - } + CONSTANT [11] { value: 1 } + CONSTANT [12] { value: 2 } } } - COMPREHENSION [15] { - iter_var: @c0:0 + } + } + CALL [13] { + function: _==_ + args: { + COMPREHENSION [14] { + iter_var: @it:1:0 + iter_var2: @it2:1:0 iter_range: { - IDENT [16] { - name: @index0 + IDENT [15] { + name: @index1 } } - accu_var: @x0:0 + accu_var: @ac:1:0 accu_init: { - CREATE_LIST [17] { + LIST [16] { elements: { } } } loop_condition: { - CONSTANT [18] { value: true } + CONSTANT [17] { value: true } } loop_step: { - CALL [19] { + CALL [18] { function: _+_ args: { - IDENT [20] { - name: @x0:0 + IDENT [19] { + name: @ac:1:0 } - CREATE_LIST [21] { + LIST [20] { elements: { - COMPREHENSION [22] { - iter_var: @c1:0 + COMPREHENSION [21] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 iter_range: { - IDENT [23] { - name: @index0 + IDENT [22] { + name: @index1 } } - accu_var: @x1:0 + accu_var: @ac:0:0 accu_init: { - CREATE_LIST [24] { + LIST [23] { elements: { } } } loop_condition: { - CONSTANT [25] { value: true } + CONSTANT [24] { value: true } } loop_step: { - CALL [26] { + CALL [25] { function: _+_ args: { - IDENT [27] { - name: @x1:0 + IDENT [26] { + name: @ac:0:0 } - CREATE_LIST [28] { + LIST [27] { elements: { - IDENT [29] { - name: @index1 + LIST [28] { + elements: { + CONSTANT [29] { value: 3 } + CONSTANT [30] { value: 4 } + } } } } @@ -2166,8 +3360,8 @@ CALL [1] { } } result: { - IDENT [30] { - name: @x1:0 + IDENT [31] { + name: @ac:0:0 } } } @@ -2177,21 +3371,20 @@ CALL [1] { } } result: { - IDENT [31] { - name: @x0:0 + IDENT [32] { + name: @ac:1:0 } } } - } - } - CALL [32] { - function: _==_ - args: { - IDENT [33] { - name: @index4 - } - IDENT [34] { - name: @index3 + LIST [33] { + elements: { + IDENT [34] { + name: @index0 + } + IDENT [35] { + name: @index0 + } + } } } } @@ -2203,59 +3396,324 @@ Source: [x - 1 > 3 ? x - 1 : 5].exists(x, x - 1 > 3) || x - 1 > 3 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { - function: _-_ - args: { - IDENT [4] { - name: x - } - CONSTANT [5] { value: 1 } - } - } - CALL [6] { function: _>_ args: { - IDENT [7] { - name: @index0 + CALL [4] { + function: _-_ + args: { + IDENT [5] { + name: x + } + CONSTANT [6] { value: 1 } + } } - CONSTANT [8] { value: 3 } + CONSTANT [7] { value: 3 } } } + } + } + CALL [8] { + function: _||_ + args: { COMPREHENSION [9] { - iter_var: @c0:0 + iter_var: @it:0:0 iter_range: { - CREATE_LIST [10] { + LIST [10] { elements: { CALL [11] { function: _?_:_ args: { IDENT [12] { - name: @index1 + name: @index0 } - IDENT [13] { + CALL [13] { + function: _-_ + args: { + IDENT [14] { + name: x + } + CONSTANT [15] { value: 1 } + } + } + CONSTANT [16] { value: 5 } + } + } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [17] { value: false } + } + loop_condition: { + CALL [18] { + function: @not_strictly_false + args: { + CALL [19] { + function: !_ + args: { + IDENT [20] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [21] { + function: _||_ + args: { + IDENT [22] { + name: @ac:0:0 + } + CALL [23] { + function: _>_ + args: { + CALL [24] { + function: _-_ + args: { + IDENT [25] { + name: @it:0:0 + } + CONSTANT [26] { value: 1 } + } + } + CONSTANT [27] { value: 3 } + } + } + } + } + } + result: { + IDENT [28] { + name: @ac:0:0 + } + } + } + IDENT [29] { + name: @index0 + } + } + } + } +} +Test case: MACRO_SHADOWED_VARIABLE_2 +Source: ["foo", "bar"].map(x, [x + x, x + x]).map(x, [x + x, x + x]) +=====> +COMPREHENSION [35] { + iter_var: x + iter_range: { + COMPREHENSION [19] { + iter_var: x + iter_range: { + LIST [1] { + elements: { + CONSTANT [2] { value: "foo" } + CONSTANT [3] { value: "bar" } + } + } + } + accu_var: @result + accu_init: { + LIST [13] { + elements: { + } + } + } + loop_condition: { + CONSTANT [14] { value: true } + } + loop_step: { + CALL [17] { + function: _+_ + args: { + IDENT [15] { + name: @result + } + LIST [16] { + elements: { + LIST [6] { + elements: { + CALL [8] { + function: _+_ + args: { + IDENT [7] { + name: x + } + IDENT [9] { + name: x + } + } + } + CALL [11] { + function: _+_ + args: { + IDENT [10] { + name: x + } + IDENT [12] { + name: x + } + } + } + } + } + } + } + } + } + } + result: { + IDENT [18] { + name: @result + } + } + } + } + accu_var: @result + accu_init: { + LIST [29] { + elements: { + } + } + } + loop_condition: { + CONSTANT [30] { value: true } + } + loop_step: { + CALL [33] { + function: _+_ + args: { + IDENT [31] { + name: @result + } + LIST [32] { + elements: { + LIST [22] { + elements: { + CALL [24] { + function: _+_ + args: { + IDENT [23] { + name: x + } + IDENT [25] { + name: x + } + } + } + CALL [27] { + function: _+_ + args: { + IDENT [26] { + name: x + } + IDENT [28] { + name: x + } + } + } + } + } + } + } + } + } + } + result: { + IDENT [34] { + name: @result + } + } +} +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 +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + CALL [3] { + function: _>_ + args: { + CALL [4] { + function: _-_ + args: { + CALL [5] { + function: _-_ + args: { + IDENT [6] { + name: x + } + IDENT [7] { + name: y + } + } + } + CONSTANT [8] { value: 1 } + } + } + CONSTANT [9] { value: 3 } + } + } + } + } + CALL [10] { + function: _||_ + args: { + COMPREHENSION [11] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + LIST [12] { + elements: { + CALL [13] { + function: _?_:_ + args: { + IDENT [14] { name: @index0 } - CONSTANT [14] { value: 5 } + CALL [15] { + function: _-_ + args: { + CALL [16] { + function: _-_ + args: { + IDENT [17] { + name: x + } + IDENT [18] { + name: y + } + } + } + CONSTANT [19] { value: 1 } + } + } + CONSTANT [20] { value: 5 } } } } } } - accu_var: @x0:0 + accu_var: @ac:0:0 accu_init: { - CONSTANT [15] { value: false } + CONSTANT [21] { value: false } } loop_condition: { - CALL [16] { + CALL [22] { function: @not_strictly_false args: { - CALL [17] { + CALL [23] { function: !_ args: { - IDENT [18] { - name: @x0:0 + IDENT [24] { + name: @ac:0:0 } } } @@ -2263,96 +3721,109 @@ CALL [1] { } } loop_step: { - CALL [19] { + CALL [25] { function: _||_ args: { - IDENT [20] { - name: @x0:0 + IDENT [26] { + name: @ac:0:0 } - CALL [21] { + CALL [27] { function: _>_ args: { - CALL [22] { + CALL [28] { function: _-_ args: { - IDENT [23] { - name: @c0:0 + CALL [29] { + function: _-_ + args: { + IDENT [30] { + name: @it:0:0 + } + IDENT [31] { + name: @it2:0:0 + } + } } - CONSTANT [24] { value: 1 } + CONSTANT [32] { value: 1 } } } - CONSTANT [25] { value: 3 } + CONSTANT [33] { value: 3 } } } } } } result: { - IDENT [26] { - name: @x0:0 + IDENT [34] { + name: @ac:0:0 } } } - } - } - CALL [27] { - function: _||_ - args: { - IDENT [28] { - name: @index2 - } - IDENT [29] { - name: @index1 + IDENT [35] { + name: @index0 } } } } } -Test case: MACRO_SHADOWED_VARIABLE_2 -Source: ["foo", "bar"].map(x, [x + x, x + x]).map(x, [x + x, x + x]) +Test case: MACRO_SHADOWED_VARIABLE_COMP_V2_2 +Source: ["foo", "bar"].transformMap(x, y, [x + x, y + y]).transformMap(x, y, [x + x, y + y]) =====> -CALL [1] { - function: cel.@block - args: { - CREATE_LIST [2] { - elements: { - CALL [3] { - function: _+_ - args: { - IDENT [4] { - name: @c1:0 - } - IDENT [5] { - name: @c1:0 - } +COMPREHENSION [35] { + iter_var: x + iter_var2: y + iter_range: { + COMPREHENSION [19] { + iter_var: x + iter_var2: y + iter_range: { + LIST [1] { + elements: { + CONSTANT [2] { value: "foo" } + CONSTANT [3] { value: "bar" } } } - CALL [6] { - function: _+_ - args: { - IDENT [7] { - name: @c0:0 - } - IDENT [8] { - name: @c0:0 - } - } + } + accu_var: @result + accu_init: { + MAP [14] { + } - CALL [9] { - function: _+_ + } + loop_condition: { + CONSTANT [15] { value: true } + } + loop_step: { + CALL [17] { + function: cel.@mapInsert args: { - IDENT [10] { - name: @x0:0 + IDENT [16] { + name: @result + } + IDENT [5] { + name: x } - CREATE_LIST [11] { + LIST [7] { elements: { - CREATE_LIST [12] { - elements: { - IDENT [13] { - name: @index1 + CALL [9] { + function: _+_ + args: { + IDENT [8] { + name: x } - IDENT [14] { - name: @index1 + IDENT [10] { + name: x + } + } + } + CALL [12] { + function: _+_ + args: { + IDENT [11] { + name: y + } + IDENT [13] { + name: y } } } @@ -2360,85 +3831,65 @@ CALL [1] { } } } - COMPREHENSION [15] { - iter_var: @c1:0 - iter_range: { - CREATE_LIST [16] { - elements: { - CONSTANT [17] { value: "foo" } - CONSTANT [18] { value: "bar" } - } - } - } - accu_var: @x1:0 - accu_init: { - CREATE_LIST [19] { - elements: { + } + result: { + IDENT [18] { + name: @result + } + } + } + } + accu_var: @result + accu_init: { + MAP [30] { + + } + } + loop_condition: { + CONSTANT [31] { value: true } + } + loop_step: { + CALL [33] { + function: cel.@mapInsert + args: { + IDENT [32] { + name: @result + } + IDENT [21] { + name: x + } + LIST [23] { + elements: { + CALL [25] { + function: _+_ + args: { + IDENT [24] { + name: x + } + IDENT [26] { + name: x + } } } - } - loop_condition: { - CONSTANT [20] { value: true } - } - loop_step: { - CALL [21] { + CALL [28] { function: _+_ args: { - IDENT [22] { - name: @x1:0 + IDENT [27] { + name: y } - CREATE_LIST [23] { - elements: { - CREATE_LIST [24] { - elements: { - IDENT [25] { - name: @index0 - } - IDENT [26] { - name: @index0 - } - } - } - } + IDENT [29] { + name: y } } } } - result: { - IDENT [27] { - name: @x1:0 - } - } } } } - COMPREHENSION [28] { - iter_var: @c0:0 - iter_range: { - IDENT [29] { - name: @index3 - } - } - accu_var: @x0:0 - accu_init: { - CREATE_LIST [30] { - elements: { - } - } - } - loop_condition: { - CONSTANT [31] { value: true } - } - loop_step: { - IDENT [32] { - name: @index2 - } - } - result: { - IDENT [33] { - name: @x0:0 - } - } + } + result: { + IDENT [34] { + name: @result } } } @@ -2448,9 +3899,9 @@ Source: has({'a': true}.a) && {'a':true}['a'] CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_MAP [3] { + MAP [3] { MAP_ENTRY [4] { key: { CONSTANT [5] { value: "a" } @@ -2460,27 +3911,59 @@ CALL [1] { } } } - CALL [7] { + } + } + CALL [7] { + function: _&&_ + args: { + SELECT [8] { + IDENT [9] { + name: @index0 + }.a~presence_test + } + CALL [10] { function: _[_] args: { - IDENT [8] { + IDENT [11] { name: @index0 } - CONSTANT [9] { value: "a" } + CONSTANT [12] { value: "a" } } } } } - CALL [10] { + } +} +Test case: PRESENCE_TEST_2 +Source: has({'a': true}.a) && has({'a': true}.a) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + SELECT [3] { + MAP [4] { + MAP_ENTRY [5] { + key: { + CONSTANT [6] { value: "a" } + } + value: { + CONSTANT [7] { value: true } + } + } + }.a~presence_test + } + } + } + CALL [8] { function: _&&_ args: { - SELECT [11] { - IDENT [12] { - name: @index0 - }.a~presence_test + IDENT [9] { + name: @index0 } - IDENT [13] { - name: @index1 + IDENT [10] { + name: @index0 } } } @@ -2492,40 +3975,37 @@ Source: (has(msg.oneof_type.payload) ? msg.oneof_type.payload.single_int64 : 0) CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { name: msg }.oneof_type } - CALL [5] { + } + } + CALL [5] { + function: _==_ + args: { + CALL [6] { function: _?_:_ args: { - SELECT [6] { - IDENT [7] { + SELECT [7] { + IDENT [8] { name: @index0 }.payload~presence_test } - SELECT [8] { - SELECT [9] { - IDENT [10] { + SELECT [9] { + SELECT [10] { + IDENT [11] { name: @index0 }.payload }.single_int64 } - CONSTANT [11] { value: 0 } + CONSTANT [12] { value: 0 } } } - } - } - CALL [12] { - function: _==_ - args: { - IDENT [13] { - name: @index1 - } - CONSTANT [14] { value: 10 } + CONSTANT [13] { value: 10 } } } } @@ -2536,54 +4016,47 @@ Source: (has(msg.oneof_type.payload) ? msg.oneof_type.payload.single_int64 : msg CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload }.single_int64 } - CALL [9] { + } + } + CALL [7] { + function: _==_ + args: { + CALL [8] { function: _?_:_ args: { - SELECT [10] { - IDENT [11] { - name: @index0 + SELECT [9] { + SELECT [10] { + IDENT [11] { + name: msg + }.oneof_type }.payload~presence_test } IDENT [12] { - name: @index2 + name: @index0 } CALL [13] { function: _*_ args: { IDENT [14] { - name: @index2 + name: @index0 } CONSTANT [15] { value: 0 } } } } } - } - } - CALL [16] { - function: _==_ - args: { - IDENT [17] { - name: @index3 - } - CONSTANT [18] { value: 10 } + CONSTANT [16] { value: 10 } } } } @@ -2594,54 +4067,49 @@ Source: (has(msg.oneof_type.payload.single_int64) ? msg.oneof_type.payload.singl CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload }.single_int64 } - CALL [9] { + } + } + CALL [7] { + function: _==_ + args: { + CALL [8] { function: _?_:_ args: { - SELECT [10] { - IDENT [11] { - name: @index1 + SELECT [9] { + SELECT [10] { + SELECT [11] { + IDENT [12] { + name: msg + }.oneof_type + }.payload }.single_int64~presence_test } - IDENT [12] { - name: @index2 + IDENT [13] { + name: @index0 } - CALL [13] { + CALL [14] { function: _*_ args: { - IDENT [14] { - name: @index2 + IDENT [15] { + name: @index0 } - CONSTANT [15] { value: 0 } + CONSTANT [16] { value: 0 } } } } } - } - } - CALL [16] { - function: _==_ - args: { - IDENT [17] { - name: @index3 - } - CONSTANT [18] { value: 10 } + CONSTANT [17] { value: 10 } } } } @@ -2652,91 +4120,88 @@ Source: (has(msg.oneof_type) && has(msg.oneof_type.payload) && has(msg.oneof_typ CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.map_string_string } SELECT [7] { - IDENT [8] { - name: @index1 - }.map_string_string + SELECT [8] { + IDENT [9] { + name: msg + }.oneof_type + }.payload } - CALL [9] { - function: _?_:_ + } + } + CALL [10] { + function: _?_:_ + args: { + CALL [11] { + function: _&&_ args: { - CALL [10] { + CALL [12] { function: _&&_ args: { - SELECT [11] { - IDENT [12] { - name: @index1 - }.map_string_string~presence_test - } SELECT [13] { IDENT [14] { - name: @index2 - }.key~presence_test + name: msg + }.oneof_type~presence_test } - } - } - CALL [15] { - function: _==_ - args: { - SELECT [16] { - IDENT [17] { - name: @index2 - }.key + SELECT [15] { + SELECT [16] { + IDENT [17] { + name: msg + }.oneof_type + }.payload~presence_test } - CONSTANT [18] { value: "A" } } } - CONSTANT [19] { value: false } + SELECT [18] { + IDENT [19] { + name: @index1 + }.single_int64~presence_test + } } } CALL [20] { - function: _&&_ + function: _?_:_ args: { CALL [21] { function: _&&_ args: { SELECT [22] { IDENT [23] { - name: msg - }.oneof_type~presence_test + name: @index1 + }.map_string_string~presence_test } SELECT [24] { IDENT [25] { name: @index0 - }.payload~presence_test + }.key~presence_test } } } - SELECT [26] { - IDENT [27] { - name: @index1 - }.single_int64~presence_test + CALL [26] { + function: _==_ + args: { + SELECT [27] { + IDENT [28] { + name: @index0 + }.key + } + CONSTANT [29] { value: "A" } + } } + CONSTANT [30] { value: false } } } - } - } - CALL [28] { - function: _?_:_ - args: { - IDENT [29] { - name: @index4 - } - IDENT [30] { - name: @index3 - } CONSTANT [31] { value: false } } } @@ -2748,46 +4213,51 @@ Source: [10, ?optional.none(), [?optional.none(), ?opt_x], [?optional.none(), ?o CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CALL [3] { - function: optional.none - args: { - } - } - CREATE_LIST [4] { + LIST [3] { elements: { - IDENT [5] { - name: @index0 + CALL [4] { + function: optional.none + args: { + } } - IDENT [6] { + IDENT [5] { name: opt_x } } optional_indices: [0, 1] } - CREATE_LIST [7] { + LIST [6] { elements: { - CONSTANT [8] { value: 5 } + CONSTANT [7] { value: 5 } } } - CREATE_LIST [9] { + } + } + CALL [8] { + function: _==_ + args: { + LIST [9] { elements: { CONSTANT [10] { value: 10 } - IDENT [11] { - name: @index2 + CALL [11] { + function: optional.none + args: { + } } IDENT [12] { - name: @index2 + name: @index0 + } + IDENT [13] { + name: @index0 } } + optional_indices: [0] } - CREATE_LIST [13] { + LIST [14] { elements: { - CONSTANT [14] { value: 10 } - IDENT [15] { - name: @index0 - } + CONSTANT [15] { value: 10 } IDENT [16] { name: @index1 } @@ -2795,18 +4265,6 @@ CALL [1] { name: @index1 } } - optional_indices: [0] - } - } - } - CALL [18] { - function: _==_ - args: { - IDENT [19] { - name: @index4 - } - IDENT [20] { - name: @index3 } } } @@ -2818,56 +4276,47 @@ Source: {?'hello': optional.of('hello')}['hello'] + {?'hello': optional.of('hell CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { - function: optional.of - args: { - CONSTANT [4] { value: "hello" } - } - } - CREATE_MAP [5] { - MAP_ENTRY [6] { - key: { - CONSTANT [7] { value: "hello" } - } - optional_entry: true - value: { - IDENT [8] { - name: @index0 - } - } - } - } - CALL [9] { function: _[_] args: { - IDENT [10] { - name: @index1 + MAP [4] { + MAP_ENTRY [5] { + key: { + CONSTANT [6] { value: "hello" } + } + optional_entry: true + value: { + CALL [7] { + function: optional.of + args: { + CONSTANT [8] { value: "hello" } + } + } + } + } } - CONSTANT [11] { value: "hello" } + CONSTANT [9] { value: "hello" } } } - CALL [12] { + } + } + CALL [10] { + function: _==_ + args: { + CALL [11] { function: _+_ args: { - IDENT [13] { - name: @index2 + IDENT [12] { + name: @index0 } - IDENT [14] { - name: @index2 + IDENT [13] { + name: @index0 } } } - } - } - CALL [15] { - function: _==_ - args: { - IDENT [16] { - name: @index3 - } - CONSTANT [17] { value: "hellohello" } + CONSTANT [14] { value: "hellohello" } } } } @@ -2878,9 +4327,9 @@ Source: {?'key': optional.of('test')}[?'bogus'].or({'key': 'test'}[?'bogus']).or CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_MAP [3] { + MAP [3] { MAP_ENTRY [4] { key: { CONSTANT [5] { value: "key" } @@ -2890,69 +4339,66 @@ CALL [1] { } } } - CALL [7] { + } + } + CALL [7] { + function: _==_ + args: { + CALL [8] { function: orValue target: { - CALL [8] { + CALL [9] { function: or target: { - CALL [9] { + CALL [10] { function: _[?_] args: { - CREATE_MAP [10] { - MAP_ENTRY [11] { + MAP [11] { + MAP_ENTRY [12] { key: { - CONSTANT [12] { value: "key" } + CONSTANT [13] { value: "key" } } optional_entry: true value: { - CALL [13] { + CALL [14] { function: optional.of args: { - CONSTANT [14] { value: "test" } + CONSTANT [15] { value: "test" } } } } } } - CONSTANT [15] { value: "bogus" } + CONSTANT [16] { value: "bogus" } } } } args: { - CALL [16] { + CALL [17] { function: _[?_] args: { - IDENT [17] { + IDENT [18] { name: @index0 } - CONSTANT [18] { value: "bogus" } + CONSTANT [19] { value: "bogus" } } } } } } args: { - CALL [19] { + CALL [20] { function: _[_] args: { - IDENT [20] { + IDENT [21] { name: @index0 } - CONSTANT [21] { value: "key" } + CONSTANT [22] { value: "key" } } } } } - } - } - CALL [22] { - function: _==_ - args: { - IDENT [23] { - name: @index1 - } - CONSTANT [24] { value: "test" } + CONSTANT [23] { value: "test" } } } } @@ -2963,146 +4409,69 @@ Source: TestAllTypes{?single_int64: optional.ofNonZeroValue(1), ?single_int32: o CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CALL [3] { - function: optional.ofNonZeroValue - args: { - CONSTANT [4] { value: 1 } - } - } - CALL [5] { - function: optional.of - args: { - CONSTANT [6] { value: 4 } - } - } - CREATE_STRUCT [7] { - name: TestAllTypes + STRUCT [3] { + name: cel.expr.conformance.proto3.TestAllTypes entries: { - ENTRY [8] { + ENTRY [4] { field_key: single_int64 optional_entry: true value: { - IDENT [9] { - name: @index0 - } - } - } - ENTRY [10] { - field_key: single_int32 - optional_entry: true - value: { - IDENT [11] { - name: @index1 + CALL [5] { + function: optional.ofNonZeroValue + args: { + CONSTANT [6] { value: 1 } + } } } } - } - } - CALL [12] { - function: _+_ - args: { - SELECT [13] { - IDENT [14] { - name: @index2 - }.single_int32 - } - SELECT [15] { - IDENT [16] { - name: @index2 - }.single_int64 - } - } - } - } - } - CALL [17] { - function: _==_ - args: { - IDENT [18] { - name: @index3 - } - CONSTANT [19] { value: 5 } - } - } - } -} -Test case: CALL -Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('h' + 'e' + 'l' + 'l' + 'o') -=====> -CALL [1] { - function: cel.@block - args: { - CREATE_LIST [2] { - elements: { - CALL [3] { - function: _+_ - args: { - CONSTANT [4] { value: "h" } - CONSTANT [5] { value: "e" } - } - } - CALL [6] { - function: _+_ - args: { - IDENT [7] { - name: @index0 - } - CONSTANT [8] { value: "l" } - } - } - CALL [9] { - function: _+_ - args: { - IDENT [10] { - name: @index1 - } - CONSTANT [11] { value: "l" } - } - } - CALL [12] { - function: _+_ - args: { - IDENT [13] { - name: @index2 + ENTRY [7] { + field_key: single_int32 + optional_entry: true + value: { + CALL [8] { + function: optional.of + args: { + CONSTANT [9] { value: 4 } + } + } + } } - CONSTANT [14] { value: "o" } } } - CALL [15] { + } + } + CALL [10] { + function: _==_ + args: { + CALL [11] { function: _+_ args: { - IDENT [16] { - name: @index3 + SELECT [12] { + IDENT [13] { + name: @index0 + }.single_int32 + } + SELECT [14] { + IDENT [15] { + name: @index0 + }.single_int64 } - CONSTANT [17] { value: " world" } } } - } - } - CALL [18] { - function: matches - target: { - IDENT [19] { - name: @index4 - } - } - args: { - IDENT [20] { - name: @index3 - } + CONSTANT [16] { value: 5 } } } } } -Test case: CALL_ARGUMENT_NESTED_NO_COMMON_SUBEXPR -Source: 'hello world'.matches('h' + 'e' + 'l' + 'l' + 'o') +Test case: CALL +Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('h' + 'e' + 'l' + 'l' + 'o') =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { function: _+_ @@ -3134,147 +4503,164 @@ CALL [1] { CALL [12] { function: matches target: { - CONSTANT [13] { value: "hello world" } + CALL [13] { + function: _+_ + args: { + IDENT [14] { + name: @index0 + } + CONSTANT [15] { value: " world" } + } + } } args: { - IDENT [14] { + IDENT [16] { name: @index0 } } } } } -Test case: CALL_TARGET_NESTED_NO_COMMON_SUBEXPR -Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('hello') +Test case: CALL_ARGUMENT_NESTED_NO_COMMON_SUBEXPR +Source: 'hello world'.matches('h' + 'e' + 'l' + 'l' + 'o') =====> -CALL [1] { - function: cel.@block +CALL [2] { + function: matches + target: { + CONSTANT [1] { value: "hello world" } + } args: { - CREATE_LIST [2] { - elements: { - CALL [3] { + CALL [10] { + function: _+_ + args: { + CALL [8] { function: _+_ args: { - CALL [4] { + CALL [6] { function: _+_ args: { - CALL [5] { + CALL [4] { function: _+_ args: { - CALL [6] { - function: _+_ - args: { - CALL [7] { - function: _+_ - args: { - CONSTANT [8] { value: "h" } - CONSTANT [9] { value: "e" } - } - } - CONSTANT [10] { value: "l" } - } - } - CONSTANT [11] { value: "l" } + CONSTANT [3] { value: "h" } + CONSTANT [5] { value: "e" } } } - CONSTANT [12] { value: "o" } + CONSTANT [7] { value: "l" } } } - CONSTANT [13] { value: " world" } + CONSTANT [9] { value: "l" } } } - } - } - CALL [14] { - function: matches - target: { - IDENT [15] { - name: @index0 - } - } - args: { - CONSTANT [16] { value: "hello" } + CONSTANT [11] { value: "o" } } } } } -Test case: CALL_BOTH_ARGUMENT_TARGET_NESTED_NO_COMMON_SUBEXPR -Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('w' + 'o' + 'r' + 'l' + 'd') +Test case: CALL_TARGET_NESTED_NO_COMMON_SUBEXPR +Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('hello') =====> -CALL [1] { - function: cel.@block - args: { - CREATE_LIST [2] { - elements: { - CALL [3] { +CALL [12] { + function: matches + target: { + CALL [10] { + function: _+_ + args: { + CALL [8] { function: _+_ args: { - CALL [4] { + CALL [6] { function: _+_ args: { - CALL [5] { + CALL [4] { function: _+_ args: { - CALL [6] { + CALL [2] { function: _+_ args: { - CONSTANT [7] { value: "w" } - CONSTANT [8] { value: "o" } + CONSTANT [1] { value: "h" } + CONSTANT [3] { value: "e" } } } - CONSTANT [9] { value: "r" } + CONSTANT [5] { value: "l" } } } - CONSTANT [10] { value: "l" } + CONSTANT [7] { value: "l" } } } - CONSTANT [11] { value: "d" } + CONSTANT [9] { value: "o" } } } - CALL [12] { + CONSTANT [11] { value: " world" } + } + } + } + args: { + CONSTANT [13] { value: "hello" } + } +} +Test case: CALL_BOTH_ARGUMENT_TARGET_NESTED_NO_COMMON_SUBEXPR +Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('w' + 'o' + 'r' + 'l' + 'd') +=====> +CALL [12] { + function: matches + target: { + CALL [10] { + function: _+_ + args: { + CALL [8] { function: _+_ args: { - CALL [13] { + CALL [6] { function: _+_ args: { - CALL [14] { + CALL [4] { function: _+_ args: { - CALL [15] { + CALL [2] { function: _+_ args: { - CALL [16] { - function: _+_ - args: { - CONSTANT [17] { value: "h" } - CONSTANT [18] { value: "e" } - } - } - CONSTANT [19] { value: "l" } + CONSTANT [1] { value: "h" } + CONSTANT [3] { value: "e" } } } - CONSTANT [20] { value: "l" } + CONSTANT [5] { value: "l" } } } - CONSTANT [21] { value: "o" } + CONSTANT [7] { value: "l" } } } - CONSTANT [22] { value: " world" } + CONSTANT [9] { value: "o" } } } + CONSTANT [11] { value: " world" } } } - CALL [23] { - function: matches - target: { - IDENT [24] { - name: @index1 - } - } + } + args: { + CALL [20] { + function: _+_ args: { - IDENT [25] { - name: @index0 + CALL [18] { + function: _+_ + args: { + CALL [16] { + function: _+_ + args: { + CALL [14] { + function: _+_ + args: { + CONSTANT [13] { value: "w" } + CONSTANT [15] { value: "o" } + } + } + CONSTANT [17] { value: "r" } + } + } + CONSTANT [19] { value: "l" } + } } + CONSTANT [21] { value: "d" } } } } @@ -3285,77 +4671,69 @@ Source: non_pure_custom_func(msg.oneof_type.payload.single_int64) + non_pure_cus CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 - }.single_int64 - } - SELECT [9] { - IDENT [10] { - name: msg + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload }.single_int64 } - SELECT [11] { - IDENT [12] { - name: @index1 - }.single_int32 - } } } - CALL [13] { + CALL [7] { function: _+_ args: { - CALL [14] { + CALL [8] { function: _+_ args: { - CALL [15] { + CALL [9] { function: _+_ args: { - CALL [16] { + CALL [10] { function: non_pure_custom_func args: { - IDENT [17] { - name: @index2 + IDENT [11] { + name: @index0 } } } - CALL [18] { + CALL [12] { function: non_pure_custom_func args: { - IDENT [19] { - name: @index4 + SELECT [13] { + SELECT [14] { + SELECT [15] { + IDENT [16] { + name: msg + }.oneof_type + }.payload + }.single_int32 } } } } } - CALL [20] { + CALL [17] { function: non_pure_custom_func args: { - IDENT [21] { - name: @index2 + IDENT [18] { + name: @index0 } } } } } - CALL [22] { + CALL [19] { function: non_pure_custom_func args: { - IDENT [23] { - name: @index3 + SELECT [20] { + IDENT [21] { + name: msg + }.single_int64 } } } @@ -3369,79 +4747,68 @@ Source: pure_custom_func(msg.oneof_type.payload.single_int64) + pure_custom_func CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 - }.single_int64 - } - CALL [9] { - function: pure_custom_func - args: { - IDENT [10] { - name: @index2 - } - } - } - CALL [11] { + CALL [3] { function: pure_custom_func args: { - SELECT [12] { - IDENT [13] { - name: msg + SELECT [4] { + SELECT [5] { + SELECT [6] { + IDENT [7] { + name: msg + }.oneof_type + }.payload }.single_int64 } } } - CALL [14] { + } + } + CALL [8] { + function: _+_ + args: { + CALL [9] { function: _+_ args: { - CALL [15] { + CALL [10] { function: _+_ args: { - IDENT [16] { - name: @index3 + IDENT [11] { + name: @index0 } - CALL [17] { + CALL [12] { function: pure_custom_func args: { - SELECT [18] { - IDENT [19] { - name: @index1 + SELECT [13] { + SELECT [14] { + SELECT [15] { + IDENT [16] { + name: msg + }.oneof_type + }.payload }.single_int32 } } } } } - IDENT [20] { - name: @index3 + IDENT [17] { + name: @index0 } } } - } - } - CALL [21] { - function: _+_ - args: { - IDENT [22] { - name: @index5 - } - IDENT [23] { - name: @index4 + CALL [18] { + function: pure_custom_func + args: { + SELECT [19] { + IDENT [20] { + name: msg + }.single_int64 + } + } } } } } -} +} \ No newline at end of file diff --git a/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_9.baseline b/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_9.baseline index fc16cdf85..c2d7334af 100644 --- a/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_9.baseline +++ b/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_9.baseline @@ -4,22 +4,24 @@ Source: size([1,2]) + size([1,2]) + 1 == 5 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - CONSTANT [5] { value: 2 } - } - } - CALL [6] { + CALL [3] { function: size args: { - IDENT [7] { - name: @index0 + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + CONSTANT [6] { value: 2 } + } } } } + } + } + CALL [7] { + function: _==_ + args: { CALL [8] { function: _+_ args: { @@ -27,25 +29,17 @@ CALL [1] { function: _+_ args: { IDENT [10] { - name: @index1 + name: @index0 } IDENT [11] { - name: @index1 + name: @index0 } } } CONSTANT [12] { value: 1 } } } - } - } - CALL [13] { - function: _==_ - args: { - IDENT [14] { - name: @index2 - } - CONSTANT [15] { value: 5 } + CONSTANT [13] { value: 5 } } } } @@ -56,22 +50,24 @@ Source: 2 + size([1,2]) + size([1,2]) + 1 == 7 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - CONSTANT [5] { value: 2 } - } - } - CALL [6] { + CALL [3] { function: size args: { - IDENT [7] { - name: @index0 + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + CONSTANT [6] { value: 2 } + } } } } + } + } + CALL [7] { + function: _==_ + args: { CALL [8] { function: _+_ args: { @@ -83,27 +79,19 @@ CALL [1] { args: { CONSTANT [11] { value: 2 } IDENT [12] { - name: @index1 + name: @index0 } } } IDENT [13] { - name: @index1 + name: @index0 } } } CONSTANT [14] { value: 1 } } } - } - } - CALL [15] { - function: _==_ - args: { - IDENT [16] { - name: @index2 - } - CONSTANT [17] { value: 7 } + CONSTANT [15] { value: 7 } } } } @@ -114,71 +102,62 @@ Source: size([0]) + size([0]) + size([1,2]) + size([1,2]) == 6 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 0 } - } - } - CALL [5] { + CALL [3] { function: size args: { - IDENT [6] { - name: @index0 + LIST [4] { + elements: { + CONSTANT [5] { value: 0 } + } } } } - CREATE_LIST [7] { - elements: { - CONSTANT [8] { value: 1 } - CONSTANT [9] { value: 2 } - } - } - CALL [10] { + CALL [6] { function: size args: { - IDENT [11] { - name: @index2 + LIST [7] { + elements: { + CONSTANT [8] { value: 1 } + CONSTANT [9] { value: 2 } + } } } } - CALL [12] { + } + } + CALL [10] { + function: _==_ + args: { + CALL [11] { function: _+_ args: { - CALL [13] { + CALL [12] { function: _+_ args: { - CALL [14] { + CALL [13] { function: _+_ args: { - IDENT [15] { - name: @index1 + IDENT [14] { + name: @index0 } - IDENT [16] { - name: @index1 + IDENT [15] { + name: @index0 } } } - IDENT [17] { - name: @index3 + IDENT [16] { + name: @index1 } } } - IDENT [18] { - name: @index3 + IDENT [17] { + name: @index1 } } } - } - } - CALL [19] { - function: _==_ - args: { - IDENT [20] { - name: @index4 - } - CONSTANT [21] { value: 6 } + CONSTANT [18] { value: 6 } } } } @@ -189,108 +168,96 @@ Source: 5 + size([0]) + size([0]) + size([1,2]) + size([1,2]) + size([1,2,3]) + CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 0 } - } - } - CALL [5] { + CALL [3] { function: size args: { - IDENT [6] { - name: @index0 + LIST [4] { + elements: { + CONSTANT [5] { value: 0 } + } } } } - CREATE_LIST [7] { - elements: { - CONSTANT [8] { value: 1 } - CONSTANT [9] { value: 2 } - } - } - CALL [10] { + CALL [6] { function: size args: { - IDENT [11] { - name: @index2 + LIST [7] { + elements: { + CONSTANT [8] { value: 1 } + CONSTANT [9] { value: 2 } + } } } } - CREATE_LIST [12] { - elements: { - CONSTANT [13] { value: 1 } - CONSTANT [14] { value: 2 } - CONSTANT [15] { value: 3 } - } - } - CALL [16] { + CALL [10] { function: size args: { - IDENT [17] { - name: @index4 + LIST [11] { + elements: { + CONSTANT [12] { value: 1 } + CONSTANT [13] { value: 2 } + CONSTANT [14] { value: 3 } + } } } } - CALL [18] { + } + } + CALL [15] { + function: _==_ + args: { + CALL [16] { function: _+_ args: { - CALL [19] { + CALL [17] { function: _+_ args: { - CALL [20] { + CALL [18] { function: _+_ args: { - CALL [21] { + CALL [19] { function: _+_ args: { - CALL [22] { + CALL [20] { function: _+_ args: { - CALL [23] { + CALL [21] { function: _+_ args: { - CONSTANT [24] { value: 5 } - IDENT [25] { - name: @index1 + CONSTANT [22] { value: 5 } + IDENT [23] { + name: @index0 } } } - IDENT [26] { - name: @index1 + IDENT [24] { + name: @index0 } } } - IDENT [27] { - name: @index3 + IDENT [25] { + name: @index1 } } } - IDENT [28] { - name: @index3 + IDENT [26] { + name: @index1 } } } - IDENT [29] { - name: @index5 + IDENT [27] { + name: @index2 } } } - IDENT [30] { - name: @index5 + IDENT [28] { + name: @index2 } } } - } - } - CALL [31] { - function: _==_ - args: { - IDENT [32] { - name: @index6 - } - CONSTANT [33] { value: 17 } + CONSTANT [29] { value: 17 } } } } @@ -301,148 +268,118 @@ Source: timestamp(int(timestamp(1000000000))).getFullYear() + timestamp(int(time CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { - function: timestamp - args: { - CONSTANT [4] { value: 1000000000 } - } - } - CALL [5] { - function: int - args: { - IDENT [6] { - name: @index0 + function: getFullYear + target: { + CALL [4] { + function: timestamp + args: { + CALL [5] { + function: int + args: { + CALL [6] { + function: timestamp + args: { + CONSTANT [7] { value: 1000000000 } + } + } + } + } + } } } - } - CALL [7] { - function: timestamp args: { - IDENT [8] { - name: @index1 - } } } - CALL [9] { + CALL [8] { function: getFullYear target: { - IDENT [10] { - name: @index2 + CALL [9] { + function: timestamp + args: { + CALL [10] { + function: int + args: { + CALL [11] { + function: timestamp + args: { + CONSTANT [12] { value: 200 } + } + } + } + } + } } } args: { } } - CALL [11] { - function: timestamp - args: { - CONSTANT [12] { value: 50 } - } - } CALL [13] { - function: int - args: { - IDENT [14] { - name: @index4 - } - } - } - CALL [15] { function: timestamp args: { - IDENT [16] { - name: @index5 + CALL [14] { + function: int + args: { + CALL [15] { + function: timestamp + args: { + CONSTANT [16] { value: 50 } + } + } + } } } } CALL [17] { function: timestamp args: { - CONSTANT [18] { value: 200 } - } - } - CALL [19] { - function: int - args: { - IDENT [20] { - name: @index7 + CALL [18] { + function: int + args: { + CALL [19] { + function: timestamp + args: { + CONSTANT [20] { value: 75 } + } + } + } } } } CALL [21] { - function: timestamp - args: { - IDENT [22] { - name: @index8 - } - } - } - CALL [23] { - function: getFullYear - target: { - IDENT [24] { - name: @index9 - } - } - args: { - } - } - CALL [25] { - function: timestamp - args: { - CONSTANT [26] { value: 75 } - } - } - CALL [27] { - function: int - args: { - IDENT [28] { - name: @index11 - } - } - } - CALL [29] { - function: timestamp - args: { - IDENT [30] { - name: @index12 - } - } - } - CALL [31] { function: _+_ args: { - CALL [32] { + CALL [22] { function: _+_ args: { - CALL [33] { + CALL [23] { function: _+_ args: { - CALL [34] { + CALL [24] { function: _+_ args: { - CALL [35] { + CALL [25] { function: _+_ args: { - CALL [36] { + CALL [26] { function: _+_ args: { - CALL [37] { + CALL [27] { function: _+_ args: { - CALL [38] { + CALL [28] { function: _+_ args: { - IDENT [39] { - name: @index3 + IDENT [29] { + name: @index0 } - CALL [40] { + CALL [30] { function: getFullYear target: { - IDENT [41] { - name: @index13 + IDENT [31] { + name: @index3 } } args: { @@ -450,11 +387,11 @@ CALL [1] { } } } - CALL [42] { + CALL [32] { function: getFullYear target: { - IDENT [43] { - name: @index6 + IDENT [33] { + name: @index2 } } args: { @@ -462,16 +399,16 @@ CALL [1] { } } } - IDENT [44] { - name: @index3 + IDENT [34] { + name: @index0 } } } - CALL [45] { + CALL [35] { function: getSeconds target: { - IDENT [46] { - name: @index6 + IDENT [36] { + name: @index2 } } args: { @@ -479,21 +416,21 @@ CALL [1] { } } } - IDENT [47] { - name: @index10 + IDENT [37] { + name: @index1 } } } - IDENT [48] { - name: @index10 + IDENT [38] { + name: @index1 } } } - CALL [49] { + CALL [39] { function: getMinutes target: { - IDENT [50] { - name: @index13 + IDENT [40] { + name: @index3 } } args: { @@ -501,20 +438,20 @@ CALL [1] { } } } - IDENT [51] { - name: @index3 + IDENT [41] { + name: @index0 } } } } } - CALL [52] { + CALL [42] { function: _==_ args: { - IDENT [53] { - name: @index14 + IDENT [43] { + name: @index4 } - CONSTANT [54] { value: 13934 } + CONSTANT [44] { value: 13934 } } } } @@ -525,55 +462,49 @@ Source: {"a": 2}["a"] + {"a": 2}["a"] * {"a": 2}["a"] == 6 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_MAP [3] { - MAP_ENTRY [4] { - key: { - CONSTANT [5] { value: "a" } - } - value: { - CONSTANT [6] { value: 2 } - } - } - } - CALL [7] { + CALL [3] { function: _[_] args: { - IDENT [8] { - name: @index0 + MAP [4] { + MAP_ENTRY [5] { + key: { + CONSTANT [6] { value: "a" } + } + value: { + CONSTANT [7] { value: 2 } + } + } } - CONSTANT [9] { value: "a" } + CONSTANT [8] { value: "a" } } } + } + } + CALL [9] { + function: _==_ + args: { CALL [10] { function: _+_ args: { IDENT [11] { - name: @index1 + name: @index0 } CALL [12] { function: _*_ args: { IDENT [13] { - name: @index1 + name: @index0 } IDENT [14] { - name: @index1 + name: @index0 } } } } } - } - } - CALL [15] { - function: _==_ - args: { - IDENT [16] { - name: @index2 - } - CONSTANT [17] { value: 6 } + CONSTANT [15] { value: 6 } } } } @@ -584,56 +515,53 @@ Source: {'a': {'b': 1}, 'c': {'b': 1}, 'd': {'e': {'b': 1}}, 'e': {'e': {'b': 1} CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_MAP [3] { + MAP [3] { MAP_ENTRY [4] { key: { - CONSTANT [5] { value: "b" } + CONSTANT [5] { value: "e" } } value: { - CONSTANT [6] { value: 1 } + MAP [6] { + MAP_ENTRY [7] { + key: { + CONSTANT [8] { value: "b" } + } + value: { + CONSTANT [9] { value: 1 } + } + } + } } } } - CREATE_MAP [7] { - MAP_ENTRY [8] { + MAP [10] { + MAP_ENTRY [11] { key: { - CONSTANT [9] { value: "e" } + CONSTANT [12] { value: "b" } } value: { - IDENT [10] { - name: @index0 - } + CONSTANT [13] { value: 1 } } } } } } - CREATE_MAP [11] { - MAP_ENTRY [12] { - key: { - CONSTANT [13] { value: "a" } - } - value: { - IDENT [14] { - name: @index0 - } - } - } + MAP [14] { MAP_ENTRY [15] { key: { - CONSTANT [16] { value: "c" } + CONSTANT [16] { value: "a" } } value: { IDENT [17] { - name: @index0 + name: @index1 } } } MAP_ENTRY [18] { key: { - CONSTANT [19] { value: "d" } + CONSTANT [19] { value: "c" } } value: { IDENT [20] { @@ -643,11 +571,21 @@ CALL [1] { } MAP_ENTRY [21] { key: { - CONSTANT [22] { value: "e" } + CONSTANT [22] { value: "d" } } value: { IDENT [23] { - name: @index1 + name: @index0 + } + } + } + MAP_ENTRY [24] { + key: { + CONSTANT [25] { value: "e" } + } + value: { + IDENT [26] { + name: @index0 } } } @@ -660,9 +598,9 @@ Source: [1, [1,2,3,4], 2, [1,2,3,4], 5, [1,2,3,4], 7, [[1,2], [1,2,3,4]], [1,2]] CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { + LIST [3] { elements: { CONSTANT [4] { value: 1 } CONSTANT [5] { value: 2 } @@ -670,43 +608,40 @@ CALL [1] { CONSTANT [7] { value: 4 } } } - CREATE_LIST [8] { + LIST [8] { elements: { CONSTANT [9] { value: 1 } CONSTANT [10] { value: 2 } } } - CREATE_LIST [11] { - elements: { - IDENT [12] { - name: @index1 - } - IDENT [13] { - name: @index0 - } - } - } } } - CREATE_LIST [14] { + LIST [11] { elements: { - CONSTANT [15] { value: 1 } - IDENT [16] { + CONSTANT [12] { value: 1 } + IDENT [13] { name: @index0 } - CONSTANT [17] { value: 2 } - IDENT [18] { + CONSTANT [14] { value: 2 } + IDENT [15] { name: @index0 } - CONSTANT [19] { value: 5 } - IDENT [20] { + CONSTANT [16] { value: 5 } + IDENT [17] { name: @index0 } - CONSTANT [21] { value: 7 } - IDENT [22] { - name: @index2 + CONSTANT [18] { value: 7 } + LIST [19] { + elements: { + IDENT [20] { + name: @index1 + } + IDENT [21] { + name: @index0 + } + } } - IDENT [23] { + IDENT [22] { name: @index1 } } @@ -719,33 +654,30 @@ Source: msg.single_int64 + msg.single_int64 == 6 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { name: msg }.single_int64 } - CALL [5] { + } + } + CALL [5] { + function: _==_ + args: { + CALL [6] { function: _+_ args: { - IDENT [6] { + IDENT [7] { name: @index0 } - IDENT [7] { + IDENT [8] { name: @index0 } } } - } - } - CALL [8] { - function: _==_ - args: { - IDENT [9] { - name: @index1 - } - CONSTANT [10] { value: 6 } + CONSTANT [9] { value: 6 } } } } @@ -756,61 +688,67 @@ Source: msg.oneof_type.payload.single_int64 + msg.oneof_type.payload.single_int3 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.single_int64 } SELECT [7] { - IDENT [8] { - name: @index1 - }.single_int64 + SELECT [8] { + IDENT [9] { + name: msg + }.oneof_type + }.payload } - CALL [9] { + } + } + CALL [10] { + function: _==_ + args: { + CALL [11] { function: _+_ args: { - CALL [10] { + CALL [12] { function: _+_ args: { - CALL [11] { + CALL [13] { function: _+_ args: { - CALL [12] { + CALL [14] { function: _+_ args: { - IDENT [13] { - name: @index2 + IDENT [15] { + name: @index0 } - SELECT [14] { - IDENT [15] { + SELECT [16] { + IDENT [17] { name: @index1 }.single_int32 } } } - IDENT [16] { - name: @index2 + IDENT [18] { + name: @index0 } } } - SELECT [17] { - IDENT [18] { + SELECT [19] { + IDENT [20] { name: msg }.single_int64 } } } - SELECT [19] { - SELECT [20] { - SELECT [21] { - IDENT [22] { + SELECT [21] { + SELECT [22] { + SELECT [23] { + IDENT [24] { name: @index1 }.oneof_type }.payload @@ -818,14 +756,6 @@ CALL [1] { } } } - } - } - CALL [23] { - function: _==_ - args: { - IDENT [24] { - name: @index3 - } CONSTANT [25] { value: 31 } } } @@ -837,54 +767,36 @@ Source: true || msg.oneof_type.payload.oneof_type.payload.oneof_type.payload.one CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 - }.oneof_type - } - SELECT [9] { - IDENT [10] { - name: @index2 - }.payload - } - SELECT [11] { - IDENT [12] { - name: @index3 - }.oneof_type - } - SELECT [13] { - SELECT [14] { - SELECT [15] { - SELECT [16] { - IDENT [17] { - name: @index4 - }.child - }.child + SELECT [4] { + SELECT [5] { + SELECT [6] { + SELECT [7] { + IDENT [8] { + name: msg + }.oneof_type + }.payload + }.oneof_type }.payload - }.single_bool + }.oneof_type } - CALL [18] { + } + } + CALL [9] { + function: _||_ + args: { + CALL [10] { function: _||_ args: { - CONSTANT [19] { value: true } - SELECT [20] { - SELECT [21] { - SELECT [22] { - SELECT [23] { - IDENT [24] { - name: @index4 + CONSTANT [11] { value: true } + SELECT [12] { + SELECT [13] { + SELECT [14] { + SELECT [15] { + IDENT [16] { + name: @index0 }.payload }.oneof_type }.payload @@ -892,16 +804,16 @@ CALL [1] { } } } - } - } - CALL [25] { - function: _||_ - args: { - IDENT [26] { - name: @index6 - } - IDENT [27] { - name: @index5 + SELECT [17] { + SELECT [18] { + SELECT [19] { + SELECT [20] { + IDENT [21] { + name: @index0 + }.child + }.child + }.payload + }.single_bool } } } @@ -913,60 +825,48 @@ Source: msg.oneof_type.payload.map_int32_int64[1] + msg.oneof_type.payload.map_i CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 - }.map_int32_int64 - } - CALL [9] { + CALL [3] { function: _[_] args: { - IDENT [10] { - name: @index2 + SELECT [4] { + SELECT [5] { + SELECT [6] { + IDENT [7] { + name: msg + }.oneof_type + }.payload + }.map_int32_int64 } - CONSTANT [11] { value: 1 } + CONSTANT [8] { value: 1 } } } - CALL [12] { + } + } + CALL [9] { + function: _==_ + args: { + CALL [10] { function: _+_ args: { - CALL [13] { + CALL [11] { function: _+_ args: { - IDENT [14] { - name: @index3 + IDENT [12] { + name: @index0 } - IDENT [15] { - name: @index3 + IDENT [13] { + name: @index0 } } } - IDENT [16] { - name: @index3 + IDENT [14] { + name: @index0 } } } - } - } - CALL [17] { - function: _==_ - args: { - IDENT [18] { - name: @index4 - } - CONSTANT [19] { value: 15 } + CONSTANT [15] { value: 15 } } } } @@ -977,69 +877,60 @@ Source: msg.oneof_type.payload.map_int32_int64[0] + msg.oneof_type.payload.map_i CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.map_int32_int64 } - SELECT [7] { - IDENT [8] { - name: @index1 - }.map_int32_int64 - } - CALL [9] { + } + } + CALL [7] { + function: _==_ + args: { + CALL [8] { function: _+_ args: { - CALL [10] { + CALL [9] { function: _+_ args: { - CALL [11] { + CALL [10] { function: _[_] args: { - IDENT [12] { - name: @index2 + IDENT [11] { + name: @index0 } - CONSTANT [13] { value: 0 } + CONSTANT [12] { value: 0 } } } - CALL [14] { + CALL [13] { function: _[_] args: { - IDENT [15] { - name: @index2 + IDENT [14] { + name: @index0 } - CONSTANT [16] { value: 1 } + CONSTANT [15] { value: 1 } } } } } - CALL [17] { + CALL [16] { function: _[_] args: { - IDENT [18] { - name: @index2 + IDENT [17] { + name: @index0 } - CONSTANT [19] { value: 2 } + CONSTANT [18] { value: 2 } } } } } - } - } - CALL [20] { - function: _==_ - args: { - IDENT [21] { - name: @index3 - } - CONSTANT [22] { value: 8 } + CONSTANT [19] { value: 8 } } } } @@ -1047,38 +938,26 @@ CALL [1] { Test case: SELECT_NESTED_NO_COMMON_SUBEXPR Source: msg.oneof_type.payload.oneof_type.payload.oneof_type.payload.oneof_type.payload.single_int64 =====> -CALL [1] { - function: cel.@block - args: { - CREATE_LIST [2] { - elements: { - SELECT [3] { - SELECT [4] { - SELECT [5] { - SELECT [6] { - SELECT [7] { - SELECT [8] { - SELECT [9] { - SELECT [10] { - IDENT [11] { - name: msg - }.oneof_type - }.payload - }.oneof_type - }.payload - }.oneof_type - }.payload - }.oneof_type - }.payload - } - } - } - SELECT [12] { - IDENT [13] { - name: @index0 - }.single_int64 - } - } +SELECT [10] { + SELECT [9] { + SELECT [8] { + SELECT [7] { + SELECT [6] { + SELECT [5] { + SELECT [4] { + SELECT [3] { + SELECT [2] { + IDENT [1] { + name: msg + }.oneof_type + }.payload + }.oneof_type + }.payload + }.oneof_type + }.payload + }.oneof_type + }.payload + }.single_int64 } Test case: TERNARY Source: (msg.single_int64 > 0 ? msg.single_int64 : 0) == 3 @@ -1086,40 +965,37 @@ Source: (msg.single_int64 > 0 ? msg.single_int64 : 0) == 3 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { name: msg }.single_int64 } - CALL [5] { + } + } + CALL [5] { + function: _==_ + args: { + CALL [6] { function: _?_:_ args: { - CALL [6] { + CALL [7] { function: _>_ args: { - IDENT [7] { + IDENT [8] { name: @index0 } - CONSTANT [8] { value: 0 } + CONSTANT [9] { value: 0 } } } - IDENT [9] { + IDENT [10] { name: @index0 } - CONSTANT [10] { value: 0 } + CONSTANT [11] { value: 0 } } } - } - } - CALL [11] { - function: _==_ - args: { - IDENT [12] { - name: @index1 - } - CONSTANT [13] { value: 3 } + CONSTANT [12] { value: 3 } } } } @@ -1130,54 +1006,51 @@ Source: false ? false : (msg.single_int64) + ((msg.single_int64 + 1) * 2) == 11 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { name: msg }.single_int64 } - CALL [5] { + } + } + CALL [5] { + function: _?_:_ + args: { + CONSTANT [6] { value: false } + CONSTANT [7] { value: false } + CALL [8] { function: _==_ args: { - CALL [6] { + CALL [9] { function: _+_ args: { - IDENT [7] { + IDENT [10] { name: @index0 } - CALL [8] { + CALL [11] { function: _*_ args: { - CALL [9] { + CALL [12] { function: _+_ args: { - IDENT [10] { + IDENT [13] { name: @index0 } - CONSTANT [11] { value: 1 } + CONSTANT [14] { value: 1 } } } - CONSTANT [12] { value: 2 } + CONSTANT [15] { value: 2 } } } } } - CONSTANT [13] { value: 11 } + CONSTANT [16] { value: 11 } } } } } - CALL [14] { - function: _?_:_ - args: { - CONSTANT [15] { value: false } - CONSTANT [16] { value: false } - IDENT [17] { - name: @index1 - } - } - } } } Test case: NESTED_TERNARY @@ -1186,7 +1059,7 @@ Source: (msg.single_int64 > 0 ? (msg.single_int32 > 0 ? msg.single_int64 + msg.s CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { @@ -1198,56 +1071,53 @@ CALL [1] { name: msg }.single_int32 } - CALL [7] { + } + } + CALL [7] { + function: _==_ + args: { + CALL [8] { function: _?_:_ args: { - CALL [8] { + CALL [9] { function: _>_ args: { - IDENT [9] { + IDENT [10] { name: @index0 } - CONSTANT [10] { value: 0 } + CONSTANT [11] { value: 0 } } } - CALL [11] { + CALL [12] { function: _?_:_ args: { - CALL [12] { + CALL [13] { function: _>_ args: { - IDENT [13] { + IDENT [14] { name: @index1 } - CONSTANT [14] { value: 0 } + CONSTANT [15] { value: 0 } } } - CALL [15] { + CALL [16] { function: _+_ args: { - IDENT [16] { + IDENT [17] { name: @index0 } - IDENT [17] { + IDENT [18] { name: @index1 } } } - CONSTANT [18] { value: 0 } + CONSTANT [19] { value: 0 } } } - CONSTANT [19] { value: 0 } + CONSTANT [20] { value: 0 } } } - } - } - CALL [20] { - function: _==_ - args: { - IDENT [21] { - name: @index2 - } - CONSTANT [22] { value: 8 } + CONSTANT [21] { value: 8 } } } } @@ -1258,53 +1128,30 @@ Source: size([[1].exists(i, i > 0)]) + size([[1].exists(j, j > 0)]) + size([[2]. CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - } - } - CALL [5] { - function: _>_ - args: { - IDENT [6] { - name: @c0:0 - } - CONSTANT [7] { value: 0 } - } - } - CALL [8] { - function: _||_ - args: { - IDENT [9] { - name: @x0:0 - } - IDENT [10] { - name: @index1 - } - } - } - COMPREHENSION [11] { - iter_var: @c0:0 + COMPREHENSION [3] { + iter_var: @it:0:0 iter_range: { - IDENT [12] { - name: @index0 + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + } } } - accu_var: @x0:0 + accu_var: @ac:0:0 accu_init: { - CONSTANT [13] { value: false } + CONSTANT [6] { value: false } } loop_condition: { - CALL [14] { + CALL [7] { function: @not_strictly_false args: { - CALL [15] { + CALL [8] { function: !_ args: { - IDENT [16] { - name: @x0:0 + IDENT [9] { + name: @ac:0:0 } } } @@ -1312,76 +1159,52 @@ CALL [1] { } } loop_step: { - IDENT [17] { - name: @index2 + CALL [10] { + function: _||_ + args: { + IDENT [11] { + name: @ac:0:0 + } + CALL [12] { + function: _>_ + args: { + IDENT [13] { + name: @it:0:0 + } + CONSTANT [14] { value: 0 } + } + } + } } } result: { - IDENT [18] { - name: @x0:0 - } - } - } - CREATE_LIST [19] { - elements: { - IDENT [20] { - name: @index3 - } - } - } - CALL [21] { - function: size - args: { - IDENT [22] { - name: @index4 - } - } - } - CREATE_LIST [23] { - elements: { - CONSTANT [24] { value: 2 } - } - } - CALL [25] { - function: _>_ - args: { - IDENT [26] { - name: @c0:0 - } - CONSTANT [27] { value: 1 } - } - } - CALL [28] { - function: _||_ - args: { - IDENT [29] { - name: @x0:0 - } - IDENT [30] { - name: @index7 + IDENT [15] { + name: @ac:0:0 } } } - COMPREHENSION [31] { - iter_var: @c0:0 + COMPREHENSION [16] { + iter_var: @it:0:0 iter_range: { - IDENT [32] { - name: @index6 + LIST [17] { + elements: { + CONSTANT [18] { value: 2 } + } } } - accu_var: @x0:0 + accu_var: @ac:0:0 accu_init: { - CONSTANT [33] { value: false } + CONSTANT [19] { value: false } } loop_condition: { - CALL [34] { + CALL [20] { function: @not_strictly_false args: { - CALL [35] { + CALL [21] { function: !_ args: { - IDENT [36] { - name: @x0:0 + IDENT [22] { + name: @ac:0:0 } } } @@ -1389,67 +1212,87 @@ CALL [1] { } } loop_step: { - IDENT [37] { - name: @index8 + CALL [23] { + function: _||_ + args: { + IDENT [24] { + name: @ac:0:0 + } + CALL [25] { + function: _>_ + args: { + IDENT [26] { + name: @it:0:0 + } + CONSTANT [27] { value: 1 } + } + } + } } } result: { - IDENT [38] { - name: @x0:0 + IDENT [28] { + name: @ac:0:0 } } } - CREATE_LIST [39] { - elements: { - IDENT [40] { - name: @index9 + CALL [29] { + function: size + args: { + LIST [30] { + elements: { + IDENT [31] { + name: @index0 + } + } } } } - CALL [41] { + CALL [32] { function: size args: { - IDENT [42] { - name: @index10 + LIST [33] { + elements: { + IDENT [34] { + name: @index1 + } + } } } } - CALL [43] { + } + } + CALL [35] { + function: _==_ + args: { + CALL [36] { function: _+_ args: { - CALL [44] { + CALL [37] { function: _+_ args: { - CALL [45] { + CALL [38] { function: _+_ args: { - IDENT [46] { - name: @index5 + IDENT [39] { + name: @index2 } - IDENT [47] { - name: @index5 + IDENT [40] { + name: @index2 } } } - IDENT [48] { - name: @index11 + IDENT [41] { + name: @index3 } } } - IDENT [49] { - name: @index11 + IDENT [42] { + name: @index3 } } } - } - } - CALL [50] { - function: _==_ - args: { - IDENT [51] { - name: @index12 - } - CONSTANT [52] { value: 4 } + CONSTANT [43] { value: 4 } } } } @@ -1460,53 +1303,30 @@ Source: [[1].exists(i, i > 0)] + [[1].exists(j, j > 0)] + [['a'].exists(k, k == CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - } - } - CALL [5] { - function: _>_ - args: { - IDENT [6] { - name: @c0:0 + COMPREHENSION [3] { + iter_var: @it:0:0 + iter_range: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + } } - CONSTANT [7] { value: 0 } } - } - CALL [8] { - function: _||_ - args: { - IDENT [9] { - name: @x0:0 - } - IDENT [10] { - name: @index1 - } - } - } - COMPREHENSION [11] { - iter_var: @c0:0 - iter_range: { - IDENT [12] { - name: @index0 - } - } - accu_var: @x0:0 + accu_var: @ac:0:0 accu_init: { - CONSTANT [13] { value: false } + CONSTANT [6] { value: false } } loop_condition: { - CALL [14] { + CALL [7] { function: @not_strictly_false args: { - CALL [15] { + CALL [8] { function: !_ args: { - IDENT [16] { - name: @x0:0 + IDENT [9] { + name: @ac:0:0 } } } @@ -1514,68 +1334,52 @@ CALL [1] { } } loop_step: { - IDENT [17] { - name: @index2 + CALL [10] { + function: _||_ + args: { + IDENT [11] { + name: @ac:0:0 + } + CALL [12] { + function: _>_ + args: { + IDENT [13] { + name: @it:0:0 + } + CONSTANT [14] { value: 0 } + } + } + } } } result: { - IDENT [18] { - name: @x0:0 - } - } - } - CREATE_LIST [19] { - elements: { - IDENT [20] { - name: @index3 - } - } - } - CREATE_LIST [21] { - elements: { - CONSTANT [22] { value: "a" } - } - } - CALL [23] { - function: _==_ - args: { - IDENT [24] { - name: @c0:1 - } - CONSTANT [25] { value: "a" } - } - } - CALL [26] { - function: _||_ - args: { - IDENT [27] { - name: @x0:1 - } - IDENT [28] { - name: @index6 + IDENT [15] { + name: @ac:0:0 } } } - COMPREHENSION [29] { - iter_var: @c0:1 + COMPREHENSION [16] { + iter_var: @it:0:1 iter_range: { - IDENT [30] { - name: @index5 + LIST [17] { + elements: { + CONSTANT [18] { value: "a" } + } } } - accu_var: @x0:1 + accu_var: @ac:0:1 accu_init: { - CONSTANT [31] { value: false } + CONSTANT [19] { value: false } } loop_condition: { - CALL [32] { + CALL [20] { function: @not_strictly_false args: { - CALL [33] { + CALL [21] { function: !_ args: { - IDENT [34] { - name: @x0:1 + IDENT [22] { + name: @ac:0:1 } } } @@ -1583,317 +1387,1454 @@ CALL [1] { } } loop_step: { - IDENT [35] { - name: @index7 + CALL [23] { + function: _||_ + args: { + IDENT [24] { + name: @ac:0:1 + } + CALL [25] { + function: _==_ + args: { + IDENT [26] { + name: @it:0:1 + } + CONSTANT [27] { value: "a" } + } + } + } } } result: { - IDENT [36] { - name: @x0:1 + IDENT [28] { + name: @ac:0:1 } } } - CREATE_LIST [37] { + LIST [29] { elements: { - IDENT [38] { - name: @index8 + IDENT [30] { + name: @index0 } } } - CREATE_LIST [39] { + LIST [31] { elements: { - CONSTANT [40] { value: true } - CONSTANT [41] { value: true } - CONSTANT [42] { value: true } - CONSTANT [43] { value: true } + IDENT [32] { + name: @index1 + } } } - CALL [44] { + } + } + CALL [33] { + function: _==_ + args: { + CALL [34] { function: _+_ args: { - CALL [45] { + CALL [35] { function: _+_ args: { - CALL [46] { + CALL [36] { function: _+_ args: { - IDENT [47] { - name: @index4 + IDENT [37] { + name: @index2 } - IDENT [48] { - name: @index4 + IDENT [38] { + name: @index2 } } } - IDENT [49] { - name: @index9 + IDENT [39] { + name: @index3 } } } - IDENT [50] { - name: @index9 + IDENT [40] { + name: @index3 } } } - } - } - CALL [51] { - function: _==_ - args: { - IDENT [52] { - name: @index11 - } - IDENT [53] { - name: @index10 + LIST [41] { + elements: { + CONSTANT [42] { value: true } + CONSTANT [43] { value: true } + CONSTANT [44] { value: true } + CONSTANT [45] { value: true } + } } } } } } -Test case: NESTED_MACROS -Source: [1,2,3].map(i, [1, 2, 3].map(i, i + 1)) == [[2, 3, 4], [2, 3, 4], [2, 3, 4]] +Test case: MULTIPLE_MACROS_3 +Source: [1].exists(i, i > 0) && [1].exists(j, j > 0) && [1].exists(k, k > 1) && [2].exists(l, l > 1) =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - CONSTANT [5] { value: 2 } - CONSTANT [6] { value: 3 } - } - } - CREATE_LIST [7] { - elements: { - CONSTANT [8] { value: 2 } - CONSTANT [9] { value: 3 } - CONSTANT [10] { value: 4 } - } - } - CREATE_LIST [11] { - elements: { - IDENT [12] { - name: @index1 - } - IDENT [13] { - name: @index1 - } - IDENT [14] { - name: @index1 - } - } - } - COMPREHENSION [15] { - iter_var: @c0:0 + COMPREHENSION [3] { + iter_var: @it:0:0 iter_range: { - IDENT [16] { - name: @index0 - } - } - accu_var: @x0:0 - accu_init: { - CREATE_LIST [17] { + LIST [4] { elements: { + CONSTANT [5] { value: 1 } } } } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [6] { value: false } + } loop_condition: { - CONSTANT [18] { value: true } + CALL [7] { + function: @not_strictly_false + args: { + CALL [8] { + function: !_ + args: { + IDENT [9] { + name: @ac:0:0 + } + } + } + } + } } loop_step: { - CALL [19] { - function: _+_ + CALL [10] { + function: _||_ args: { - IDENT [20] { - name: @x0:0 + IDENT [11] { + name: @ac:0:0 } - CREATE_LIST [21] { - elements: { - COMPREHENSION [22] { - iter_var: @c1:0 - iter_range: { - IDENT [23] { - name: @index0 - } - } - accu_var: @x1:0 - accu_init: { - CREATE_LIST [24] { - elements: { - } - } - } - loop_condition: { - CONSTANT [25] { value: true } - } - loop_step: { - CALL [26] { - function: _+_ - args: { - IDENT [27] { - name: @x1:0 - } - CREATE_LIST [28] { - elements: { - CALL [29] { - function: _+_ - args: { - IDENT [30] { - name: @c1:0 - } - CONSTANT [31] { value: 1 } - } - } - } - } - } - } - } - result: { - IDENT [32] { - name: @x1:0 - } - } + CALL [12] { + function: _>_ + args: { + IDENT [13] { + name: @it:0:0 } + CONSTANT [14] { value: 0 } } } } } } result: { - IDENT [33] { - name: @x0:0 + IDENT [15] { + name: @ac:0:0 } } } } } - CALL [34] { - function: _==_ + CALL [16] { + function: _&&_ args: { - IDENT [35] { - name: @index3 - } - IDENT [36] { - name: @index2 - } - } - } - } -} -Test case: NESTED_MACROS_2 -Source: [1, 2].map(y, [1, 2, 3].filter(x, x == y)) == [[1], [2]] -=====> -CALL [1] { - function: cel.@block - args: { - CREATE_LIST [2] { - elements: { - CREATE_LIST [3] { - elements: { - CREATE_LIST [4] { - elements: { - CONSTANT [5] { value: 1 } - } + CALL [17] { + function: _&&_ + args: { + IDENT [18] { + name: @index0 } - CREATE_LIST [6] { - elements: { - CONSTANT [7] { value: 2 } - } + IDENT [19] { + name: @index0 } } } - COMPREHENSION [8] { - iter_var: @c0:0 - iter_range: { - CREATE_LIST [9] { - elements: { - CONSTANT [10] { value: 1 } - CONSTANT [11] { value: 2 } + CALL [20] { + function: _&&_ + args: { + COMPREHENSION [21] { + iter_var: @it:0:0 + iter_range: { + LIST [22] { + elements: { + CONSTANT [23] { value: 1 } + } + } } - } - } - accu_var: @x0:0 - accu_init: { - CREATE_LIST [12] { - elements: { + accu_var: @ac:0:0 + accu_init: { + CONSTANT [24] { value: false } } - } - } - loop_condition: { - CONSTANT [13] { value: true } - } - loop_step: { - CALL [14] { - function: _+_ - args: { - IDENT [15] { - name: @x0:0 - } - CREATE_LIST [16] { - elements: { - COMPREHENSION [17] { - iter_var: @c1:0 - iter_range: { - CREATE_LIST [18] { - elements: { - CONSTANT [19] { value: 1 } - CONSTANT [20] { value: 2 } - CONSTANT [21] { value: 3 } - } + loop_condition: { + CALL [25] { + function: @not_strictly_false + args: { + CALL [26] { + function: !_ + args: { + IDENT [27] { + name: @ac:0:0 } } - accu_var: @x1:0 - accu_init: { - CREATE_LIST [22] { - elements: { - } - } + } + } + } + } + loop_step: { + CALL [28] { + function: _||_ + args: { + IDENT [29] { + name: @ac:0:0 + } + CALL [30] { + function: _>_ + args: { + IDENT [31] { + name: @it:0:0 + } + CONSTANT [32] { value: 1 } + } + } + } + } + } + result: { + IDENT [33] { + name: @ac:0:0 + } + } + } + COMPREHENSION [34] { + iter_var: @it:0:0 + iter_range: { + LIST [35] { + elements: { + CONSTANT [36] { value: 2 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [37] { value: false } + } + loop_condition: { + CALL [38] { + function: @not_strictly_false + args: { + CALL [39] { + function: !_ + args: { + IDENT [40] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [41] { + function: _||_ + args: { + IDENT [42] { + name: @ac:0:0 + } + CALL [43] { + function: _>_ + args: { + IDENT [44] { + name: @it:0:0 + } + CONSTANT [45] { value: 1 } + } + } + } + } + } + result: { + IDENT [46] { + name: @ac:0:0 + } + } + } + } + } + } + } + } +} +Test case: MULTIPLE_MACROS_COMP_V2_1 +Source: size([[1].exists(i,x,i > 0 && x >= 0)]) + size([[1].exists(j, y, j > 0 && y >= 0)]) + size([[2].exists(k,z,k > 1 && z >= 0)]) + size([[2].exists(l, w, l > 1 && w >= 0)]) == 4 +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + COMPREHENSION [3] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [6] { value: false } + } + loop_condition: { + CALL [7] { + function: @not_strictly_false + args: { + CALL [8] { + function: !_ + args: { + IDENT [9] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [10] { + function: _||_ + args: { + IDENT [11] { + name: @ac:0:0 + } + CALL [12] { + function: _&&_ + args: { + CALL [13] { + function: _>_ + args: { + IDENT [14] { + name: @it:0:0 + } + CONSTANT [15] { value: 0 } + } + } + CALL [16] { + function: _>=_ + args: { + IDENT [17] { + name: @it2:0:0 + } + CONSTANT [18] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [19] { + name: @ac:0:0 + } + } + } + COMPREHENSION [20] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + LIST [21] { + elements: { + CONSTANT [22] { value: 2 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [23] { value: false } + } + loop_condition: { + CALL [24] { + function: @not_strictly_false + args: { + CALL [25] { + function: !_ + args: { + IDENT [26] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [27] { + function: _||_ + args: { + IDENT [28] { + name: @ac:0:0 + } + CALL [29] { + function: _&&_ + args: { + CALL [30] { + function: _>_ + args: { + IDENT [31] { + name: @it:0:0 + } + CONSTANT [32] { value: 1 } + } + } + CALL [33] { + function: _>=_ + args: { + IDENT [34] { + name: @it2:0:0 + } + CONSTANT [35] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [36] { + name: @ac:0:0 + } + } + } + CALL [37] { + function: size + args: { + LIST [38] { + elements: { + IDENT [39] { + name: @index0 + } + } + } + } + } + CALL [40] { + function: size + args: { + LIST [41] { + elements: { + IDENT [42] { + name: @index1 + } + } + } + } + } + } + } + CALL [43] { + function: _==_ + args: { + CALL [44] { + function: _+_ + args: { + CALL [45] { + function: _+_ + args: { + CALL [46] { + function: _+_ + args: { + IDENT [47] { + name: @index2 + } + IDENT [48] { + name: @index2 + } + } + } + IDENT [49] { + name: @index3 + } + } + } + IDENT [50] { + name: @index3 + } + } + } + CONSTANT [51] { value: 4 } + } + } + } +} +Test case: MULTIPLE_MACROS_COMP_V2_2 +Source: [1].exists(i, x, i > 0 && x > 0) && [1].exists(j, y, j > 0 && y > 0) && [1].exists(k, z, k > 1 && z > 0) && [2].exists(l, w, l > 1 && w > 0) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + COMPREHENSION [3] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [6] { value: false } + } + loop_condition: { + CALL [7] { + function: @not_strictly_false + args: { + CALL [8] { + function: !_ + args: { + IDENT [9] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [10] { + function: _||_ + args: { + IDENT [11] { + name: @ac:0:0 + } + CALL [12] { + function: _&&_ + args: { + CALL [13] { + function: _>_ + args: { + IDENT [14] { + name: @it:0:0 + } + CONSTANT [15] { value: 0 } + } + } + CALL [16] { + function: _>_ + args: { + IDENT [17] { + name: @it2:0:0 + } + CONSTANT [18] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [19] { + name: @ac:0:0 + } + } + } + } + } + CALL [20] { + function: _&&_ + args: { + CALL [21] { + function: _&&_ + args: { + IDENT [22] { + name: @index0 + } + IDENT [23] { + name: @index0 + } + } + } + CALL [24] { + function: _&&_ + args: { + COMPREHENSION [25] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + LIST [26] { + elements: { + CONSTANT [27] { value: 1 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [28] { value: false } + } + loop_condition: { + CALL [29] { + function: @not_strictly_false + args: { + CALL [30] { + function: !_ + args: { + IDENT [31] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [32] { + function: _||_ + args: { + IDENT [33] { + name: @ac:0:0 + } + CALL [34] { + function: _&&_ + args: { + CALL [35] { + function: _>_ + args: { + IDENT [36] { + name: @it:0:0 + } + CONSTANT [37] { value: 1 } + } + } + CALL [38] { + function: _>_ + args: { + IDENT [39] { + name: @it2:0:0 + } + CONSTANT [40] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [41] { + name: @ac:0:0 + } + } + } + COMPREHENSION [42] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + LIST [43] { + elements: { + CONSTANT [44] { value: 2 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [45] { value: false } + } + loop_condition: { + CALL [46] { + function: @not_strictly_false + args: { + CALL [47] { + function: !_ + args: { + IDENT [48] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [49] { + function: _||_ + args: { + IDENT [50] { + name: @ac:0:0 + } + CALL [51] { + function: _&&_ + args: { + CALL [52] { + function: _>_ + args: { + IDENT [53] { + name: @it:0:0 + } + CONSTANT [54] { value: 1 } + } + } + CALL [55] { + function: _>_ + args: { + IDENT [56] { + name: @it2:0:0 + } + CONSTANT [57] { value: 0 } + } + } + } + } + } + } + } + result: { + IDENT [58] { + name: @ac:0:0 + } + } + } + } + } + } + } + } +} +Test case: NESTED_MACROS +Source: [1,2,3].map(i, [1, 2, 3].map(i, i + 1)) == [[2, 3, 4], [2, 3, 4], [2, 3, 4]] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + CONSTANT [5] { value: 2 } + CONSTANT [6] { value: 3 } + } + } + LIST [7] { + elements: { + CONSTANT [8] { value: 2 } + CONSTANT [9] { value: 3 } + CONSTANT [10] { value: 4 } + } + } + } + } + CALL [11] { + function: _==_ + args: { + COMPREHENSION [12] { + iter_var: @it:1:0 + iter_range: { + IDENT [13] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [14] { + elements: { + } + } + } + loop_condition: { + CONSTANT [15] { value: true } + } + loop_step: { + CALL [16] { + function: _+_ + args: { + IDENT [17] { + name: @ac:1:0 + } + LIST [18] { + elements: { + COMPREHENSION [19] { + iter_var: @it:0:0 + iter_range: { + IDENT [20] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [21] { + elements: { + } + } + } + loop_condition: { + CONSTANT [22] { value: true } + } + loop_step: { + CALL [23] { + function: _+_ + args: { + IDENT [24] { + name: @ac:0:0 + } + LIST [25] { + elements: { + CALL [26] { + function: _+_ + args: { + IDENT [27] { + name: @it:0:0 + } + CONSTANT [28] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [29] { + name: @ac:0:0 + } + } + } + } + } + } + } + } + result: { + IDENT [30] { + name: @ac:1:0 + } + } + } + LIST [31] { + elements: { + IDENT [32] { + name: @index1 + } + IDENT [33] { + name: @index1 + } + IDENT [34] { + name: @index1 + } + } + } + } + } + } +} +Test case: NESTED_MACROS_2 +Source: [1, 2].map(y, [1, 2, 3].filter(x, x == y)) == [[1], [2]] +=====> +CALL [31] { + function: _==_ + args: { + COMPREHENSION [30] { + iter_var: y + iter_range: { + LIST [1] { + elements: { + CONSTANT [2] { value: 1 } + CONSTANT [3] { value: 2 } + } + } + } + accu_var: @result + accu_init: { + LIST [24] { + elements: { + } + } + } + loop_condition: { + CONSTANT [25] { value: true } + } + loop_step: { + CALL [28] { + function: _+_ + args: { + IDENT [26] { + name: @result + } + LIST [27] { + elements: { + COMPREHENSION [23] { + iter_var: x + iter_range: { + LIST [6] { + elements: { + CONSTANT [7] { value: 1 } + CONSTANT [8] { value: 2 } + CONSTANT [9] { value: 3 } + } + } + } + accu_var: @result + accu_init: { + LIST [15] { + elements: { + } + } + } + loop_condition: { + CONSTANT [16] { value: true } + } + loop_step: { + CALL [21] { + function: _?_:_ + args: { + CALL [13] { + function: _==_ + args: { + IDENT [12] { + name: x + } + IDENT [14] { + name: y + } + } + } + CALL [19] { + function: _+_ + args: { + IDENT [17] { + name: @result + } + LIST [18] { + elements: { + IDENT [11] { + name: x + } + } + } + } + } + IDENT [20] { + name: @result + } + } + } + } + result: { + IDENT [22] { + name: @result + } + } + } + } + } + } + } + } + result: { + IDENT [29] { + name: @result + } + } + } + LIST [32] { + elements: { + LIST [33] { + elements: { + CONSTANT [34] { value: 1 } + } + } + LIST [35] { + elements: { + CONSTANT [36] { value: 2 } + } + } + } + } + } +} +Test case: NESTED_MACROS_3 +Source: [1,2,3].map(i, i * 2) + [1,2,3].map(i, i * 2).map(i, i * 2) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + COMPREHENSION [3] { + iter_var: @it:0:0 + iter_range: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + CONSTANT [6] { value: 2 } + CONSTANT [7] { value: 3 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [8] { + elements: { + } + } + } + loop_condition: { + CONSTANT [9] { value: true } + } + loop_step: { + CALL [10] { + function: _+_ + args: { + IDENT [11] { + name: @ac:0:0 + } + LIST [12] { + elements: { + CALL [13] { + function: _*_ + args: { + IDENT [14] { + name: @it:0:0 + } + CONSTANT [15] { value: 2 } + } + } + } + } + } + } + } + result: { + IDENT [16] { + name: @ac:0:0 + } + } + } + } + } + CALL [17] { + function: _+_ + args: { + IDENT [18] { + name: @index0 + } + COMPREHENSION [19] { + iter_var: @it:1:0 + iter_range: { + IDENT [20] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [21] { + elements: { + } + } + } + loop_condition: { + CONSTANT [22] { value: true } + } + loop_step: { + CALL [23] { + function: _+_ + args: { + IDENT [24] { + name: @ac:1:0 + } + LIST [25] { + elements: { + CALL [26] { + function: _*_ + args: { + IDENT [27] { + name: @it:1:0 + } + CONSTANT [28] { value: 2 } + } + } + } + } + } + } + } + result: { + IDENT [29] { + name: @ac:1:0 + } + } + } + } + } + } +} +Test case: NESTED_MACROS_COMP_V2_1 +Source: [1,2,3].transformList(i, v, [1, 2, 3].transformList(i, v, i + v + 1)) == [[2, 4, 6], [2, 4, 6], [2, 4, 6]] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + CONSTANT [5] { value: 2 } + CONSTANT [6] { value: 3 } + } + } + LIST [7] { + elements: { + CONSTANT [8] { value: 2 } + CONSTANT [9] { value: 4 } + CONSTANT [10] { value: 6 } + } + } + } + } + CALL [11] { + function: _==_ + args: { + COMPREHENSION [12] { + iter_var: @it:1:0 + iter_var2: @it2:1:0 + iter_range: { + IDENT [13] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [14] { + elements: { + } + } + } + loop_condition: { + CONSTANT [15] { value: true } + } + loop_step: { + CALL [16] { + function: _+_ + args: { + IDENT [17] { + name: @ac:1:0 + } + LIST [18] { + elements: { + COMPREHENSION [19] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + IDENT [20] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [21] { + elements: { + } + } + } + loop_condition: { + CONSTANT [22] { value: true } + } + loop_step: { + CALL [23] { + function: _+_ + args: { + IDENT [24] { + name: @ac:0:0 + } + LIST [25] { + elements: { + CALL [26] { + function: _+_ + args: { + CALL [27] { + function: _+_ + args: { + IDENT [28] { + name: @it:0:0 + } + IDENT [29] { + name: @it2:0:0 + } + } + } + CONSTANT [30] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [31] { + name: @ac:0:0 + } + } + } + } + } + } + } + } + result: { + IDENT [32] { + name: @ac:1:0 + } + } + } + LIST [33] { + elements: { + IDENT [34] { + name: @index1 + } + IDENT [35] { + name: @index1 + } + IDENT [36] { + name: @index1 + } + } + } + } + } + } +} +Test case: NESTED_MACROS_COMP_V2_2 +Source: [1, 2].transformList(i, y, [1, 2, 3].filter(x, x == y && i < y)) == [[1], [2]] +=====> +CALL [36] { + function: _==_ + args: { + COMPREHENSION [35] { + iter_var: i + iter_var2: y + iter_range: { + LIST [1] { + elements: { + CONSTANT [2] { value: 1 } + CONSTANT [3] { value: 2 } + } + } + } + accu_var: @result + accu_init: { + LIST [29] { + elements: { + } + } + } + loop_condition: { + CONSTANT [30] { value: true } + } + loop_step: { + CALL [33] { + function: _+_ + args: { + IDENT [31] { + name: @result + } + LIST [32] { + elements: { + COMPREHENSION [28] { + iter_var: x + iter_range: { + LIST [7] { + elements: { + CONSTANT [8] { value: 1 } + CONSTANT [9] { value: 2 } + CONSTANT [10] { value: 3 } + } + } + } + accu_var: @result + accu_init: { + LIST [20] { + elements: { + } + } + } + loop_condition: { + CONSTANT [21] { value: true } + } + loop_step: { + CALL [26] { + function: _?_:_ + args: { + CALL [16] { + function: _&&_ + args: { + CALL [14] { + function: _==_ + args: { + IDENT [13] { + name: x + } + IDENT [15] { + name: y + } + } + } + CALL [18] { + function: _<_ + args: { + IDENT [17] { + name: i + } + IDENT [19] { + name: y + } + } + } + } + } + CALL [24] { + function: _+_ + args: { + IDENT [22] { + name: @result + } + LIST [23] { + elements: { + IDENT [12] { + name: x + } + } + } + } + } + IDENT [25] { + name: @result + } + } + } + } + result: { + IDENT [27] { + name: @result + } + } + } + } + } + } + } + } + result: { + IDENT [34] { + name: @result + } + } + } + LIST [37] { + elements: { + LIST [38] { + elements: { + CONSTANT [39] { value: 1 } + } + } + LIST [40] { + elements: { + CONSTANT [41] { value: 2 } + } + } + } + } + } +} +Test case: ADJACENT_NESTED_MACROS +Source: [1,2,3].map(i, [1, 2, 3].map(i, i + 1)) == [1,2,3].map(j, [1, 2, 3].map(j, j + 1)) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + COMPREHENSION [3] { + iter_var: @it:1:0 + iter_range: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + CONSTANT [6] { value: 2 } + CONSTANT [7] { value: 3 } + } + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [8] { + elements: { + } + } + } + loop_condition: { + CONSTANT [9] { value: true } + } + loop_step: { + CALL [10] { + function: _+_ + args: { + IDENT [11] { + name: @ac:1:0 + } + LIST [12] { + elements: { + COMPREHENSION [13] { + iter_var: @it:0:0 + iter_range: { + LIST [14] { + elements: { + CONSTANT [15] { value: 1 } + CONSTANT [16] { value: 2 } + CONSTANT [17] { value: 3 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [18] { + elements: { + } + } } loop_condition: { - CONSTANT [23] { value: true } + CONSTANT [19] { value: true } } loop_step: { - CALL [24] { - function: _?_:_ + CALL [20] { + function: _+_ args: { - CALL [25] { - function: _==_ - args: { - IDENT [26] { - name: @c1:0 - } - IDENT [27] { - name: @c0:0 - } - } + IDENT [21] { + name: @ac:0:0 } - CALL [28] { - function: _+_ - args: { - IDENT [29] { - name: @x1:0 - } - CREATE_LIST [30] { - elements: { - IDENT [31] { - name: @c1:0 + LIST [22] { + elements: { + CALL [23] { + function: _+_ + args: { + IDENT [24] { + name: @it:0:0 } + CONSTANT [25] { value: 1 } } } } } - IDENT [32] { - name: @x1:0 - } } } } result: { - IDENT [33] { - name: @x1:0 + IDENT [26] { + name: @ac:0:0 } } } @@ -1903,102 +2844,220 @@ CALL [1] { } } result: { - IDENT [34] { - name: @x0:0 + IDENT [27] { + name: @ac:1:0 } } } } } - CALL [35] { + CALL [28] { function: _==_ args: { - IDENT [36] { - name: @index1 + IDENT [29] { + name: @index0 } - IDENT [37] { + IDENT [30] { name: @index0 } } } } } -Test case: INCLUSION_LIST -Source: 1 in [1,2,3] && 2 in [1,2,3] && 3 in [3, [1,2,3]] && 1 in [1,2,3] +Test case: ADJACENT_NESTED_MACROS_COMP_V2 +Source: [1,2,3].transformMap(i, x, [1, 2, 3].transformMap(i, x, i + x + 1)) == [1,2,3].transformMap(j, y, [1, 2, 3].transformMap(j, y, j + y + 1)) =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - CONSTANT [5] { value: 2 } - CONSTANT [6] { value: 3 } + COMPREHENSION [3] { + iter_var: @it:1:0 + iter_var2: @it2:1:0 + iter_range: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + CONSTANT [6] { value: 2 } + CONSTANT [7] { value: 3 } + } + } + } + accu_var: @ac:1:0 + accu_init: { + MAP [8] { + + } + } + loop_condition: { + CONSTANT [9] { value: true } + } + loop_step: { + CALL [10] { + function: cel.@mapInsert + args: { + IDENT [11] { + name: @ac:1:0 + } + IDENT [12] { + name: @it:1:0 + } + COMPREHENSION [13] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + LIST [14] { + elements: { + CONSTANT [15] { value: 1 } + CONSTANT [16] { value: 2 } + CONSTANT [17] { value: 3 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + MAP [18] { + + } + } + loop_condition: { + CONSTANT [19] { value: true } + } + loop_step: { + CALL [20] { + function: cel.@mapInsert + args: { + IDENT [21] { + name: @ac:0:0 + } + IDENT [22] { + name: @it:0:0 + } + CALL [23] { + function: _+_ + args: { + CALL [24] { + function: _+_ + args: { + IDENT [25] { + name: @it:0:0 + } + IDENT [26] { + name: @it2:0:0 + } + } + } + CONSTANT [27] { value: 1 } + } + } + } + } + } + result: { + IDENT [28] { + name: @ac:0:0 + } + } + } + } + } + } + result: { + IDENT [29] { + name: @ac:1:0 + } } } - CALL [7] { + } + } + CALL [30] { + function: _==_ + args: { + IDENT [31] { + name: @index0 + } + IDENT [32] { + name: @index0 + } + } + } + } +} +Test case: INCLUSION_LIST +Source: 1 in [1,2,3] && 2 in [1,2,3] && 3 in [3, [1,2,3]] && 1 in [1,2,3] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + CALL [3] { function: @in args: { - CONSTANT [8] { value: 1 } - IDENT [9] { - name: @index0 + CONSTANT [4] { value: 1 } + LIST [5] { + elements: { + CONSTANT [6] { value: 1 } + CONSTANT [7] { value: 2 } + CONSTANT [8] { value: 3 } + } } } } - CALL [10] { + LIST [9] { + elements: { + CONSTANT [10] { value: 1 } + CONSTANT [11] { value: 2 } + CONSTANT [12] { value: 3 } + } + } + } + } + CALL [13] { + function: _&&_ + args: { + CALL [14] { function: _&&_ args: { - CALL [11] { + IDENT [15] { + name: @index0 + } + CALL [16] { function: @in args: { - CONSTANT [12] { value: 3 } - CREATE_LIST [13] { - elements: { - CONSTANT [14] { value: 3 } - IDENT [15] { - name: @index0 - } - } + CONSTANT [17] { value: 2 } + IDENT [18] { + name: @index1 } } } - IDENT [16] { - name: @index1 - } } } - CALL [17] { + CALL [19] { function: _&&_ args: { - IDENT [18] { - name: @index1 - } - CALL [19] { + CALL [20] { function: @in args: { - CONSTANT [20] { value: 2 } - IDENT [21] { - name: @index0 + CONSTANT [21] { value: 3 } + LIST [22] { + elements: { + CONSTANT [23] { value: 3 } + IDENT [24] { + name: @index1 + } + } } } } + IDENT [25] { + name: @index0 + } } } } } - CALL [22] { - function: _&&_ - args: { - IDENT [23] { - name: @index3 - } - IDENT [24] { - name: @index2 - } - } - } } } Test case: INCLUSION_MAP @@ -2007,9 +3066,9 @@ Source: 2 in {'a': 1, 2: {true: false}, 3: {true: false}} CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_MAP [3] { + MAP [3] { MAP_ENTRY [4] { key: { CONSTANT [5] { value: true } @@ -2019,31 +3078,37 @@ CALL [1] { } } } - CREATE_MAP [7] { - MAP_ENTRY [8] { + } + } + CALL [7] { + function: @in + args: { + CONSTANT [8] { value: 2 } + MAP [9] { + MAP_ENTRY [10] { key: { - CONSTANT [9] { value: "a" } + CONSTANT [11] { value: "a" } } value: { - CONSTANT [10] { value: 1 } + CONSTANT [12] { value: 1 } } } - MAP_ENTRY [11] { + MAP_ENTRY [13] { key: { - CONSTANT [12] { value: 2 } + CONSTANT [14] { value: 2 } } value: { - IDENT [13] { + IDENT [15] { name: @index0 } } } - MAP_ENTRY [14] { + MAP_ENTRY [16] { key: { - CONSTANT [15] { value: 3 } + CONSTANT [17] { value: 3 } } value: { - IDENT [16] { + IDENT [18] { name: @index0 } } @@ -2051,111 +3116,231 @@ CALL [1] { } } } - CALL [17] { - function: @in + } +} +Test case: MACRO_ITER_VAR_NOT_REFERENCED +Source: [1,2].map(i, [1, 2].map(i, [3,4])) == [[[3, 4], [3, 4]], [[3, 4], [3, 4]]] +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + LIST [4] { + elements: { + CONSTANT [5] { value: 3 } + CONSTANT [6] { value: 4 } + } + } + LIST [7] { + elements: { + CONSTANT [8] { value: 3 } + CONSTANT [9] { value: 4 } + } + } + } + } + LIST [10] { + elements: { + CONSTANT [11] { value: 1 } + CONSTANT [12] { value: 2 } + } + } + } + } + CALL [13] { + function: _==_ args: { - CONSTANT [18] { value: 2 } - IDENT [19] { - name: @index1 + COMPREHENSION [14] { + iter_var: @it:1:0 + iter_range: { + IDENT [15] { + name: @index1 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [16] { + elements: { + } + } + } + loop_condition: { + CONSTANT [17] { value: true } + } + loop_step: { + CALL [18] { + function: _+_ + args: { + IDENT [19] { + name: @ac:1:0 + } + LIST [20] { + elements: { + COMPREHENSION [21] { + iter_var: @it:0:0 + iter_range: { + IDENT [22] { + name: @index1 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [23] { + elements: { + } + } + } + loop_condition: { + CONSTANT [24] { value: true } + } + loop_step: { + CALL [25] { + function: _+_ + args: { + IDENT [26] { + name: @ac:0:0 + } + LIST [27] { + elements: { + LIST [28] { + elements: { + CONSTANT [29] { value: 3 } + CONSTANT [30] { value: 4 } + } + } + } + } + } + } + } + result: { + IDENT [31] { + name: @ac:0:0 + } + } + } + } + } + } + } + } + result: { + IDENT [32] { + name: @ac:1:0 + } + } + } + LIST [33] { + elements: { + IDENT [34] { + name: @index0 + } + IDENT [35] { + name: @index0 + } + } } } } } } -Test case: MACRO_ITER_VAR_NOT_REFERENCED -Source: [1,2].map(i, [1, 2].map(i, [3,4])) == [[[3, 4], [3, 4]], [[3, 4], [3, 4]]] +Test case: MACRO_ITER_VAR2_NOT_REFERENCED +Source: [1,2].transformList(i, v, [1, 2].transformList(i, v, [3,4])) == [[[3, 4], [3, 4]], [[3, 4], [3, 4]]] =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_LIST [3] { - elements: { - CONSTANT [4] { value: 1 } - CONSTANT [5] { value: 2 } - } - } - CREATE_LIST [6] { - elements: { - CONSTANT [7] { value: 3 } - CONSTANT [8] { value: 4 } - } - } - CREATE_LIST [9] { + LIST [3] { elements: { - IDENT [10] { - name: @index1 + LIST [4] { + elements: { + CONSTANT [5] { value: 3 } + CONSTANT [6] { value: 4 } + } } - IDENT [11] { - name: @index1 + LIST [7] { + elements: { + CONSTANT [8] { value: 3 } + CONSTANT [9] { value: 4 } + } } } } - CREATE_LIST [12] { + LIST [10] { elements: { - IDENT [13] { - name: @index2 - } - IDENT [14] { - name: @index2 - } + CONSTANT [11] { value: 1 } + CONSTANT [12] { value: 2 } } } - COMPREHENSION [15] { - iter_var: @c0:0 + } + } + CALL [13] { + function: _==_ + args: { + COMPREHENSION [14] { + iter_var: @it:1:0 + iter_var2: @it2:1:0 iter_range: { - IDENT [16] { - name: @index0 + IDENT [15] { + name: @index1 } } - accu_var: @x0:0 + accu_var: @ac:1:0 accu_init: { - CREATE_LIST [17] { + LIST [16] { elements: { } } } loop_condition: { - CONSTANT [18] { value: true } + CONSTANT [17] { value: true } } loop_step: { - CALL [19] { + CALL [18] { function: _+_ args: { - IDENT [20] { - name: @x0:0 + IDENT [19] { + name: @ac:1:0 } - CREATE_LIST [21] { + LIST [20] { elements: { - COMPREHENSION [22] { - iter_var: @c1:0 + COMPREHENSION [21] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 iter_range: { - IDENT [23] { - name: @index0 + IDENT [22] { + name: @index1 } } - accu_var: @x1:0 + accu_var: @ac:0:0 accu_init: { - CREATE_LIST [24] { + LIST [23] { elements: { } } } loop_condition: { - CONSTANT [25] { value: true } + CONSTANT [24] { value: true } } loop_step: { - CALL [26] { + CALL [25] { function: _+_ args: { - IDENT [27] { - name: @x1:0 + IDENT [26] { + name: @ac:0:0 } - CREATE_LIST [28] { + LIST [27] { elements: { - IDENT [29] { - name: @index1 + LIST [28] { + elements: { + CONSTANT [29] { value: 3 } + CONSTANT [30] { value: 4 } + } } } } @@ -2163,8 +3348,8 @@ CALL [1] { } } result: { - IDENT [30] { - name: @x1:0 + IDENT [31] { + name: @ac:0:0 } } } @@ -2174,21 +3359,20 @@ CALL [1] { } } result: { - IDENT [31] { - name: @x0:0 + IDENT [32] { + name: @ac:1:0 } } } - } - } - CALL [32] { - function: _==_ - args: { - IDENT [33] { - name: @index4 - } - IDENT [34] { - name: @index3 + LIST [33] { + elements: { + IDENT [34] { + name: @index0 + } + IDENT [35] { + name: @index0 + } + } } } } @@ -2200,59 +3384,324 @@ Source: [x - 1 > 3 ? x - 1 : 5].exists(x, x - 1 > 3) || x - 1 > 3 CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { - function: _-_ - args: { - IDENT [4] { - name: x - } - CONSTANT [5] { value: 1 } - } - } - CALL [6] { function: _>_ args: { - IDENT [7] { - name: @index0 + CALL [4] { + function: _-_ + args: { + IDENT [5] { + name: x + } + CONSTANT [6] { value: 1 } + } } - CONSTANT [8] { value: 3 } + CONSTANT [7] { value: 3 } } } + } + } + CALL [8] { + function: _||_ + args: { COMPREHENSION [9] { - iter_var: @c0:0 + iter_var: @it:0:0 iter_range: { - CREATE_LIST [10] { + LIST [10] { elements: { CALL [11] { function: _?_:_ args: { IDENT [12] { - name: @index1 + name: @index0 } - IDENT [13] { + CALL [13] { + function: _-_ + args: { + IDENT [14] { + name: x + } + CONSTANT [15] { value: 1 } + } + } + CONSTANT [16] { value: 5 } + } + } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [17] { value: false } + } + loop_condition: { + CALL [18] { + function: @not_strictly_false + args: { + CALL [19] { + function: !_ + args: { + IDENT [20] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + CALL [21] { + function: _||_ + args: { + IDENT [22] { + name: @ac:0:0 + } + CALL [23] { + function: _>_ + args: { + CALL [24] { + function: _-_ + args: { + IDENT [25] { + name: @it:0:0 + } + CONSTANT [26] { value: 1 } + } + } + CONSTANT [27] { value: 3 } + } + } + } + } + } + result: { + IDENT [28] { + name: @ac:0:0 + } + } + } + IDENT [29] { + name: @index0 + } + } + } + } +} +Test case: MACRO_SHADOWED_VARIABLE_2 +Source: ["foo", "bar"].map(x, [x + x, x + x]).map(x, [x + x, x + x]) +=====> +COMPREHENSION [35] { + iter_var: x + iter_range: { + COMPREHENSION [19] { + iter_var: x + iter_range: { + LIST [1] { + elements: { + CONSTANT [2] { value: "foo" } + CONSTANT [3] { value: "bar" } + } + } + } + accu_var: @result + accu_init: { + LIST [13] { + elements: { + } + } + } + loop_condition: { + CONSTANT [14] { value: true } + } + loop_step: { + CALL [17] { + function: _+_ + args: { + IDENT [15] { + name: @result + } + LIST [16] { + elements: { + LIST [6] { + elements: { + CALL [8] { + function: _+_ + args: { + IDENT [7] { + name: x + } + IDENT [9] { + name: x + } + } + } + CALL [11] { + function: _+_ + args: { + IDENT [10] { + name: x + } + IDENT [12] { + name: x + } + } + } + } + } + } + } + } + } + } + result: { + IDENT [18] { + name: @result + } + } + } + } + accu_var: @result + accu_init: { + LIST [29] { + elements: { + } + } + } + loop_condition: { + CONSTANT [30] { value: true } + } + loop_step: { + CALL [33] { + function: _+_ + args: { + IDENT [31] { + name: @result + } + LIST [32] { + elements: { + LIST [22] { + elements: { + CALL [24] { + function: _+_ + args: { + IDENT [23] { + name: x + } + IDENT [25] { + name: x + } + } + } + CALL [27] { + function: _+_ + args: { + IDENT [26] { + name: x + } + IDENT [28] { + name: x + } + } + } + } + } + } + } + } + } + } + result: { + IDENT [34] { + name: @result + } + } +} +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 +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + CALL [3] { + function: _>_ + args: { + CALL [4] { + function: _-_ + args: { + CALL [5] { + function: _-_ + args: { + IDENT [6] { + name: x + } + IDENT [7] { + name: y + } + } + } + CONSTANT [8] { value: 1 } + } + } + CONSTANT [9] { value: 3 } + } + } + } + } + CALL [10] { + function: _||_ + args: { + COMPREHENSION [11] { + iter_var: @it:0:0 + iter_var2: @it2:0:0 + iter_range: { + LIST [12] { + elements: { + CALL [13] { + function: _?_:_ + args: { + IDENT [14] { name: @index0 } - CONSTANT [14] { value: 5 } + CALL [15] { + function: _-_ + args: { + CALL [16] { + function: _-_ + args: { + IDENT [17] { + name: x + } + IDENT [18] { + name: y + } + } + } + CONSTANT [19] { value: 1 } + } + } + CONSTANT [20] { value: 5 } } } } } } - accu_var: @x0:0 + accu_var: @ac:0:0 accu_init: { - CONSTANT [15] { value: false } + CONSTANT [21] { value: false } } loop_condition: { - CALL [16] { + CALL [22] { function: @not_strictly_false args: { - CALL [17] { + CALL [23] { function: !_ args: { - IDENT [18] { - name: @x0:0 + IDENT [24] { + name: @ac:0:0 } } } @@ -2260,96 +3709,109 @@ CALL [1] { } } loop_step: { - CALL [19] { + CALL [25] { function: _||_ args: { - IDENT [20] { - name: @x0:0 + IDENT [26] { + name: @ac:0:0 } - CALL [21] { + CALL [27] { function: _>_ args: { - CALL [22] { + CALL [28] { function: _-_ args: { - IDENT [23] { - name: @c0:0 + CALL [29] { + function: _-_ + args: { + IDENT [30] { + name: @it:0:0 + } + IDENT [31] { + name: @it2:0:0 + } + } } - CONSTANT [24] { value: 1 } + CONSTANT [32] { value: 1 } } } - CONSTANT [25] { value: 3 } + CONSTANT [33] { value: 3 } } } } } } result: { - IDENT [26] { - name: @x0:0 + IDENT [34] { + name: @ac:0:0 } } } - } - } - CALL [27] { - function: _||_ - args: { - IDENT [28] { - name: @index2 - } - IDENT [29] { - name: @index1 + IDENT [35] { + name: @index0 } } } } } -Test case: MACRO_SHADOWED_VARIABLE_2 -Source: ["foo", "bar"].map(x, [x + x, x + x]).map(x, [x + x, x + x]) +Test case: MACRO_SHADOWED_VARIABLE_COMP_V2_2 +Source: ["foo", "bar"].transformMap(x, y, [x + x, y + y]).transformMap(x, y, [x + x, y + y]) =====> -CALL [1] { - function: cel.@block - args: { - CREATE_LIST [2] { - elements: { - CALL [3] { - function: _+_ - args: { - IDENT [4] { - name: @c1:0 - } - IDENT [5] { - name: @c1:0 - } +COMPREHENSION [35] { + iter_var: x + iter_var2: y + iter_range: { + COMPREHENSION [19] { + iter_var: x + iter_var2: y + iter_range: { + LIST [1] { + elements: { + CONSTANT [2] { value: "foo" } + CONSTANT [3] { value: "bar" } } } - CALL [6] { - function: _+_ - args: { - IDENT [7] { - name: @c0:0 - } - IDENT [8] { - name: @c0:0 - } - } + } + accu_var: @result + accu_init: { + MAP [14] { + } - CALL [9] { - function: _+_ + } + loop_condition: { + CONSTANT [15] { value: true } + } + loop_step: { + CALL [17] { + function: cel.@mapInsert args: { - IDENT [10] { - name: @x0:0 + IDENT [16] { + name: @result + } + IDENT [5] { + name: x } - CREATE_LIST [11] { + LIST [7] { elements: { - CREATE_LIST [12] { - elements: { - IDENT [13] { - name: @index1 + CALL [9] { + function: _+_ + args: { + IDENT [8] { + name: x } - IDENT [14] { - name: @index1 + IDENT [10] { + name: x + } + } + } + CALL [12] { + function: _+_ + args: { + IDENT [11] { + name: y + } + IDENT [13] { + name: y } } } @@ -2357,85 +3819,65 @@ CALL [1] { } } } - COMPREHENSION [15] { - iter_var: @c1:0 - iter_range: { - CREATE_LIST [16] { - elements: { - CONSTANT [17] { value: "foo" } - CONSTANT [18] { value: "bar" } - } - } - } - accu_var: @x1:0 - accu_init: { - CREATE_LIST [19] { - elements: { + } + result: { + IDENT [18] { + name: @result + } + } + } + } + accu_var: @result + accu_init: { + MAP [30] { + + } + } + loop_condition: { + CONSTANT [31] { value: true } + } + loop_step: { + CALL [33] { + function: cel.@mapInsert + args: { + IDENT [32] { + name: @result + } + IDENT [21] { + name: x + } + LIST [23] { + elements: { + CALL [25] { + function: _+_ + args: { + IDENT [24] { + name: x + } + IDENT [26] { + name: x + } } } - } - loop_condition: { - CONSTANT [20] { value: true } - } - loop_step: { - CALL [21] { + CALL [28] { function: _+_ args: { - IDENT [22] { - name: @x1:0 + IDENT [27] { + name: y } - CREATE_LIST [23] { - elements: { - CREATE_LIST [24] { - elements: { - IDENT [25] { - name: @index0 - } - IDENT [26] { - name: @index0 - } - } - } - } + IDENT [29] { + name: y } } } } - result: { - IDENT [27] { - name: @x1:0 - } - } } } } - COMPREHENSION [28] { - iter_var: @c0:0 - iter_range: { - IDENT [29] { - name: @index3 - } - } - accu_var: @x0:0 - accu_init: { - CREATE_LIST [30] { - elements: { - } - } - } - loop_condition: { - CONSTANT [31] { value: true } - } - loop_step: { - IDENT [32] { - name: @index2 - } - } - result: { - IDENT [33] { - name: @x0:0 - } - } + } + result: { + IDENT [34] { + name: @result } } } @@ -2445,9 +3887,9 @@ Source: has({'a': true}.a) && {'a':true}['a'] CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_MAP [3] { + MAP [3] { MAP_ENTRY [4] { key: { CONSTANT [5] { value: "a" } @@ -2457,27 +3899,59 @@ CALL [1] { } } } - CALL [7] { + } + } + CALL [7] { + function: _&&_ + args: { + SELECT [8] { + IDENT [9] { + name: @index0 + }.a~presence_test + } + CALL [10] { function: _[_] args: { - IDENT [8] { + IDENT [11] { name: @index0 } - CONSTANT [9] { value: "a" } + CONSTANT [12] { value: "a" } } } } } - CALL [10] { + } +} +Test case: PRESENCE_TEST_2 +Source: has({'a': true}.a) && has({'a': true}.a) +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + SELECT [3] { + MAP [4] { + MAP_ENTRY [5] { + key: { + CONSTANT [6] { value: "a" } + } + value: { + CONSTANT [7] { value: true } + } + } + }.a~presence_test + } + } + } + CALL [8] { function: _&&_ args: { - SELECT [11] { - IDENT [12] { - name: @index0 - }.a~presence_test + IDENT [9] { + name: @index0 } - IDENT [13] { - name: @index1 + IDENT [10] { + name: @index0 } } } @@ -2489,40 +3963,37 @@ Source: (has(msg.oneof_type.payload) ? msg.oneof_type.payload.single_int64 : 0) CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { IDENT [4] { name: msg }.oneof_type } - CALL [5] { + } + } + CALL [5] { + function: _==_ + args: { + CALL [6] { function: _?_:_ args: { - SELECT [6] { - IDENT [7] { + SELECT [7] { + IDENT [8] { name: @index0 }.payload~presence_test } - SELECT [8] { - SELECT [9] { - IDENT [10] { + SELECT [9] { + SELECT [10] { + IDENT [11] { name: @index0 }.payload }.single_int64 } - CONSTANT [11] { value: 0 } + CONSTANT [12] { value: 0 } } } - } - } - CALL [12] { - function: _==_ - args: { - IDENT [13] { - name: @index1 - } - CONSTANT [14] { value: 10 } + CONSTANT [13] { value: 10 } } } } @@ -2533,54 +4004,47 @@ Source: (has(msg.oneof_type.payload) ? msg.oneof_type.payload.single_int64 : msg CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload }.single_int64 } - CALL [9] { + } + } + CALL [7] { + function: _==_ + args: { + CALL [8] { function: _?_:_ args: { - SELECT [10] { - IDENT [11] { - name: @index0 + SELECT [9] { + SELECT [10] { + IDENT [11] { + name: msg + }.oneof_type }.payload~presence_test } IDENT [12] { - name: @index2 + name: @index0 } CALL [13] { function: _*_ args: { IDENT [14] { - name: @index2 + name: @index0 } CONSTANT [15] { value: 0 } } } } } - } - } - CALL [16] { - function: _==_ - args: { - IDENT [17] { - name: @index3 - } - CONSTANT [18] { value: 10 } + CONSTANT [16] { value: 10 } } } } @@ -2591,54 +4055,49 @@ Source: (has(msg.oneof_type.payload.single_int64) ? msg.oneof_type.payload.singl CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload }.single_int64 } - CALL [9] { + } + } + CALL [7] { + function: _==_ + args: { + CALL [8] { function: _?_:_ args: { - SELECT [10] { - IDENT [11] { - name: @index1 + SELECT [9] { + SELECT [10] { + SELECT [11] { + IDENT [12] { + name: msg + }.oneof_type + }.payload }.single_int64~presence_test } - IDENT [12] { - name: @index2 + IDENT [13] { + name: @index0 } - CALL [13] { + CALL [14] { function: _*_ args: { - IDENT [14] { - name: @index2 + IDENT [15] { + name: @index0 } - CONSTANT [15] { value: 0 } + CONSTANT [16] { value: 0 } } } } } - } - } - CALL [16] { - function: _==_ - args: { - IDENT [17] { - name: @index3 - } - CONSTANT [18] { value: 10 } + CONSTANT [17] { value: 10 } } } } @@ -2649,91 +4108,88 @@ Source: (has(msg.oneof_type) && has(msg.oneof_type.payload) && has(msg.oneof_typ CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.map_string_string } SELECT [7] { - IDENT [8] { - name: @index1 - }.map_string_string + SELECT [8] { + IDENT [9] { + name: msg + }.oneof_type + }.payload } - CALL [9] { - function: _?_:_ + } + } + CALL [10] { + function: _?_:_ + args: { + CALL [11] { + function: _&&_ args: { - CALL [10] { + CALL [12] { function: _&&_ args: { - SELECT [11] { - IDENT [12] { - name: @index1 - }.map_string_string~presence_test - } SELECT [13] { IDENT [14] { - name: @index2 - }.key~presence_test + name: msg + }.oneof_type~presence_test } - } - } - CALL [15] { - function: _==_ - args: { - SELECT [16] { - IDENT [17] { - name: @index2 - }.key + SELECT [15] { + SELECT [16] { + IDENT [17] { + name: msg + }.oneof_type + }.payload~presence_test } - CONSTANT [18] { value: "A" } } } - CONSTANT [19] { value: false } + SELECT [18] { + IDENT [19] { + name: @index1 + }.single_int64~presence_test + } } } CALL [20] { - function: _&&_ + function: _?_:_ args: { CALL [21] { function: _&&_ args: { SELECT [22] { IDENT [23] { - name: msg - }.oneof_type~presence_test + name: @index1 + }.map_string_string~presence_test } SELECT [24] { IDENT [25] { name: @index0 - }.payload~presence_test + }.key~presence_test } } } - SELECT [26] { - IDENT [27] { - name: @index1 - }.single_int64~presence_test + CALL [26] { + function: _==_ + args: { + SELECT [27] { + IDENT [28] { + name: @index0 + }.key + } + CONSTANT [29] { value: "A" } + } } + CONSTANT [30] { value: false } } } - } - } - CALL [28] { - function: _?_:_ - args: { - IDENT [29] { - name: @index4 - } - IDENT [30] { - name: @index3 - } CONSTANT [31] { value: false } } } @@ -2745,46 +4201,51 @@ Source: [10, ?optional.none(), [?optional.none(), ?opt_x], [?optional.none(), ?o CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CALL [3] { - function: optional.none - args: { - } - } - CREATE_LIST [4] { + LIST [3] { elements: { - IDENT [5] { - name: @index0 + CALL [4] { + function: optional.none + args: { + } } - IDENT [6] { + IDENT [5] { name: opt_x } } optional_indices: [0, 1] } - CREATE_LIST [7] { + LIST [6] { elements: { - CONSTANT [8] { value: 5 } + CONSTANT [7] { value: 5 } } } - CREATE_LIST [9] { + } + } + CALL [8] { + function: _==_ + args: { + LIST [9] { elements: { CONSTANT [10] { value: 10 } - IDENT [11] { - name: @index2 + CALL [11] { + function: optional.none + args: { + } } IDENT [12] { - name: @index2 + name: @index0 + } + IDENT [13] { + name: @index0 } } + optional_indices: [0] } - CREATE_LIST [13] { + LIST [14] { elements: { - CONSTANT [14] { value: 10 } - IDENT [15] { - name: @index0 - } + CONSTANT [15] { value: 10 } IDENT [16] { name: @index1 } @@ -2792,18 +4253,6 @@ CALL [1] { name: @index1 } } - optional_indices: [0] - } - } - } - CALL [18] { - function: _==_ - args: { - IDENT [19] { - name: @index4 - } - IDENT [20] { - name: @index3 } } } @@ -2815,56 +4264,47 @@ Source: {?'hello': optional.of('hello')}['hello'] + {?'hello': optional.of('hell CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { - function: optional.of - args: { - CONSTANT [4] { value: "hello" } - } - } - CREATE_MAP [5] { - MAP_ENTRY [6] { - key: { - CONSTANT [7] { value: "hello" } - } - optional_entry: true - value: { - IDENT [8] { - name: @index0 - } - } - } - } - CALL [9] { function: _[_] args: { - IDENT [10] { - name: @index1 + MAP [4] { + MAP_ENTRY [5] { + key: { + CONSTANT [6] { value: "hello" } + } + optional_entry: true + value: { + CALL [7] { + function: optional.of + args: { + CONSTANT [8] { value: "hello" } + } + } + } + } } - CONSTANT [11] { value: "hello" } + CONSTANT [9] { value: "hello" } } } - CALL [12] { + } + } + CALL [10] { + function: _==_ + args: { + CALL [11] { function: _+_ args: { - IDENT [13] { - name: @index2 + IDENT [12] { + name: @index0 } - IDENT [14] { - name: @index2 + IDENT [13] { + name: @index0 } } } - } - } - CALL [15] { - function: _==_ - args: { - IDENT [16] { - name: @index3 - } - CONSTANT [17] { value: "hellohello" } + CONSTANT [14] { value: "hellohello" } } } } @@ -2875,9 +4315,9 @@ Source: {?'key': optional.of('test')}[?'bogus'].or({'key': 'test'}[?'bogus']).or CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CREATE_MAP [3] { + MAP [3] { MAP_ENTRY [4] { key: { CONSTANT [5] { value: "key" } @@ -2887,69 +4327,66 @@ CALL [1] { } } } - CALL [7] { + } + } + CALL [7] { + function: _==_ + args: { + CALL [8] { function: orValue target: { - CALL [8] { + CALL [9] { function: or target: { - CALL [9] { + CALL [10] { function: _[?_] args: { - CREATE_MAP [10] { - MAP_ENTRY [11] { + MAP [11] { + MAP_ENTRY [12] { key: { - CONSTANT [12] { value: "key" } + CONSTANT [13] { value: "key" } } optional_entry: true value: { - CALL [13] { + CALL [14] { function: optional.of args: { - CONSTANT [14] { value: "test" } + CONSTANT [15] { value: "test" } } } } } } - CONSTANT [15] { value: "bogus" } + CONSTANT [16] { value: "bogus" } } } } args: { - CALL [16] { + CALL [17] { function: _[?_] args: { - IDENT [17] { + IDENT [18] { name: @index0 } - CONSTANT [18] { value: "bogus" } + CONSTANT [19] { value: "bogus" } } } } } } args: { - CALL [19] { + CALL [20] { function: _[_] args: { - IDENT [20] { + IDENT [21] { name: @index0 } - CONSTANT [21] { value: "key" } + CONSTANT [22] { value: "key" } } } } } - } - } - CALL [22] { - function: _==_ - args: { - IDENT [23] { - name: @index1 - } - CONSTANT [24] { value: "test" } + CONSTANT [23] { value: "test" } } } } @@ -2960,146 +4397,69 @@ Source: TestAllTypes{?single_int64: optional.ofNonZeroValue(1), ?single_int32: o CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - CALL [3] { - function: optional.ofNonZeroValue - args: { - CONSTANT [4] { value: 1 } - } - } - CALL [5] { - function: optional.of - args: { - CONSTANT [6] { value: 4 } - } - } - CREATE_STRUCT [7] { - name: TestAllTypes + STRUCT [3] { + name: cel.expr.conformance.proto3.TestAllTypes entries: { - ENTRY [8] { + ENTRY [4] { field_key: single_int64 optional_entry: true value: { - IDENT [9] { - name: @index0 + CALL [5] { + function: optional.ofNonZeroValue + args: { + CONSTANT [6] { value: 1 } + } } } } - ENTRY [10] { + ENTRY [7] { field_key: single_int32 - optional_entry: true - value: { - IDENT [11] { - name: @index1 - } - } - } - } - } - CALL [12] { - function: _+_ - args: { - SELECT [13] { - IDENT [14] { - name: @index2 - }.single_int32 - } - SELECT [15] { - IDENT [16] { - name: @index2 - }.single_int64 - } - } - } - } - } - CALL [17] { - function: _==_ - args: { - IDENT [18] { - name: @index3 - } - CONSTANT [19] { value: 5 } - } - } - } -} -Test case: CALL -Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('h' + 'e' + 'l' + 'l' + 'o') -=====> -CALL [1] { - function: cel.@block - args: { - CREATE_LIST [2] { - elements: { - CALL [3] { - function: _+_ - args: { - CONSTANT [4] { value: "h" } - CONSTANT [5] { value: "e" } - } - } - CALL [6] { - function: _+_ - args: { - IDENT [7] { - name: @index0 - } - CONSTANT [8] { value: "l" } - } - } - CALL [9] { - function: _+_ - args: { - IDENT [10] { - name: @index1 + optional_entry: true + value: { + CALL [8] { + function: optional.of + args: { + CONSTANT [9] { value: 4 } + } + } + } } - CONSTANT [11] { value: "l" } } } - CALL [12] { + } + } + CALL [10] { + function: _==_ + args: { + CALL [11] { function: _+_ args: { - IDENT [13] { - name: @index2 + SELECT [12] { + IDENT [13] { + name: @index0 + }.single_int32 } - CONSTANT [14] { value: "o" } - } - } - CALL [15] { - function: _+_ - args: { - IDENT [16] { - name: @index3 + SELECT [14] { + IDENT [15] { + name: @index0 + }.single_int64 } - CONSTANT [17] { value: " world" } } } - } - } - CALL [18] { - function: matches - target: { - IDENT [19] { - name: @index4 - } - } - args: { - IDENT [20] { - name: @index3 - } + CONSTANT [16] { value: 5 } } } } } -Test case: CALL_ARGUMENT_NESTED_NO_COMMON_SUBEXPR -Source: 'hello world'.matches('h' + 'e' + 'l' + 'l' + 'o') +Test case: CALL +Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('h' + 'e' + 'l' + 'l' + 'o') =====> CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { CALL [3] { function: _+_ @@ -3131,147 +4491,164 @@ CALL [1] { CALL [12] { function: matches target: { - CONSTANT [13] { value: "hello world" } + CALL [13] { + function: _+_ + args: { + IDENT [14] { + name: @index0 + } + CONSTANT [15] { value: " world" } + } + } } args: { - IDENT [14] { + IDENT [16] { name: @index0 } } } } } -Test case: CALL_TARGET_NESTED_NO_COMMON_SUBEXPR -Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('hello') +Test case: CALL_ARGUMENT_NESTED_NO_COMMON_SUBEXPR +Source: 'hello world'.matches('h' + 'e' + 'l' + 'l' + 'o') =====> -CALL [1] { - function: cel.@block +CALL [2] { + function: matches + target: { + CONSTANT [1] { value: "hello world" } + } args: { - CREATE_LIST [2] { - elements: { - CALL [3] { + CALL [10] { + function: _+_ + args: { + CALL [8] { function: _+_ args: { - CALL [4] { + CALL [6] { function: _+_ args: { - CALL [5] { + CALL [4] { function: _+_ args: { - CALL [6] { - function: _+_ - args: { - CALL [7] { - function: _+_ - args: { - CONSTANT [8] { value: "h" } - CONSTANT [9] { value: "e" } - } - } - CONSTANT [10] { value: "l" } - } - } - CONSTANT [11] { value: "l" } + CONSTANT [3] { value: "h" } + CONSTANT [5] { value: "e" } } } - CONSTANT [12] { value: "o" } + CONSTANT [7] { value: "l" } } } - CONSTANT [13] { value: " world" } + CONSTANT [9] { value: "l" } } } - } - } - CALL [14] { - function: matches - target: { - IDENT [15] { - name: @index0 - } - } - args: { - CONSTANT [16] { value: "hello" } + CONSTANT [11] { value: "o" } } } } } -Test case: CALL_BOTH_ARGUMENT_TARGET_NESTED_NO_COMMON_SUBEXPR -Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('w' + 'o' + 'r' + 'l' + 'd') +Test case: CALL_TARGET_NESTED_NO_COMMON_SUBEXPR +Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('hello') =====> -CALL [1] { - function: cel.@block - args: { - CREATE_LIST [2] { - elements: { - CALL [3] { +CALL [12] { + function: matches + target: { + CALL [10] { + function: _+_ + args: { + CALL [8] { function: _+_ args: { - CALL [4] { + CALL [6] { function: _+_ args: { - CALL [5] { + CALL [4] { function: _+_ args: { - CALL [6] { + CALL [2] { function: _+_ args: { - CONSTANT [7] { value: "w" } - CONSTANT [8] { value: "o" } + CONSTANT [1] { value: "h" } + CONSTANT [3] { value: "e" } } } - CONSTANT [9] { value: "r" } + CONSTANT [5] { value: "l" } } } - CONSTANT [10] { value: "l" } + CONSTANT [7] { value: "l" } } } - CONSTANT [11] { value: "d" } + CONSTANT [9] { value: "o" } } } - CALL [12] { + CONSTANT [11] { value: " world" } + } + } + } + args: { + CONSTANT [13] { value: "hello" } + } +} +Test case: CALL_BOTH_ARGUMENT_TARGET_NESTED_NO_COMMON_SUBEXPR +Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('w' + 'o' + 'r' + 'l' + 'd') +=====> +CALL [12] { + function: matches + target: { + CALL [10] { + function: _+_ + args: { + CALL [8] { function: _+_ args: { - CALL [13] { + CALL [6] { function: _+_ args: { - CALL [14] { + CALL [4] { function: _+_ args: { - CALL [15] { + CALL [2] { function: _+_ args: { - CALL [16] { - function: _+_ - args: { - CONSTANT [17] { value: "h" } - CONSTANT [18] { value: "e" } - } - } - CONSTANT [19] { value: "l" } + CONSTANT [1] { value: "h" } + CONSTANT [3] { value: "e" } } } - CONSTANT [20] { value: "l" } + CONSTANT [5] { value: "l" } } } - CONSTANT [21] { value: "o" } + CONSTANT [7] { value: "l" } } } - CONSTANT [22] { value: " world" } + CONSTANT [9] { value: "o" } } } + CONSTANT [11] { value: " world" } } } - CALL [23] { - function: matches - target: { - IDENT [24] { - name: @index1 - } - } + } + args: { + CALL [20] { + function: _+_ args: { - IDENT [25] { - name: @index0 + CALL [18] { + function: _+_ + args: { + CALL [16] { + function: _+_ + args: { + CALL [14] { + function: _+_ + args: { + CONSTANT [13] { value: "w" } + CONSTANT [15] { value: "o" } + } + } + CONSTANT [17] { value: "r" } + } + } + CONSTANT [19] { value: "l" } + } } + CONSTANT [21] { value: "d" } } } } @@ -3282,77 +4659,69 @@ Source: non_pure_custom_func(msg.oneof_type.payload.single_int64) + non_pure_cus CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 - }.single_int64 - } - SELECT [9] { - IDENT [10] { - name: msg + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload }.single_int64 } - SELECT [11] { - IDENT [12] { - name: @index1 - }.single_int32 - } } } - CALL [13] { + CALL [7] { function: _+_ args: { - CALL [14] { + CALL [8] { function: _+_ args: { - CALL [15] { + CALL [9] { function: _+_ args: { - CALL [16] { + CALL [10] { function: non_pure_custom_func args: { - IDENT [17] { - name: @index2 + IDENT [11] { + name: @index0 } } } - CALL [18] { + CALL [12] { function: non_pure_custom_func args: { - IDENT [19] { - name: @index4 + SELECT [13] { + SELECT [14] { + SELECT [15] { + IDENT [16] { + name: msg + }.oneof_type + }.payload + }.single_int32 } } } } } - CALL [20] { + CALL [17] { function: non_pure_custom_func args: { - IDENT [21] { - name: @index2 + IDENT [18] { + name: @index0 } } } } } - CALL [22] { + CALL [19] { function: non_pure_custom_func args: { - IDENT [23] { - name: @index3 + SELECT [20] { + IDENT [21] { + name: msg + }.single_int64 } } } @@ -3366,79 +4735,68 @@ Source: pure_custom_func(msg.oneof_type.payload.single_int64) + pure_custom_func CALL [1] { function: cel.@block args: { - CREATE_LIST [2] { + LIST [2] { elements: { - SELECT [3] { - IDENT [4] { - name: msg - }.oneof_type - } - SELECT [5] { - IDENT [6] { - name: @index0 - }.payload - } - SELECT [7] { - IDENT [8] { - name: @index1 - }.single_int64 - } - CALL [9] { - function: pure_custom_func - args: { - IDENT [10] { - name: @index2 - } - } - } - CALL [11] { + CALL [3] { function: pure_custom_func args: { - SELECT [12] { - IDENT [13] { - name: msg + SELECT [4] { + SELECT [5] { + SELECT [6] { + IDENT [7] { + name: msg + }.oneof_type + }.payload }.single_int64 } } } - CALL [14] { + } + } + CALL [8] { + function: _+_ + args: { + CALL [9] { function: _+_ args: { - CALL [15] { + CALL [10] { function: _+_ args: { - IDENT [16] { - name: @index3 + IDENT [11] { + name: @index0 } - CALL [17] { + CALL [12] { function: pure_custom_func args: { - SELECT [18] { - IDENT [19] { - name: @index1 + SELECT [13] { + SELECT [14] { + SELECT [15] { + IDENT [16] { + name: msg + }.oneof_type + }.payload }.single_int32 } } } } } - IDENT [20] { - name: @index3 + IDENT [17] { + name: @index0 } } } - } - } - CALL [21] { - function: _+_ - args: { - IDENT [22] { - name: @index5 - } - IDENT [23] { - name: @index4 + CALL [18] { + function: pure_custom_func + args: { + SELECT [19] { + IDENT [20] { + name: msg + }.single_int64 + } + } } } } } -} +} \ No newline at end of file diff --git a/optimizer/src/test/resources/subexpression_ast_cascaded_binds.baseline b/optimizer/src/test/resources/subexpression_ast_cascaded_binds.baseline index 08e584050..016dc257d 100644 --- a/optimizer/src/test/resources/subexpression_ast_cascaded_binds.baseline +++ b/optimizer/src/test/resources/subexpression_ast_cascaded_binds.baseline @@ -10,7 +10,7 @@ CALL [1] { COMPREHENSION [3] { iter_var: #unused iter_range: { - CREATE_LIST [4] { + LIST [4] { elements: { } } @@ -20,7 +20,7 @@ CALL [1] { CALL [5] { function: size args: { - CREATE_LIST [6] { + LIST [6] { elements: { CONSTANT [7] { value: 1 } CONSTANT [8] { value: 2 } @@ -69,7 +69,7 @@ CALL [1] { COMPREHENSION [3] { iter_var: #unused iter_range: { - CREATE_LIST [4] { + LIST [4] { elements: { } } @@ -79,7 +79,7 @@ CALL [1] { CALL [5] { function: size args: { - CREATE_LIST [6] { + LIST [6] { elements: { CONSTANT [7] { value: 1 } CONSTANT [8] { value: 2 } @@ -131,7 +131,7 @@ CALL [1] { COMPREHENSION [2] { iter_var: #unused iter_range: { - CREATE_LIST [3] { + LIST [3] { elements: { } } @@ -141,7 +141,7 @@ CALL [1] { CALL [4] { function: size args: { - CREATE_LIST [5] { + LIST [5] { elements: { CONSTANT [6] { value: 1 } CONSTANT [7] { value: 2 } @@ -168,7 +168,7 @@ CALL [1] { COMPREHENSION [12] { iter_var: #unused iter_range: { - CREATE_LIST [13] { + LIST [13] { elements: { } } @@ -178,7 +178,7 @@ CALL [1] { CALL [14] { function: size args: { - CREATE_LIST [15] { + LIST [15] { elements: { CONSTANT [16] { value: 0 } } @@ -232,7 +232,7 @@ CALL [1] { COMPREHENSION [2] { iter_var: #unused iter_range: { - CREATE_LIST [3] { + LIST [3] { elements: { } } @@ -242,7 +242,7 @@ CALL [1] { CALL [4] { function: size args: { - CREATE_LIST [5] { + LIST [5] { elements: { CONSTANT [6] { value: 1 } CONSTANT [7] { value: 2 } @@ -270,7 +270,7 @@ CALL [1] { COMPREHENSION [13] { iter_var: #unused iter_range: { - CREATE_LIST [14] { + LIST [14] { elements: { } } @@ -280,7 +280,7 @@ CALL [1] { CALL [15] { function: size args: { - CREATE_LIST [16] { + LIST [16] { elements: { CONSTANT [17] { value: 1 } CONSTANT [18] { value: 2 } @@ -307,7 +307,7 @@ CALL [1] { COMPREHENSION [23] { iter_var: #unused iter_range: { - CREATE_LIST [24] { + LIST [24] { elements: { } } @@ -317,7 +317,7 @@ CALL [1] { CALL [25] { function: size args: { - CREATE_LIST [26] { + LIST [26] { elements: { CONSTANT [27] { value: 0 } } @@ -389,7 +389,7 @@ CALL [1] { COMPREHENSION [2] { iter_var: #unused iter_range: { - CREATE_LIST [3] { + LIST [3] { elements: { } } @@ -435,7 +435,7 @@ CALL [1] { COMPREHENSION [12] { iter_var: #unused iter_range: { - CREATE_LIST [13] { + LIST [13] { elements: { } } @@ -474,7 +474,7 @@ CALL [1] { COMPREHENSION [21] { iter_var: #unused iter_range: { - CREATE_LIST [22] { + LIST [22] { elements: { } } @@ -523,7 +523,7 @@ CALL [1] { COMPREHENSION [32] { iter_var: #unused iter_range: { - CREATE_LIST [33] { + LIST [33] { elements: { } } @@ -659,7 +659,7 @@ CALL [1] { COMPREHENSION [2] { iter_var: #unused iter_range: { - CREATE_LIST [3] { + LIST [3] { elements: { } } @@ -669,7 +669,7 @@ CALL [1] { CALL [4] { function: _[_] args: { - CREATE_MAP [5] { + MAP [5] { MAP_ENTRY [6] { key: { CONSTANT [7] { value: "a" } @@ -722,14 +722,14 @@ Source: {'a': {'b': 1}, 'c': {'b': 1}, 'd': {'e': {'b': 1}}, 'e': {'e': {'b': 1} COMPREHENSION [1] { iter_var: #unused iter_range: { - CREATE_LIST [2] { + LIST [2] { elements: { } } } accu_var: @r0 accu_init: { - CREATE_MAP [3] { + MAP [3] { MAP_ENTRY [4] { key: { CONSTANT [5] { value: "b" } @@ -752,14 +752,14 @@ COMPREHENSION [1] { COMPREHENSION [9] { iter_var: #unused iter_range: { - CREATE_LIST [10] { + LIST [10] { elements: { } } } accu_var: @r1 accu_init: { - CREATE_MAP [11] { + MAP [11] { MAP_ENTRY [12] { key: { CONSTANT [13] { value: "e" } @@ -781,7 +781,7 @@ COMPREHENSION [1] { } } result: { - CREATE_MAP [17] { + MAP [17] { MAP_ENTRY [18] { key: { CONSTANT [19] { value: "a" } @@ -833,14 +833,14 @@ Source: [1, [1,2,3,4], 2, [1,2,3,4], 5, [1,2,3,4], 7, [[1,2], [1,2,3,4]], [1,2]] COMPREHENSION [1] { iter_var: #unused iter_range: { - CREATE_LIST [2] { + LIST [2] { elements: { } } } accu_var: @r0 accu_init: { - CREATE_LIST [3] { + LIST [3] { elements: { CONSTANT [4] { value: 1 } CONSTANT [5] { value: 2 } @@ -861,14 +861,14 @@ COMPREHENSION [1] { COMPREHENSION [10] { iter_var: #unused iter_range: { - CREATE_LIST [11] { + LIST [11] { elements: { } } } accu_var: @r1 accu_init: { - CREATE_LIST [12] { + LIST [12] { elements: { CONSTANT [13] { value: 1 } CONSTANT [14] { value: 2 } @@ -884,7 +884,7 @@ COMPREHENSION [1] { } } result: { - CREATE_LIST [17] { + LIST [17] { elements: { CONSTANT [18] { value: 1 } IDENT [19] { @@ -899,7 +899,7 @@ COMPREHENSION [1] { name: @r0 } CONSTANT [24] { value: 7 } - CREATE_LIST [25] { + LIST [25] { elements: { IDENT [26] { name: @r1 @@ -927,7 +927,7 @@ CALL [1] { COMPREHENSION [2] { iter_var: #unused iter_range: { - CREATE_LIST [3] { + LIST [3] { elements: { } } @@ -974,7 +974,7 @@ CALL [1] { COMPREHENSION [2] { iter_var: #unused iter_range: { - CREATE_LIST [3] { + LIST [3] { elements: { } } @@ -1007,7 +1007,7 @@ CALL [1] { COMPREHENSION [11] { iter_var: #unused iter_range: { - CREATE_LIST [12] { + LIST [12] { elements: { } } @@ -1081,7 +1081,7 @@ Source: true || msg.oneof_type.payload.oneof_type.payload.oneof_type.payload.one COMPREHENSION [1] { iter_var: #unused iter_range: { - CREATE_LIST [2] { + LIST [2] { elements: { } } @@ -1155,7 +1155,7 @@ CALL [1] { COMPREHENSION [2] { iter_var: #unused iter_range: { - CREATE_LIST [3] { + LIST [3] { elements: { } } @@ -1220,7 +1220,7 @@ CALL [1] { COMPREHENSION [2] { iter_var: #unused iter_range: { - CREATE_LIST [3] { + LIST [3] { elements: { } } @@ -1321,7 +1321,7 @@ CALL [1] { COMPREHENSION [2] { iter_var: #unused iter_range: { - CREATE_LIST [3] { + LIST [3] { elements: { } } @@ -1380,7 +1380,7 @@ CALL [1] { COMPREHENSION [5] { iter_var: #unused iter_range: { - CREATE_LIST [6] { + LIST [6] { elements: { } } @@ -1441,7 +1441,7 @@ CALL [1] { COMPREHENSION [2] { iter_var: #unused iter_range: { - CREATE_LIST [3] { + LIST [3] { elements: { } } @@ -1478,7 +1478,7 @@ CALL [1] { COMPREHENSION [12] { iter_var: #unused iter_range: { - CREATE_LIST [13] { + LIST [13] { elements: { } } @@ -1545,68 +1545,303 @@ CALL [1] { COMPREHENSION [2] { iter_var: #unused iter_range: { - CREATE_LIST [3] { + LIST [3] { elements: { } } } accu_var: @r1 accu_init: { - CALL [4] { - function: size + LIST [4] { + elements: { + CONSTANT [5] { value: 2 } + } + } + } + loop_condition: { + CONSTANT [6] { value: false } + } + loop_step: { + IDENT [7] { + name: @r1 + } + } + result: { + CALL [8] { + function: _+_ args: { - CREATE_LIST [5] { - elements: { - COMPREHENSION [6] { - iter_var: @c0:0 + CALL [9] { + function: _+_ + args: { + COMPREHENSION [10] { + iter_var: #unused iter_range: { - CREATE_LIST [7] { + LIST [11] { elements: { - CONSTANT [8] { value: 2 } } } } - accu_var: @x0:0 + accu_var: @r0 accu_init: { - CONSTANT [9] { value: false } + LIST [12] { + elements: { + CONSTANT [13] { value: 1 } + } + } } loop_condition: { - CALL [10] { - function: @not_strictly_false + CONSTANT [14] { value: false } + } + loop_step: { + IDENT [15] { + name: @r0 + } + } + result: { + CALL [16] { + function: _+_ args: { - CALL [11] { - function: !_ + CALL [17] { + function: size args: { - IDENT [12] { - name: @x0:0 + LIST [18] { + elements: { + COMPREHENSION [19] { + iter_var: @it:0 + iter_range: { + IDENT [20] { + name: @r0 + } + } + accu_var: @ac:0 + accu_init: { + CONSTANT [21] { value: false } + } + loop_condition: { + CALL [22] { + function: @not_strictly_false + args: { + CALL [23] { + function: !_ + args: { + IDENT [24] { + name: @ac:0 + } + } + } + } + } + } + loop_step: { + CALL [25] { + function: _||_ + args: { + IDENT [26] { + name: @ac:0 + } + CALL [27] { + function: _>_ + args: { + IDENT [28] { + name: @it:0 + } + CONSTANT [29] { value: 0 } + } + } + } + } + } + result: { + IDENT [30] { + name: @ac:0 + } + } + } + } + } + } + } + CALL [31] { + function: size + args: { + LIST [32] { + elements: { + COMPREHENSION [33] { + iter_var: @it:1 + iter_range: { + IDENT [34] { + name: @r0 + } + } + accu_var: @ac:1 + accu_init: { + CONSTANT [35] { value: false } + } + loop_condition: { + CALL [36] { + function: @not_strictly_false + args: { + CALL [37] { + function: !_ + args: { + IDENT [38] { + name: @ac:1 + } + } + } + } + } + } + loop_step: { + CALL [39] { + function: _||_ + args: { + IDENT [40] { + name: @ac:1 + } + CALL [41] { + function: _>_ + args: { + IDENT [42] { + name: @it:1 + } + CONSTANT [43] { value: 0 } + } + } + } + } + } + result: { + IDENT [44] { + name: @ac:1 + } + } + } + } } } } } } } - loop_step: { - CALL [13] { - function: _||_ - args: { - IDENT [14] { - name: @x0:0 - } - CALL [15] { - function: _>_ - args: { - IDENT [16] { - name: @c0:0 + } + CALL [45] { + function: size + args: { + LIST [46] { + elements: { + COMPREHENSION [47] { + iter_var: @it:2 + iter_range: { + IDENT [48] { + name: @r1 + } + } + accu_var: @ac:2 + accu_init: { + CONSTANT [49] { value: false } + } + loop_condition: { + CALL [50] { + function: @not_strictly_false + args: { + CALL [51] { + function: !_ + args: { + IDENT [52] { + name: @ac:2 + } + } + } + } + } + } + loop_step: { + CALL [53] { + function: _||_ + args: { + IDENT [54] { + name: @ac:2 + } + CALL [55] { + function: _>_ + args: { + IDENT [56] { + name: @it:2 + } + CONSTANT [57] { value: 1 } + } + } + } + } + } + result: { + IDENT [58] { + name: @ac:2 } - CONSTANT [17] { value: 1 } } } } } } - result: { - IDENT [18] { - name: @x0:0 + } + } + } + CALL [59] { + function: size + args: { + LIST [60] { + elements: { + COMPREHENSION [61] { + iter_var: @it:3 + iter_range: { + IDENT [62] { + name: @r1 + } + } + accu_var: @ac:3 + accu_init: { + CONSTANT [63] { value: false } + } + loop_condition: { + CALL [64] { + function: @not_strictly_false + args: { + CALL [65] { + function: !_ + args: { + IDENT [66] { + name: @ac:3 + } + } + } + } + } + } + loop_step: { + CALL [67] { + function: _||_ + args: { + IDENT [68] { + name: @ac:3 + } + CALL [69] { + function: _>_ + args: { + IDENT [70] { + name: @it:3 + } + CONSTANT [71] { value: 1 } + } + } + } + } + } + result: { + IDENT [72] { + name: @ac:3 + } + } } } } @@ -1615,58 +1850,97 @@ CALL [1] { } } } + } + CONSTANT [73] { value: 4 } + } +} +Test case: MULTIPLE_MACROS_2 +Source: [[1].exists(i, i > 0)] + [[1].exists(j, j > 0)] + [['a'].exists(k, k == 'a')] + [['a'].exists(l, l == 'a')] == [true, true, true, true] +=====> +CALL [1] { + function: _==_ + args: { + COMPREHENSION [2] { + iter_var: #unused + iter_range: { + LIST [3] { + elements: { + } + } + } + accu_var: @r1 + accu_init: { + LIST [4] { + elements: { + CONSTANT [5] { value: "a" } + } + } + } loop_condition: { - CONSTANT [19] { value: false } + CONSTANT [6] { value: false } } loop_step: { - IDENT [20] { + IDENT [7] { name: @r1 } } result: { - CALL [21] { + CALL [8] { function: _+_ args: { - CALL [22] { + CALL [9] { function: _+_ args: { - COMPREHENSION [23] { + COMPREHENSION [10] { iter_var: #unused iter_range: { - CREATE_LIST [24] { + LIST [11] { elements: { } } } accu_var: @r0 accu_init: { - CALL [25] { - function: size + LIST [12] { + elements: { + CONSTANT [13] { value: 1 } + } + } + } + loop_condition: { + CONSTANT [14] { value: false } + } + loop_step: { + IDENT [15] { + name: @r0 + } + } + result: { + CALL [16] { + function: _+_ args: { - CREATE_LIST [26] { + LIST [17] { elements: { - COMPREHENSION [27] { - iter_var: @c0:0 + COMPREHENSION [18] { + iter_var: @it:0 iter_range: { - CREATE_LIST [28] { - elements: { - CONSTANT [29] { value: 1 } - } + IDENT [19] { + name: @r0 } } - accu_var: @x0:0 + accu_var: @ac:0 accu_init: { - CONSTANT [30] { value: false } + CONSTANT [20] { value: false } } loop_condition: { - CALL [31] { + CALL [21] { function: @not_strictly_false args: { - CALL [32] { + CALL [22] { function: !_ args: { - IDENT [33] { - name: @x0:0 + IDENT [23] { + name: @ac:0 } } } @@ -1674,316 +1948,798 @@ CALL [1] { } } loop_step: { - CALL [34] { + CALL [24] { function: _||_ args: { - IDENT [35] { - name: @x0:0 + IDENT [25] { + name: @ac:0 } - CALL [36] { + CALL [26] { function: _>_ args: { - IDENT [37] { - name: @c0:0 + IDENT [27] { + name: @it:0 } - CONSTANT [38] { value: 0 } + CONSTANT [28] { value: 0 } } } } } } result: { - IDENT [39] { - name: @x0:0 + IDENT [29] { + name: @ac:0 } } } } } - } - } - } - loop_condition: { - CONSTANT [40] { value: false } - } - loop_step: { - IDENT [41] { - name: @r0 - } - } - result: { - CALL [42] { - function: _+_ - args: { - IDENT [43] { - name: @r0 - } - IDENT [44] { - name: @r0 - } - } + LIST [30] { + elements: { + COMPREHENSION [31] { + iter_var: @it:1 + iter_range: { + IDENT [32] { + name: @r0 + } + } + accu_var: @ac:1 + accu_init: { + CONSTANT [33] { value: false } + } + loop_condition: { + CALL [34] { + function: @not_strictly_false + args: { + CALL [35] { + function: !_ + args: { + IDENT [36] { + name: @ac:1 + } + } + } + } + } + } + loop_step: { + CALL [37] { + function: _||_ + args: { + IDENT [38] { + name: @ac:1 + } + CALL [39] { + function: _>_ + args: { + IDENT [40] { + name: @it:1 + } + CONSTANT [41] { value: 0 } + } + } + } + } + } + result: { + IDENT [42] { + name: @ac:1 + } + } + } + } + } + } + } + } + } + LIST [43] { + elements: { + COMPREHENSION [44] { + iter_var: @it:2 + iter_range: { + IDENT [45] { + name: @r1 + } + } + accu_var: @ac:2 + accu_init: { + CONSTANT [46] { value: false } + } + loop_condition: { + CALL [47] { + function: @not_strictly_false + args: { + CALL [48] { + function: !_ + args: { + IDENT [49] { + name: @ac:2 + } + } + } + } + } + } + loop_step: { + CALL [50] { + function: _||_ + args: { + IDENT [51] { + name: @ac:2 + } + CALL [52] { + function: _==_ + args: { + IDENT [53] { + name: @it:2 + } + CONSTANT [54] { value: "a" } + } + } + } + } + } + result: { + IDENT [55] { + name: @ac:2 + } + } + } + } + } + } + } + LIST [56] { + elements: { + COMPREHENSION [57] { + iter_var: @it:3 + iter_range: { + IDENT [58] { + name: @r1 + } + } + accu_var: @ac:3 + accu_init: { + CONSTANT [59] { value: false } + } + loop_condition: { + CALL [60] { + function: @not_strictly_false + args: { + CALL [61] { + function: !_ + args: { + IDENT [62] { + name: @ac:3 + } + } + } + } + } + } + loop_step: { + CALL [63] { + function: _||_ + args: { + IDENT [64] { + name: @ac:3 + } + CALL [65] { + function: _==_ + args: { + IDENT [66] { + name: @it:3 + } + CONSTANT [67] { value: "a" } + } + } + } + } + } + result: { + IDENT [68] { + name: @ac:3 + } + } + } + } + } + } + } + } + } + LIST [69] { + elements: { + CONSTANT [70] { value: true } + CONSTANT [71] { value: true } + CONSTANT [72] { value: true } + CONSTANT [73] { value: true } + } + } + } +} +Test case: MULTIPLE_MACROS_3 +Source: [1].exists(i, i > 0) && [1].exists(j, j > 0) && [1].exists(k, k > 1) && [2].exists(l, l > 1) +=====> +COMPREHENSION [1] { + iter_var: #unused + iter_range: { + LIST [2] { + elements: { + } + } + } + accu_var: @r0 + accu_init: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + } + } + } + loop_condition: { + CONSTANT [5] { value: false } + } + loop_step: { + IDENT [6] { + name: @r0 + } + } + result: { + CALL [7] { + function: _&&_ + args: { + CALL [8] { + function: _&&_ + args: { + COMPREHENSION [9] { + iter_var: @it:0 + iter_range: { + IDENT [10] { + name: @r0 + } + } + accu_var: @ac:0 + accu_init: { + CONSTANT [11] { value: false } + } + loop_condition: { + CALL [12] { + function: @not_strictly_false + args: { + CALL [13] { + function: !_ + args: { + IDENT [14] { + name: @ac:0 + } + } + } + } + } + } + loop_step: { + CALL [15] { + function: _||_ + args: { + IDENT [16] { + name: @ac:0 + } + CALL [17] { + function: _>_ + args: { + IDENT [18] { + name: @it:0 + } + CONSTANT [19] { value: 0 } + } + } + } + } + } + result: { + IDENT [20] { + name: @ac:0 + } + } + } + COMPREHENSION [21] { + iter_var: @it:1 + iter_range: { + IDENT [22] { + name: @r0 + } + } + accu_var: @ac:1 + accu_init: { + CONSTANT [23] { value: false } + } + loop_condition: { + CALL [24] { + function: @not_strictly_false + args: { + CALL [25] { + function: !_ + args: { + IDENT [26] { + name: @ac:1 + } + } + } + } + } + } + loop_step: { + CALL [27] { + function: _||_ + args: { + IDENT [28] { + name: @ac:1 + } + CALL [29] { + function: _>_ + args: { + IDENT [30] { + name: @it:1 + } + CONSTANT [31] { value: 0 } + } + } + } + } + } + result: { + IDENT [32] { + name: @ac:1 + } + } + } + } + } + CALL [33] { + function: _&&_ + args: { + COMPREHENSION [34] { + iter_var: @it:2 + iter_range: { + IDENT [35] { + name: @r0 + } + } + accu_var: @ac:2 + accu_init: { + CONSTANT [36] { value: false } + } + loop_condition: { + CALL [37] { + function: @not_strictly_false + args: { + CALL [38] { + function: !_ + args: { + IDENT [39] { + name: @ac:2 + } + } + } + } + } + } + loop_step: { + CALL [40] { + function: _||_ + args: { + IDENT [41] { + name: @ac:2 + } + CALL [42] { + function: _>_ + args: { + IDENT [43] { + name: @it:2 + } + CONSTANT [44] { value: 1 } + } + } + } + } + } + result: { + IDENT [45] { + name: @ac:2 + } + } + } + COMPREHENSION [46] { + iter_var: @it:3 + iter_range: { + LIST [47] { + elements: { + CONSTANT [48] { value: 2 } + } + } + } + accu_var: @ac:3 + accu_init: { + CONSTANT [49] { value: false } + } + loop_condition: { + CALL [50] { + function: @not_strictly_false + args: { + CALL [51] { + function: !_ + args: { + IDENT [52] { + name: @ac:3 + } + } + } + } + } + } + loop_step: { + CALL [53] { + function: _||_ + args: { + IDENT [54] { + name: @ac:3 + } + CALL [55] { + function: _>_ + args: { + IDENT [56] { + name: @it:3 + } + CONSTANT [57] { value: 1 } + } + } + } + } + } + result: { + IDENT [58] { + name: @ac:3 + } + } + } + } + } + } + } + } +} +Test case: NESTED_MACROS +Source: [1,2,3].map(i, [1, 2, 3].map(i, i + 1)) == [[2, 3, 4], [2, 3, 4], [2, 3, 4]] +=====> +CALL [1] { + function: _==_ + args: { + COMPREHENSION [2] { + iter_var: #unused + iter_range: { + LIST [3] { + elements: { + } + } + } + accu_var: @r0 + accu_init: { + LIST [4] { + elements: { + CONSTANT [5] { value: 1 } + CONSTANT [6] { value: 2 } + CONSTANT [7] { value: 3 } + } + } + } + loop_condition: { + CONSTANT [8] { value: false } + } + loop_step: { + IDENT [9] { + name: @r0 + } + } + result: { + COMPREHENSION [10] { + iter_var: @it:1 + iter_range: { + IDENT [11] { + name: @r0 + } + } + accu_var: @ac:1 + accu_init: { + LIST [12] { + elements: { + } + } + } + loop_condition: { + CONSTANT [13] { value: true } + } + loop_step: { + CALL [14] { + function: _+_ + args: { + IDENT [15] { + name: @ac:1 + } + LIST [16] { + elements: { + COMPREHENSION [17] { + iter_var: @it:0 + iter_range: { + IDENT [18] { + name: @r0 + } + } + accu_var: @ac:0 + accu_init: { + LIST [19] { + elements: { + } + } + } + loop_condition: { + CONSTANT [20] { value: true } + } + loop_step: { + CALL [21] { + function: _+_ + args: { + IDENT [22] { + name: @ac:0 + } + LIST [23] { + elements: { + CALL [24] { + function: _+_ + args: { + IDENT [25] { + name: @it:0 + } + CONSTANT [26] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [27] { + name: @ac:0 + } + } } } } - IDENT [45] { - name: @r1 - } } } - IDENT [46] { - name: @r1 + } + result: { + IDENT [28] { + name: @ac:1 } } } } } - CONSTANT [47] { value: 4 } - } -} -Test case: MULTIPLE_MACROS_2 -Source: [[1].exists(i, i > 0)] + [[1].exists(j, j > 0)] + [['a'].exists(k, k == 'a')] + [['a'].exists(l, l == 'a')] == [true, true, true, true] -=====> -CALL [1] { - function: _==_ - args: { - COMPREHENSION [2] { + COMPREHENSION [29] { iter_var: #unused iter_range: { - CREATE_LIST [3] { + LIST [30] { elements: { } } } accu_var: @r1 accu_init: { - CREATE_LIST [4] { + LIST [31] { elements: { - COMPREHENSION [5] { - iter_var: @c0:1 - iter_range: { - CREATE_LIST [6] { - elements: { - CONSTANT [7] { value: "a" } - } - } - } - accu_var: @x0:1 - accu_init: { - CONSTANT [8] { value: false } - } - loop_condition: { - CALL [9] { - function: @not_strictly_false - args: { - CALL [10] { - function: !_ - args: { - IDENT [11] { - name: @x0:1 - } - } - } - } - } - } - loop_step: { - CALL [12] { - function: _||_ - args: { - IDENT [13] { - name: @x0:1 - } - CALL [14] { - function: _==_ - args: { - IDENT [15] { - name: @c0:1 - } - CONSTANT [16] { value: "a" } - } - } - } - } - } - result: { - IDENT [17] { - name: @x0:1 - } - } - } + CONSTANT [32] { value: 2 } + CONSTANT [33] { value: 3 } + CONSTANT [34] { value: 4 } } } } loop_condition: { - CONSTANT [18] { value: false } + CONSTANT [35] { value: false } } loop_step: { - IDENT [19] { + IDENT [36] { name: @r1 } } result: { - CALL [20] { + LIST [37] { + elements: { + IDENT [38] { + name: @r1 + } + IDENT [39] { + name: @r1 + } + IDENT [40] { + name: @r1 + } + } + } + } + } + } +} +Test case: NESTED_MACROS_2 +Source: [1, 2].map(y, [1, 2, 3].filter(x, x == y)) == [[1], [2]] +=====> +CALL [31] { + function: _==_ + args: { + COMPREHENSION [30] { + iter_var: y + iter_range: { + LIST [1] { + elements: { + CONSTANT [2] { value: 1 } + CONSTANT [3] { value: 2 } + } + } + } + accu_var: @result + accu_init: { + LIST [24] { + elements: { + } + } + } + loop_condition: { + CONSTANT [25] { value: true } + } + loop_step: { + CALL [28] { function: _+_ args: { - CALL [21] { - function: _+_ - args: { - COMPREHENSION [22] { - iter_var: #unused + IDENT [26] { + name: @result + } + LIST [27] { + elements: { + COMPREHENSION [23] { + iter_var: x iter_range: { - CREATE_LIST [23] { + LIST [6] { elements: { + CONSTANT [7] { value: 1 } + CONSTANT [8] { value: 2 } + CONSTANT [9] { value: 3 } } } } - accu_var: @r0 + accu_var: @result accu_init: { - CREATE_LIST [24] { + LIST [15] { elements: { - COMPREHENSION [25] { - iter_var: @c0:0 - iter_range: { - CREATE_LIST [26] { - elements: { - CONSTANT [27] { value: 1 } - } + } + } + } + loop_condition: { + CONSTANT [16] { value: true } + } + loop_step: { + CALL [21] { + function: _?_:_ + args: { + CALL [13] { + function: _==_ + args: { + IDENT [12] { + name: x } - } - accu_var: @x0:0 - accu_init: { - CONSTANT [28] { value: false } - } - loop_condition: { - CALL [29] { - function: @not_strictly_false - args: { - CALL [30] { - function: !_ - args: { - IDENT [31] { - name: @x0:0 - } - } - } - } + IDENT [14] { + name: y } } - loop_step: { - CALL [32] { - function: _||_ - args: { - IDENT [33] { - name: @x0:0 - } - CALL [34] { - function: _>_ - args: { - IDENT [35] { - name: @c0:0 - } - CONSTANT [36] { value: 0 } - } + } + CALL [19] { + function: _+_ + args: { + IDENT [17] { + name: @result + } + LIST [18] { + elements: { + IDENT [11] { + name: x } } } } - result: { - IDENT [37] { - name: @x0:0 - } - } + } + IDENT [20] { + name: @result } } } } - loop_condition: { - CONSTANT [38] { value: false } - } - loop_step: { - IDENT [39] { - name: @r0 - } - } result: { - CALL [40] { - function: _+_ - args: { - IDENT [41] { - name: @r0 - } - IDENT [42] { - name: @r0 - } - } + IDENT [22] { + name: @result } } } - IDENT [43] { - name: @r1 - } } } - IDENT [44] { - name: @r1 - } } } } - } - CREATE_LIST [45] { - elements: { - CONSTANT [46] { value: true } - CONSTANT [47] { value: true } - CONSTANT [48] { value: true } - CONSTANT [49] { value: true } + result: { + IDENT [29] { + name: @result + } } } - } -} -Test case: NESTED_MACROS -Source: [1,2,3].map(i, [1, 2, 3].map(i, i + 1)) == [[2, 3, 4], [2, 3, 4], [2, 3, 4]] -=====> -CALL [1] { - function: _==_ - args: { - COMPREHENSION [2] { - iter_var: #unused - iter_range: { - CREATE_LIST [3] { + LIST [32] { + elements: { + LIST [33] { elements: { + CONSTANT [34] { value: 1 } } } - } - accu_var: @r0 - accu_init: { - CREATE_LIST [4] { + LIST [35] { elements: { - CONSTANT [5] { value: 1 } - CONSTANT [6] { value: 2 } - CONSTANT [7] { value: 3 } + CONSTANT [36] { value: 2 } } } } - loop_condition: { - CONSTANT [8] { value: false } + } + } +} +Test case: ADJACENT_NESTED_MACROS +Source: [1,2,3].map(i, [1, 2, 3].map(i, i + 1)) == [1,2,3].map(j, [1, 2, 3].map(j, j + 1)) +=====> +COMPREHENSION [1] { + iter_var: #unused + iter_range: { + LIST [2] { + elements: { } - loop_step: { - IDENT [9] { - name: @r0 - } + } + } + accu_var: @r0 + accu_init: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + CONSTANT [5] { value: 2 } + CONSTANT [6] { value: 3 } } - result: { + } + } + loop_condition: { + CONSTANT [7] { value: false } + } + loop_step: { + IDENT [8] { + name: @r0 + } + } + result: { + CALL [9] { + function: _==_ + args: { COMPREHENSION [10] { - iter_var: @c0:0 + iter_var: @it:1 iter_range: { IDENT [11] { name: @r0 } } - accu_var: @x0:0 + accu_var: @ac:1 accu_init: { - CREATE_LIST [12] { + LIST [12] { elements: { } } @@ -1996,20 +2752,20 @@ CALL [1] { function: _+_ args: { IDENT [15] { - name: @x0:0 + name: @ac:1 } - CREATE_LIST [16] { + LIST [16] { elements: { COMPREHENSION [17] { - iter_var: @c1:0 + iter_var: @it:0 iter_range: { IDENT [18] { name: @r0 } } - accu_var: @x1:0 + accu_var: @ac:0 accu_init: { - CREATE_LIST [19] { + LIST [19] { elements: { } } @@ -2022,15 +2778,15 @@ CALL [1] { function: _+_ args: { IDENT [22] { - name: @x1:0 + name: @ac:0 } - CREATE_LIST [23] { + LIST [23] { elements: { CALL [24] { function: _+_ args: { IDENT [25] { - name: @c1:0 + name: @it:0 } CONSTANT [26] { value: 1 } } @@ -2042,7 +2798,7 @@ CALL [1] { } result: { IDENT [27] { - name: @x1:0 + name: @ac:0 } } } @@ -2053,175 +2809,91 @@ CALL [1] { } result: { IDENT [28] { - name: @x0:0 + name: @ac:1 } } } - } - } - COMPREHENSION [29] { - iter_var: #unused - iter_range: { - CREATE_LIST [30] { - elements: { - } - } - } - accu_var: @r1 - accu_init: { - CREATE_LIST [31] { - elements: { - CONSTANT [32] { value: 2 } - CONSTANT [33] { value: 3 } - CONSTANT [34] { value: 4 } - } - } - } - loop_condition: { - CONSTANT [35] { value: false } - } - loop_step: { - IDENT [36] { - name: @r1 - } - } - result: { - CREATE_LIST [37] { - elements: { - IDENT [38] { - name: @r1 - } - IDENT [39] { - name: @r1 - } - IDENT [40] { - name: @r1 + COMPREHENSION [29] { + iter_var: @it:3 + iter_range: { + IDENT [30] { + name: @r0 } } - } - } - } - } -} -Test case: NESTED_MACROS_2 -Source: [1, 2].map(y, [1, 2, 3].filter(x, x == y)) == [[1], [2]] -=====> -CALL [31] { - function: _==_ - args: { - COMPREHENSION [30] { - iter_var: @c0:0 - iter_range: { - CREATE_LIST [1] { - elements: { - CONSTANT [2] { value: 1 } - CONSTANT [3] { value: 2 } + accu_var: @ac:3 + accu_init: { + LIST [31] { + elements: { + } + } } - } - } - accu_var: @x0:0 - accu_init: { - CREATE_LIST [24] { - elements: { + loop_condition: { + CONSTANT [32] { value: true } } - } - } - loop_condition: { - CONSTANT [25] { value: true } - } - loop_step: { - CALL [28] { - function: _+_ - args: { - IDENT [26] { - name: @x0:0 - } - CREATE_LIST [27] { - elements: { - COMPREHENSION [23] { - iter_var: @c1:0 - iter_range: { - CREATE_LIST [6] { - elements: { - CONSTANT [7] { value: 1 } - CONSTANT [8] { value: 2 } - CONSTANT [9] { value: 3 } - } - } - } - accu_var: @x1:0 - accu_init: { - CREATE_LIST [15] { - elements: { + loop_step: { + CALL [33] { + function: _+_ + args: { + IDENT [34] { + name: @ac:3 + } + LIST [35] { + elements: { + COMPREHENSION [36] { + iter_var: @it:2 + iter_range: { + IDENT [37] { + name: @r0 + } } - } - } - loop_condition: { - CONSTANT [16] { value: true } - } - loop_step: { - CALL [21] { - function: _?_:_ - args: { - CALL [13] { - function: _==_ - args: { - IDENT [12] { - name: @c1:0 - } - IDENT [14] { - name: @c0:0 - } + accu_var: @ac:2 + accu_init: { + LIST [38] { + elements: { } } - CALL [19] { + } + loop_condition: { + CONSTANT [39] { value: true } + } + loop_step: { + CALL [40] { function: _+_ args: { - IDENT [17] { - name: @x1:0 + IDENT [41] { + name: @ac:2 } - CREATE_LIST [18] { + LIST [42] { elements: { - IDENT [11] { - name: @c1:0 + CALL [43] { + function: _+_ + args: { + IDENT [44] { + name: @it:2 + } + CONSTANT [45] { value: 1 } + } } } } } } - IDENT [20] { - name: @x1:0 + } + result: { + IDENT [46] { + name: @ac:2 } } } } - result: { - IDENT [22] { - name: @x1:0 - } - } } } } } - } - } - result: { - IDENT [29] { - name: @x0:0 - } - } - } - CREATE_LIST [32] { - elements: { - CREATE_LIST [33] { - elements: { - CONSTANT [34] { value: 1 } - } - } - CREATE_LIST [35] { - elements: { - CONSTANT [36] { value: 2 } + result: { + IDENT [47] { + name: @ac:3 + } } } } @@ -2234,14 +2906,14 @@ Source: 1 in [1,2,3] && 2 in [1,2,3] && 3 in [3, [1,2,3]] && 1 in [1,2,3] COMPREHENSION [1] { iter_var: #unused iter_range: { - CREATE_LIST [2] { + LIST [2] { elements: { } } } accu_var: @r0 accu_init: { - CREATE_LIST [3] { + LIST [3] { elements: { CONSTANT [4] { value: 1 } CONSTANT [5] { value: 2 } @@ -2261,7 +2933,7 @@ COMPREHENSION [1] { COMPREHENSION [9] { iter_var: #unused iter_range: { - CREATE_LIST [10] { + LIST [10] { elements: { } } @@ -2314,7 +2986,7 @@ COMPREHENSION [1] { function: @in args: { CONSTANT [24] { value: 3 } - CREATE_LIST [25] { + LIST [25] { elements: { CONSTANT [26] { value: 3 } IDENT [27] { @@ -2345,14 +3017,14 @@ CALL [1] { COMPREHENSION [3] { iter_var: #unused iter_range: { - CREATE_LIST [4] { + LIST [4] { elements: { } } } accu_var: @r0 accu_init: { - CREATE_MAP [5] { + MAP [5] { MAP_ENTRY [6] { key: { CONSTANT [7] { value: true } @@ -2372,7 +3044,7 @@ CALL [1] { } } result: { - CREATE_MAP [11] { + MAP [11] { MAP_ENTRY [12] { key: { CONSTANT [13] { value: "a" } @@ -2412,14 +3084,14 @@ Source: [1,2].map(i, [1, 2].map(i, [3,4])) == [[[3, 4], [3, 4]], [[3, 4], [3, 4] COMPREHENSION [1] { iter_var: #unused iter_range: { - CREATE_LIST [2] { + LIST [2] { elements: { } } } accu_var: @r1 accu_init: { - CREATE_LIST [3] { + LIST [3] { elements: { CONSTANT [4] { value: 3 } CONSTANT [5] { value: 4 } @@ -2441,14 +3113,14 @@ COMPREHENSION [1] { COMPREHENSION [9] { iter_var: #unused iter_range: { - CREATE_LIST [10] { + LIST [10] { elements: { } } } accu_var: @r0 accu_init: { - CREATE_LIST [11] { + LIST [11] { elements: { CONSTANT [12] { value: 1 } CONSTANT [13] { value: 2 } @@ -2465,15 +3137,15 @@ COMPREHENSION [1] { } result: { COMPREHENSION [16] { - iter_var: @c0:0 + iter_var: @it:1 iter_range: { IDENT [17] { name: @r0 } } - accu_var: @x0:0 + accu_var: @ac:1 accu_init: { - CREATE_LIST [18] { + LIST [18] { elements: { } } @@ -2486,20 +3158,20 @@ COMPREHENSION [1] { function: _+_ args: { IDENT [21] { - name: @x0:0 + name: @ac:1 } - CREATE_LIST [22] { + LIST [22] { elements: { COMPREHENSION [23] { - iter_var: @c1:0 + iter_var: @it:0 iter_range: { IDENT [24] { name: @r0 } } - accu_var: @x1:0 + accu_var: @ac:0 accu_init: { - CREATE_LIST [25] { + LIST [25] { elements: { } } @@ -2512,9 +3184,9 @@ COMPREHENSION [1] { function: _+_ args: { IDENT [28] { - name: @x1:0 + name: @ac:0 } - CREATE_LIST [29] { + LIST [29] { elements: { IDENT [30] { name: @r1 @@ -2526,7 +3198,7 @@ COMPREHENSION [1] { } result: { IDENT [31] { - name: @x1:0 + name: @ac:0 } } } @@ -2537,7 +3209,7 @@ COMPREHENSION [1] { } result: { IDENT [32] { - name: @x0:0 + name: @ac:1 } } } @@ -2546,14 +3218,14 @@ COMPREHENSION [1] { COMPREHENSION [33] { iter_var: #unused iter_range: { - CREATE_LIST [34] { + LIST [34] { elements: { } } } accu_var: @r2 accu_init: { - CREATE_LIST [35] { + LIST [35] { elements: { IDENT [36] { name: @r1 @@ -2573,7 +3245,7 @@ COMPREHENSION [1] { } } result: { - CREATE_LIST [40] { + LIST [40] { elements: { IDENT [41] { name: @r2 @@ -2595,7 +3267,7 @@ Source: [x - 1 > 3 ? x - 1 : 5].exists(x, x - 1 > 3) || x - 1 > 3 COMPREHENSION [1] { iter_var: #unused iter_range: { - CREATE_LIST [2] { + LIST [2] { elements: { } } @@ -2624,7 +3296,7 @@ COMPREHENSION [1] { COMPREHENSION [8] { iter_var: #unused iter_range: { - CREATE_LIST [9] { + LIST [9] { elements: { } } @@ -2654,9 +3326,9 @@ COMPREHENSION [1] { function: _||_ args: { COMPREHENSION [16] { - iter_var: @c0:0 + iter_var: @it:0 iter_range: { - CREATE_LIST [17] { + LIST [17] { elements: { CALL [18] { function: _?_:_ @@ -2673,7 +3345,7 @@ COMPREHENSION [1] { } } } - accu_var: @x0:0 + accu_var: @ac:0 accu_init: { CONSTANT [22] { value: false } } @@ -2685,7 +3357,7 @@ COMPREHENSION [1] { function: !_ args: { IDENT [25] { - name: @x0:0 + name: @ac:0 } } } @@ -2697,7 +3369,7 @@ COMPREHENSION [1] { function: _||_ args: { IDENT [27] { - name: @x0:0 + name: @ac:0 } CALL [28] { function: _>_ @@ -2706,7 +3378,7 @@ COMPREHENSION [1] { function: _-_ args: { IDENT [30] { - name: @c0:0 + name: @it:0 } CONSTANT [31] { value: 1 } } @@ -2719,7 +3391,7 @@ COMPREHENSION [1] { } result: { IDENT [33] { - name: @x0:0 + name: @ac:0 } } } @@ -2735,76 +3407,59 @@ COMPREHENSION [1] { Test case: MACRO_SHADOWED_VARIABLE_2 Source: ["foo", "bar"].map(x, [x + x, x + x]).map(x, [x + x, x + x]) =====> -COMPREHENSION [1] { - iter_var: @c0:0 +COMPREHENSION [35] { + iter_var: x iter_range: { - COMPREHENSION [2] { - iter_var: @c1:0 + COMPREHENSION [19] { + iter_var: x iter_range: { - CREATE_LIST [3] { + LIST [1] { elements: { - CONSTANT [4] { value: "foo" } - CONSTANT [5] { value: "bar" } + CONSTANT [2] { value: "foo" } + CONSTANT [3] { value: "bar" } } } } - accu_var: @x1:0 + accu_var: @result accu_init: { - CREATE_LIST [6] { + LIST [13] { elements: { } } } loop_condition: { - CONSTANT [7] { value: true } + CONSTANT [14] { value: true } } loop_step: { - CALL [8] { + CALL [17] { function: _+_ args: { - IDENT [9] { - name: @x1:0 + IDENT [15] { + name: @result } - CREATE_LIST [10] { + LIST [16] { elements: { - COMPREHENSION [11] { - iter_var: #unused - iter_range: { - CREATE_LIST [12] { - elements: { - } - } - } - accu_var: @r0 - accu_init: { - CALL [13] { + LIST [6] { + elements: { + CALL [8] { function: _+_ args: { - IDENT [14] { - name: @c1:0 + IDENT [7] { + name: x } - IDENT [15] { - name: @c1:0 + IDENT [9] { + name: x } } } - } - loop_condition: { - CONSTANT [16] { value: false } - } - loop_step: { - IDENT [17] { - name: @r0 - } - } - result: { - CREATE_LIST [18] { - elements: { - IDENT [19] { - name: @r0 + CALL [11] { + function: _+_ + args: { + IDENT [10] { + name: x } - IDENT [20] { - name: @r0 + IDENT [12] { + name: x } } } @@ -2816,69 +3471,52 @@ COMPREHENSION [1] { } } result: { - IDENT [21] { - name: @x1:0 + IDENT [18] { + name: @result } } } } - accu_var: @x0:0 + accu_var: @result accu_init: { - CREATE_LIST [22] { + LIST [29] { elements: { } } } loop_condition: { - CONSTANT [23] { value: true } + CONSTANT [30] { value: true } } loop_step: { - CALL [24] { + CALL [33] { function: _+_ args: { - IDENT [25] { - name: @x0:0 + IDENT [31] { + name: @result } - CREATE_LIST [26] { + LIST [32] { elements: { - COMPREHENSION [27] { - iter_var: #unused - iter_range: { - CREATE_LIST [28] { - elements: { - } - } - } - accu_var: @r1 - accu_init: { - CALL [29] { + LIST [22] { + elements: { + CALL [24] { function: _+_ args: { - IDENT [30] { - name: @c0:0 + IDENT [23] { + name: x } - IDENT [31] { - name: @c0:0 + IDENT [25] { + name: x } } } - } - loop_condition: { - CONSTANT [32] { value: false } - } - loop_step: { - IDENT [33] { - name: @r1 - } - } - result: { - CREATE_LIST [34] { - elements: { - IDENT [35] { - name: @r1 + CALL [27] { + function: _+_ + args: { + IDENT [26] { + name: x } - IDENT [36] { - name: @r1 + IDENT [28] { + name: x } } } @@ -2890,8 +3528,8 @@ COMPREHENSION [1] { } } result: { - IDENT [37] { - name: @x0:0 + IDENT [34] { + name: @result } } } @@ -2901,14 +3539,14 @@ Source: has({'a': true}.a) && {'a':true}['a'] COMPREHENSION [1] { iter_var: #unused iter_range: { - CREATE_LIST [2] { + LIST [2] { elements: { } } } accu_var: @r0 accu_init: { - CREATE_MAP [3] { + MAP [3] { MAP_ENTRY [4] { key: { CONSTANT [5] { value: "a" } @@ -2949,6 +3587,54 @@ COMPREHENSION [1] { } } } +Test case: PRESENCE_TEST_2 +Source: has({'a': true}.a) && has({'a': true}.a) +=====> +COMPREHENSION [1] { + iter_var: #unused + iter_range: { + LIST [2] { + elements: { + } + } + } + accu_var: @r0 + accu_init: { + SELECT [3] { + MAP [4] { + MAP_ENTRY [5] { + key: { + CONSTANT [6] { value: "a" } + } + value: { + CONSTANT [7] { value: true } + } + } + }.a~presence_test + } + } + loop_condition: { + CONSTANT [8] { value: false } + } + loop_step: { + IDENT [9] { + name: @r0 + } + } + result: { + CALL [10] { + function: _&&_ + args: { + IDENT [11] { + name: @r0 + } + IDENT [12] { + name: @r0 + } + } + } + } +} Test case: PRESENCE_TEST_WITH_TERNARY Source: (has(msg.oneof_type.payload) ? msg.oneof_type.payload.single_int64 : 0) == 10 =====> @@ -2958,7 +3644,7 @@ CALL [1] { COMPREHENSION [2] { iter_var: #unused iter_range: { - CREATE_LIST [3] { + LIST [3] { elements: { } } @@ -3012,7 +3698,7 @@ CALL [1] { COMPREHENSION [2] { iter_var: #unused iter_range: { - CREATE_LIST [3] { + LIST [3] { elements: { } } @@ -3037,7 +3723,7 @@ CALL [1] { COMPREHENSION [8] { iter_var: #unused iter_range: { - CREATE_LIST [9] { + LIST [9] { elements: { } } @@ -3099,7 +3785,7 @@ CALL [1] { COMPREHENSION [2] { iter_var: #unused iter_range: { - CREATE_LIST [3] { + LIST [3] { elements: { } } @@ -3126,7 +3812,7 @@ CALL [1] { COMPREHENSION [9] { iter_var: #unused iter_range: { - CREATE_LIST [10] { + LIST [10] { elements: { } } @@ -3183,7 +3869,7 @@ Source: (has(msg.oneof_type) && has(msg.oneof_type.payload) && has(msg.oneof_typ COMPREHENSION [1] { iter_var: #unused iter_range: { - CREATE_LIST [2] { + LIST [2] { elements: { } } @@ -3208,7 +3894,7 @@ COMPREHENSION [1] { COMPREHENSION [7] { iter_var: #unused iter_range: { - CREATE_LIST [8] { + LIST [8] { elements: { } } @@ -3261,7 +3947,7 @@ COMPREHENSION [1] { COMPREHENSION [22] { iter_var: #unused iter_range: { - CREATE_LIST [23] { + LIST [23] { elements: { } } @@ -3333,7 +4019,7 @@ CALL [1] { COMPREHENSION [2] { iter_var: #unused iter_range: { - CREATE_LIST [3] { + LIST [3] { elements: { } } @@ -3358,14 +4044,14 @@ CALL [1] { COMPREHENSION [7] { iter_var: #unused iter_range: { - CREATE_LIST [8] { + LIST [8] { elements: { } } } accu_var: @r1 accu_init: { - CREATE_LIST [9] { + LIST [9] { elements: { IDENT [10] { name: @r0 @@ -3386,7 +4072,7 @@ CALL [1] { } } result: { - CREATE_LIST [14] { + LIST [14] { elements: { CONSTANT [15] { value: 10 } IDENT [16] { @@ -3408,14 +4094,14 @@ CALL [1] { COMPREHENSION [19] { iter_var: #unused iter_range: { - CREATE_LIST [20] { + LIST [20] { elements: { } } } accu_var: @r2 accu_init: { - CREATE_LIST [21] { + LIST [21] { elements: { CONSTANT [22] { value: 5 } } @@ -3430,7 +4116,7 @@ CALL [1] { } } result: { - CREATE_LIST [25] { + LIST [25] { elements: { CONSTANT [26] { value: 10 } IDENT [27] { @@ -3454,7 +4140,7 @@ CALL [1] { COMPREHENSION [2] { iter_var: #unused iter_range: { - CREATE_LIST [3] { + LIST [3] { elements: { } } @@ -3464,7 +4150,7 @@ CALL [1] { CALL [4] { function: _[_] args: { - CREATE_MAP [5] { + MAP [5] { MAP_ENTRY [6] { key: { CONSTANT [7] { value: "hello" } @@ -3518,14 +4204,14 @@ CALL [1] { COMPREHENSION [2] { iter_var: #unused iter_range: { - CREATE_LIST [3] { + LIST [3] { elements: { } } } accu_var: @r0 accu_init: { - CREATE_MAP [4] { + MAP [4] { MAP_ENTRY [5] { key: { CONSTANT [6] { value: "key" } @@ -3554,7 +4240,7 @@ CALL [1] { CALL [12] { function: _[?_] args: { - CREATE_MAP [13] { + MAP [13] { MAP_ENTRY [14] { key: { CONSTANT [15] { value: "key" } @@ -3613,14 +4299,14 @@ CALL [1] { COMPREHENSION [2] { iter_var: #unused iter_range: { - CREATE_LIST [3] { + LIST [3] { elements: { } } } accu_var: @r0 accu_init: { - CREATE_STRUCT [4] { + STRUCT [4] { name: TestAllTypes entries: { ENTRY [5] { @@ -3685,7 +4371,7 @@ Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('h' + 'e' + 'l' + 'l' + COMPREHENSION [1] { iter_var: #unused iter_range: { - CREATE_LIST [2] { + LIST [2] { elements: { } } @@ -3901,7 +4587,7 @@ CALL [1] { COMPREHENSION [2] { iter_var: #unused iter_range: { - CREATE_LIST [3] { + LIST [3] { elements: { } } @@ -3928,7 +4614,7 @@ CALL [1] { COMPREHENSION [9] { iter_var: #unused iter_range: { - CREATE_LIST [10] { + LIST [10] { elements: { } } @@ -4011,7 +4697,7 @@ CALL [1] { COMPREHENSION [2] { iter_var: #unused iter_range: { - CREATE_LIST [3] { + LIST [3] { elements: { } } @@ -4038,7 +4724,7 @@ CALL [1] { COMPREHENSION [9] { iter_var: #unused iter_range: { - CREATE_LIST [10] { + LIST [10] { elements: { } } @@ -4106,4 +4792,4 @@ CALL [1] { } } } -} +} \ No newline at end of file diff --git a/optimizer/src/test/resources/subexpression_unparsed.baseline b/optimizer/src/test/resources/subexpression_unparsed.baseline index f820ae1e8..780664a14 100644 --- a/optimizer/src/test/resources/subexpression_unparsed.baseline +++ b/optimizer/src/test/resources/subexpression_unparsed.baseline @@ -2,654 +2,793 @@ Test case: SIZE_1 Source: size([1,2]) + size([1,2]) + 1 == 5 =====> Result: true -[CASCADED_BINDS]: cel.bind(@r0, size([1, 2]), @r0 + @r0) + 1 == 5 [BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([size([1, 2])], @index0 + @index0 + 1 == 5) [BLOCK_RECURSION_DEPTH_1]: cel.@block([[1, 2], size(@index0), @index1 + @index1, @index2 + 1], @index3 == 5) -[BLOCK_RECURSION_DEPTH_2]: cel.@block([[1, 2], size(@index0), @index1 + @index1 + 1], @index2 == 5) -[BLOCK_RECURSION_DEPTH_3]: cel.@block([[1, 2], size(@index0), @index1 + @index1 + 1], @index2 == 5) -[BLOCK_RECURSION_DEPTH_4]: cel.@block([[1, 2], size(@index0), @index1 + @index1 + 1], @index2 == 5) -[BLOCK_RECURSION_DEPTH_5]: cel.@block([[1, 2], size(@index0), @index1 + @index1 + 1], @index2 == 5) -[BLOCK_RECURSION_DEPTH_6]: cel.@block([[1, 2], size(@index0), @index1 + @index1 + 1], @index2 == 5) -[BLOCK_RECURSION_DEPTH_7]: cel.@block([[1, 2], size(@index0), @index1 + @index1 + 1], @index2 == 5) -[BLOCK_RECURSION_DEPTH_8]: cel.@block([[1, 2], size(@index0), @index1 + @index1 + 1], @index2 == 5) -[BLOCK_RECURSION_DEPTH_9]: cel.@block([[1, 2], size(@index0), @index1 + @index1 + 1], @index2 == 5) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([size([1, 2]), @index0 + @index0 + 1], @index1 == 5) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([size([1, 2])], @index0 + @index0 + 1 == 5) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([size([1, 2])], @index0 + @index0 + 1 == 5) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([size([1, 2])], @index0 + @index0 + 1 == 5) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([size([1, 2])], @index0 + @index0 + 1 == 5) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([size([1, 2])], @index0 + @index0 + 1 == 5) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([size([1, 2])], @index0 + @index0 + 1 == 5) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([size([1, 2])], @index0 + @index0 + 1 == 5) Test case: SIZE_2 Source: 2 + size([1,2]) + size([1,2]) + 1 == 7 =====> Result: true -[CASCADED_BINDS]: cel.bind(@r0, size([1, 2]), 2 + @r0 + @r0) + 1 == 7 [BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([size([1, 2])], 2 + @index0 + @index0 + 1 == 7) [BLOCK_RECURSION_DEPTH_1]: cel.@block([[1, 2], size(@index0), 2 + @index1, @index2 + @index1, @index3 + 1], @index4 == 7) -[BLOCK_RECURSION_DEPTH_2]: cel.@block([[1, 2], size(@index0), 2 + @index1 + @index1, @index2 + 1], @index3 == 7) -[BLOCK_RECURSION_DEPTH_3]: cel.@block([[1, 2], size(@index0), 2 + @index1 + @index1 + 1], @index2 == 7) -[BLOCK_RECURSION_DEPTH_4]: cel.@block([[1, 2], size(@index0), 2 + @index1 + @index1 + 1], @index2 == 7) -[BLOCK_RECURSION_DEPTH_5]: cel.@block([[1, 2], size(@index0), 2 + @index1 + @index1 + 1], @index2 == 7) -[BLOCK_RECURSION_DEPTH_6]: cel.@block([[1, 2], size(@index0), 2 + @index1 + @index1 + 1], @index2 == 7) -[BLOCK_RECURSION_DEPTH_7]: cel.@block([[1, 2], size(@index0), 2 + @index1 + @index1 + 1], @index2 == 7) -[BLOCK_RECURSION_DEPTH_8]: cel.@block([[1, 2], size(@index0), 2 + @index1 + @index1 + 1], @index2 == 7) -[BLOCK_RECURSION_DEPTH_9]: cel.@block([[1, 2], size(@index0), 2 + @index1 + @index1 + 1], @index2 == 7) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([size([1, 2]), 2 + @index0 + @index0], @index1 + 1 == 7) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([size([1, 2]), 2 + @index0 + @index0 + 1], @index1 == 7) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([size([1, 2])], 2 + @index0 + @index0 + 1 == 7) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([size([1, 2])], 2 + @index0 + @index0 + 1 == 7) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([size([1, 2])], 2 + @index0 + @index0 + 1 == 7) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([size([1, 2])], 2 + @index0 + @index0 + 1 == 7) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([size([1, 2])], 2 + @index0 + @index0 + 1 == 7) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([size([1, 2])], 2 + @index0 + @index0 + 1 == 7) Test case: SIZE_3 Source: size([0]) + size([0]) + size([1,2]) + size([1,2]) == 6 =====> Result: true -[CASCADED_BINDS]: cel.bind(@r1, size([1, 2]), cel.bind(@r0, size([0]), @r0 + @r0) + @r1 + @r1) == 6 [BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([size([0]), size([1, 2])], @index0 + @index0 + @index1 + @index1 == 6) [BLOCK_RECURSION_DEPTH_1]: cel.@block([[0], size(@index0), [1, 2], size(@index2), @index1 + @index1, @index4 + @index3, @index5 + @index3], @index6 == 6) -[BLOCK_RECURSION_DEPTH_2]: cel.@block([[0], size(@index0), [1, 2], size(@index2), @index1 + @index1 + @index3, @index4 + @index3], @index5 == 6) -[BLOCK_RECURSION_DEPTH_3]: cel.@block([[0], size(@index0), [1, 2], size(@index2), @index1 + @index1 + @index3 + @index3], @index4 == 6) -[BLOCK_RECURSION_DEPTH_4]: cel.@block([[0], size(@index0), [1, 2], size(@index2), @index1 + @index1 + @index3 + @index3], @index4 == 6) -[BLOCK_RECURSION_DEPTH_5]: cel.@block([[0], size(@index0), [1, 2], size(@index2), @index1 + @index1 + @index3 + @index3], @index4 == 6) -[BLOCK_RECURSION_DEPTH_6]: cel.@block([[0], size(@index0), [1, 2], size(@index2), @index1 + @index1 + @index3 + @index3], @index4 == 6) -[BLOCK_RECURSION_DEPTH_7]: cel.@block([[0], size(@index0), [1, 2], size(@index2), @index1 + @index1 + @index3 + @index3], @index4 == 6) -[BLOCK_RECURSION_DEPTH_8]: cel.@block([[0], size(@index0), [1, 2], size(@index2), @index1 + @index1 + @index3 + @index3], @index4 == 6) -[BLOCK_RECURSION_DEPTH_9]: cel.@block([[0], size(@index0), [1, 2], size(@index2), @index1 + @index1 + @index3 + @index3], @index4 == 6) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([size([0]), size([1, 2]), @index0 + @index0 + @index1], @index2 + @index1 == 6) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([size([0]), size([1, 2]), @index0 + @index0 + @index1 + @index1], @index2 == 6) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([size([0]), size([1, 2])], @index0 + @index0 + @index1 + @index1 == 6) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([size([0]), size([1, 2])], @index0 + @index0 + @index1 + @index1 == 6) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([size([0]), size([1, 2])], @index0 + @index0 + @index1 + @index1 == 6) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([size([0]), size([1, 2])], @index0 + @index0 + @index1 + @index1 == 6) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([size([0]), size([1, 2])], @index0 + @index0 + @index1 + @index1 == 6) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([size([0]), size([1, 2])], @index0 + @index0 + @index1 + @index1 == 6) Test case: SIZE_4 Source: 5 + size([0]) + size([0]) + size([1,2]) + size([1,2]) + size([1,2,3]) + size([1,2,3]) == 17 =====> Result: true -[CASCADED_BINDS]: cel.bind(@r2, size([1, 2, 3]), cel.bind(@r1, size([1, 2]), cel.bind(@r0, size([0]), 5 + @r0 + @r0) + @r1 + @r1) + @r2 + @r2) == 17 [BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([size([0]), size([1, 2]), size([1, 2, 3])], 5 + @index0 + @index0 + @index1 + @index1 + @index2 + @index2 == 17) [BLOCK_RECURSION_DEPTH_1]: cel.@block([[0], size(@index0), [1, 2], size(@index2), [1, 2, 3], size(@index4), 5 + @index1, @index6 + @index1, @index7 + @index3, @index8 + @index3, @index9 + @index5, @index10 + @index5], @index11 == 17) -[BLOCK_RECURSION_DEPTH_2]: cel.@block([[0], size(@index0), [1, 2], size(@index2), [1, 2, 3], size(@index4), 5 + @index1 + @index1, @index6 + @index3 + @index3, @index7 + @index5 + @index5], @index8 == 17) -[BLOCK_RECURSION_DEPTH_3]: cel.@block([[0], size(@index0), [1, 2], size(@index2), [1, 2, 3], size(@index4), 5 + @index1 + @index1 + @index3, @index6 + @index3 + @index5 + @index5], @index7 == 17) -[BLOCK_RECURSION_DEPTH_4]: cel.@block([[0], size(@index0), [1, 2], size(@index2), [1, 2, 3], size(@index4), 5 + @index1 + @index1 + @index3 + @index3, @index6 + @index5 + @index5], @index7 == 17) -[BLOCK_RECURSION_DEPTH_5]: cel.@block([[0], size(@index0), [1, 2], size(@index2), [1, 2, 3], size(@index4), 5 + @index1 + @index1 + @index3 + @index3 + @index5, @index6 + @index5], @index7 == 17) -[BLOCK_RECURSION_DEPTH_6]: cel.@block([[0], size(@index0), [1, 2], size(@index2), [1, 2, 3], size(@index4), 5 + @index1 + @index1 + @index3 + @index3 + @index5 + @index5], @index6 == 17) -[BLOCK_RECURSION_DEPTH_7]: cel.@block([[0], size(@index0), [1, 2], size(@index2), [1, 2, 3], size(@index4), 5 + @index1 + @index1 + @index3 + @index3 + @index5 + @index5], @index6 == 17) -[BLOCK_RECURSION_DEPTH_8]: cel.@block([[0], size(@index0), [1, 2], size(@index2), [1, 2, 3], size(@index4), 5 + @index1 + @index1 + @index3 + @index3 + @index5 + @index5], @index6 == 17) -[BLOCK_RECURSION_DEPTH_9]: cel.@block([[0], size(@index0), [1, 2], size(@index2), [1, 2, 3], size(@index4), 5 + @index1 + @index1 + @index3 + @index3 + @index5 + @index5], @index6 == 17) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([size([0]), size([1, 2]), size([1, 2, 3]), 5 + @index0 + @index0, @index3 + @index1 + @index1, @index4 + @index2 + @index2], @index5 == 17) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([size([0]), size([1, 2]), size([1, 2, 3]), 5 + @index0 + @index0 + @index1, @index3 + @index1 + @index2 + @index2], @index4 == 17) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([size([0]), size([1, 2]), size([1, 2, 3]), 5 + @index0 + @index0 + @index1 + @index1], @index3 + @index2 + @index2 == 17) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([size([0]), size([1, 2]), size([1, 2, 3]), 5 + @index0 + @index0 + @index1 + @index1 + @index2], @index3 + @index2 == 17) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([size([0]), size([1, 2]), size([1, 2, 3]), 5 + @index0 + @index0 + @index1 + @index1 + @index2 + @index2], @index3 == 17) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([size([0]), size([1, 2]), size([1, 2, 3])], 5 + @index0 + @index0 + @index1 + @index1 + @index2 + @index2 == 17) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([size([0]), size([1, 2]), size([1, 2, 3])], 5 + @index0 + @index0 + @index1 + @index1 + @index2 + @index2 == 17) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([size([0]), size([1, 2]), size([1, 2, 3])], 5 + @index0 + @index0 + @index1 + @index1 + @index2 + @index2 == 17) Test case: TIMESTAMP Source: timestamp(int(timestamp(1000000000))).getFullYear() + timestamp(int(timestamp(75))).getFullYear() + timestamp(int(timestamp(50))).getFullYear() + timestamp(int(timestamp(1000000000))).getFullYear() + timestamp(int(timestamp(50))).getSeconds() + timestamp(int(timestamp(200))).getFullYear() + timestamp(int(timestamp(200))).getFullYear() + timestamp(int(timestamp(75))).getMinutes() + timestamp(int(timestamp(1000000000))).getFullYear() == 13934 =====> Result: true -[CASCADED_BINDS]: cel.bind(@r0, timestamp(int(timestamp(1000000000))).getFullYear(), cel.bind(@r3, timestamp(int(timestamp(75))), cel.bind(@r2, timestamp(int(timestamp(200))).getFullYear(), cel.bind(@r1, timestamp(int(timestamp(50))), @r0 + @r3.getFullYear() + @r1.getFullYear() + @r0 + @r1.getSeconds()) + @r2 + @r2) + @r3.getMinutes()) + @r0) == 13934 [BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([timestamp(int(timestamp(1000000000))).getFullYear(), timestamp(int(timestamp(50))), timestamp(int(timestamp(200))).getFullYear(), timestamp(int(timestamp(75)))], @index0 + @index3.getFullYear() + @index1.getFullYear() + @index0 + @index1.getSeconds() + @index2 + @index2 + @index3.getMinutes() + @index0 == 13934) -[BLOCK_RECURSION_DEPTH_1]: cel.@block([timestamp(1000000000), int(@index0), timestamp(@index1), @index2.getFullYear(), timestamp(50), int(@index4), timestamp(@index5), timestamp(200), int(@index7), timestamp(@index8), @index9.getFullYear(), timestamp(75), int(@index11), timestamp(@index12), @index13.getMinutes(), @index6.getSeconds(), @index6.getFullYear(), @index13.getFullYear(), @index3 + @index17, @index18 + @index16, @index19 + @index3, @index20 + @index15, @index21 + @index10, @index22 + @index10, @index23 + @index14, @index24 + @index3], @index25 == 13934) -[BLOCK_RECURSION_DEPTH_2]: cel.@block([timestamp(1000000000), int(@index0), timestamp(@index1), @index2.getFullYear(), timestamp(50), int(@index4), timestamp(@index5), timestamp(200), int(@index7), timestamp(@index8), @index9.getFullYear(), timestamp(75), int(@index11), timestamp(@index12), @index13.getMinutes(), @index6.getSeconds(), @index6.getFullYear(), @index3 + @index13.getFullYear(), @index17 + @index16 + @index3, @index18 + @index15 + @index10, @index19 + @index10 + @index14, @index20 + @index3], @index21 == 13934) -[BLOCK_RECURSION_DEPTH_3]: cel.@block([timestamp(1000000000), int(@index0), timestamp(@index1), @index2.getFullYear(), timestamp(50), int(@index4), timestamp(@index5), timestamp(200), int(@index7), timestamp(@index8), @index9.getFullYear(), timestamp(75), int(@index11), timestamp(@index12), @index13.getMinutes(), @index6.getSeconds(), @index3 + @index13.getFullYear() + @index6.getFullYear(), @index16 + @index3 + @index15 + @index10, @index17 + @index10 + @index14 + @index3], @index18 == 13934) -[BLOCK_RECURSION_DEPTH_4]: cel.@block([timestamp(1000000000), int(@index0), timestamp(@index1), @index2.getFullYear(), timestamp(50), int(@index4), timestamp(@index5), timestamp(200), int(@index7), timestamp(@index8), @index9.getFullYear(), timestamp(75), int(@index11), timestamp(@index12), @index13.getMinutes(), @index6.getSeconds(), @index3 + @index13.getFullYear() + @index6.getFullYear() + @index3, @index16 + @index15 + @index10 + @index10 + @index14, @index17 + @index3], @index18 == 13934) -[BLOCK_RECURSION_DEPTH_5]: cel.@block([timestamp(1000000000), int(@index0), timestamp(@index1), @index2.getFullYear(), timestamp(50), int(@index4), timestamp(@index5), timestamp(200), int(@index7), timestamp(@index8), @index9.getFullYear(), timestamp(75), int(@index11), timestamp(@index12), @index13.getMinutes(), @index3 + @index13.getFullYear() + @index6.getFullYear() + @index3 + @index6.getSeconds(), @index15 + @index10 + @index10 + @index14 + @index3], @index16 == 13934) -[BLOCK_RECURSION_DEPTH_6]: cel.@block([timestamp(1000000000), int(@index0), timestamp(@index1), @index2.getFullYear(), timestamp(50), int(@index4), timestamp(@index5), timestamp(200), int(@index7), timestamp(@index8), @index9.getFullYear(), timestamp(75), int(@index11), timestamp(@index12), @index13.getMinutes(), @index3 + @index13.getFullYear() + @index6.getFullYear() + @index3 + @index6.getSeconds() + @index10, @index15 + @index10 + @index14 + @index3], @index16 == 13934) -[BLOCK_RECURSION_DEPTH_7]: cel.@block([timestamp(1000000000), int(@index0), timestamp(@index1), @index2.getFullYear(), timestamp(50), int(@index4), timestamp(@index5), timestamp(200), int(@index7), timestamp(@index8), @index9.getFullYear(), timestamp(75), int(@index11), timestamp(@index12), @index13.getMinutes(), @index3 + @index13.getFullYear() + @index6.getFullYear() + @index3 + @index6.getSeconds() + @index10 + @index10, @index15 + @index14 + @index3], @index16 == 13934) -[BLOCK_RECURSION_DEPTH_8]: cel.@block([timestamp(1000000000), int(@index0), timestamp(@index1), @index2.getFullYear(), timestamp(50), int(@index4), timestamp(@index5), timestamp(200), int(@index7), timestamp(@index8), @index9.getFullYear(), timestamp(75), int(@index11), timestamp(@index12), @index3 + @index13.getFullYear() + @index6.getFullYear() + @index3 + @index6.getSeconds() + @index10 + @index10 + @index13.getMinutes(), @index14 + @index3], @index15 == 13934) -[BLOCK_RECURSION_DEPTH_9]: cel.@block([timestamp(1000000000), int(@index0), timestamp(@index1), @index2.getFullYear(), timestamp(50), int(@index4), timestamp(@index5), timestamp(200), int(@index7), timestamp(@index8), @index9.getFullYear(), timestamp(75), int(@index11), timestamp(@index12), @index3 + @index13.getFullYear() + @index6.getFullYear() + @index3 + @index6.getSeconds() + @index10 + @index10 + @index13.getMinutes() + @index3], @index14 == 13934) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([timestamp(1000000000), int(@index0), timestamp(@index1), @index2.getFullYear(), timestamp(50), int(@index4), timestamp(@index5), timestamp(200), int(@index7), timestamp(@index8), @index9.getFullYear(), timestamp(75), int(@index11), timestamp(@index12), @index13.getFullYear(), @index3 + @index14, @index6.getFullYear(), @index15 + @index16, @index17 + @index3, @index6.getSeconds(), @index18 + @index19, @index20 + @index10, @index21 + @index10, @index13.getMinutes(), @index22 + @index23, @index24 + @index3], @index25 == 13934) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([int(timestamp(1000000000)), timestamp(@index0).getFullYear(), int(timestamp(50)), int(timestamp(200)), timestamp(@index3).getFullYear(), int(timestamp(75)), timestamp(@index2), timestamp(@index5), @index1 + @index7.getFullYear(), @index8 + @index6.getFullYear(), @index9 + @index1 + @index6.getSeconds(), @index10 + @index4 + @index4, @index11 + @index7.getMinutes()], @index12 + @index1 == 13934) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([timestamp(int(timestamp(1000000000))), timestamp(int(timestamp(50))), timestamp(int(timestamp(200))), timestamp(int(timestamp(75))), @index0.getFullYear(), @index2.getFullYear(), @index4 + @index3.getFullYear() + @index1.getFullYear(), @index6 + @index4 + @index1.getSeconds() + @index5, @index7 + @index5 + @index3.getMinutes() + @index4], @index8 == 13934) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([timestamp(int(timestamp(1000000000))).getFullYear(), timestamp(int(timestamp(200))).getFullYear(), timestamp(int(timestamp(50))), timestamp(int(timestamp(75))), @index0 + @index3.getFullYear() + @index2.getFullYear() + @index0, @index4 + @index2.getSeconds() + @index1 + @index1], @index5 + @index3.getMinutes() + @index0 == 13934) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([timestamp(int(timestamp(1000000000))).getFullYear(), timestamp(int(timestamp(200))).getFullYear(), timestamp(int(timestamp(50))), timestamp(int(timestamp(75))), @index0 + @index3.getFullYear() + @index2.getFullYear() + @index0 + @index2.getSeconds()], @index4 + @index1 + @index1 + @index3.getMinutes() + @index0 == 13934) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([timestamp(int(timestamp(1000000000))).getFullYear(), timestamp(int(timestamp(200))).getFullYear(), timestamp(int(timestamp(50))), timestamp(int(timestamp(75))), @index0 + @index3.getFullYear() + @index2.getFullYear() + @index0 + @index2.getSeconds() + @index1], @index4 + @index1 + @index3.getMinutes() + @index0 == 13934) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([timestamp(int(timestamp(1000000000))).getFullYear(), timestamp(int(timestamp(200))).getFullYear(), timestamp(int(timestamp(50))), timestamp(int(timestamp(75))), @index0 + @index3.getFullYear() + @index2.getFullYear() + @index0 + @index2.getSeconds() + @index1 + @index1], @index4 + @index3.getMinutes() + @index0 == 13934) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([timestamp(int(timestamp(1000000000))).getFullYear(), timestamp(int(timestamp(200))).getFullYear(), timestamp(int(timestamp(50))), timestamp(int(timestamp(75))), @index0 + @index3.getFullYear() + @index2.getFullYear() + @index0 + @index2.getSeconds() + @index1 + @index1 + @index3.getMinutes()], @index4 + @index0 == 13934) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([timestamp(int(timestamp(1000000000))).getFullYear(), timestamp(int(timestamp(200))).getFullYear(), timestamp(int(timestamp(50))), timestamp(int(timestamp(75))), @index0 + @index3.getFullYear() + @index2.getFullYear() + @index0 + @index2.getSeconds() + @index1 + @index1 + @index3.getMinutes() + @index0], @index4 == 13934) Test case: MAP_INDEX Source: {"a": 2}["a"] + {"a": 2}["a"] * {"a": 2}["a"] == 6 =====> Result: true -[CASCADED_BINDS]: cel.bind(@r0, {"a": 2}["a"], @r0 + @r0 * @r0) == 6 [BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([{"a": 2}["a"]], @index0 + @index0 * @index0 == 6) [BLOCK_RECURSION_DEPTH_1]: cel.@block([{"a": 2}, @index0["a"], @index1 * @index1, @index1 + @index2], @index3 == 6) -[BLOCK_RECURSION_DEPTH_2]: cel.@block([{"a": 2}, @index0["a"], @index1 + @index1 * @index1], @index2 == 6) -[BLOCK_RECURSION_DEPTH_3]: cel.@block([{"a": 2}, @index0["a"], @index1 + @index1 * @index1], @index2 == 6) -[BLOCK_RECURSION_DEPTH_4]: cel.@block([{"a": 2}, @index0["a"], @index1 + @index1 * @index1], @index2 == 6) -[BLOCK_RECURSION_DEPTH_5]: cel.@block([{"a": 2}, @index0["a"], @index1 + @index1 * @index1], @index2 == 6) -[BLOCK_RECURSION_DEPTH_6]: cel.@block([{"a": 2}, @index0["a"], @index1 + @index1 * @index1], @index2 == 6) -[BLOCK_RECURSION_DEPTH_7]: cel.@block([{"a": 2}, @index0["a"], @index1 + @index1 * @index1], @index2 == 6) -[BLOCK_RECURSION_DEPTH_8]: cel.@block([{"a": 2}, @index0["a"], @index1 + @index1 * @index1], @index2 == 6) -[BLOCK_RECURSION_DEPTH_9]: cel.@block([{"a": 2}, @index0["a"], @index1 + @index1 * @index1], @index2 == 6) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([{"a": 2}["a"], @index0 + @index0 * @index0], @index1 == 6) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([{"a": 2}["a"]], @index0 + @index0 * @index0 == 6) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([{"a": 2}["a"]], @index0 + @index0 * @index0 == 6) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([{"a": 2}["a"]], @index0 + @index0 * @index0 == 6) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([{"a": 2}["a"]], @index0 + @index0 * @index0 == 6) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([{"a": 2}["a"]], @index0 + @index0 * @index0 == 6) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([{"a": 2}["a"]], @index0 + @index0 * @index0 == 6) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([{"a": 2}["a"]], @index0 + @index0 * @index0 == 6) Test case: NESTED_MAP_CONSTRUCTION Source: {'a': {'b': 1}, 'c': {'b': 1}, 'd': {'e': {'b': 1}}, 'e': {'e': {'b': 1}}} =====> Result: {a={b=1}, c={b=1}, d={e={b=1}}, e={e={b=1}}} -[CASCADED_BINDS]: cel.bind(@r0, {"b": 1}, cel.bind(@r1, {"e": @r0}, {"a": @r0, "c": @r0, "d": @r1, "e": @r1})) [BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([{"b": 1}, {"e": @index0}], {"a": @index0, "c": @index0, "d": @index1, "e": @index1}) [BLOCK_RECURSION_DEPTH_1]: cel.@block([{"b": 1}, {"e": @index0}], {"a": @index0, "c": @index0, "d": @index1, "e": @index1}) -[BLOCK_RECURSION_DEPTH_2]: cel.@block([{"b": 1}, {"e": @index0}], {"a": @index0, "c": @index0, "d": @index1, "e": @index1}) -[BLOCK_RECURSION_DEPTH_3]: cel.@block([{"b": 1}, {"e": @index0}], {"a": @index0, "c": @index0, "d": @index1, "e": @index1}) -[BLOCK_RECURSION_DEPTH_4]: cel.@block([{"b": 1}, {"e": @index0}], {"a": @index0, "c": @index0, "d": @index1, "e": @index1}) -[BLOCK_RECURSION_DEPTH_5]: cel.@block([{"b": 1}, {"e": @index0}], {"a": @index0, "c": @index0, "d": @index1, "e": @index1}) -[BLOCK_RECURSION_DEPTH_6]: cel.@block([{"b": 1}, {"e": @index0}], {"a": @index0, "c": @index0, "d": @index1, "e": @index1}) -[BLOCK_RECURSION_DEPTH_7]: cel.@block([{"b": 1}, {"e": @index0}], {"a": @index0, "c": @index0, "d": @index1, "e": @index1}) -[BLOCK_RECURSION_DEPTH_8]: cel.@block([{"b": 1}, {"e": @index0}], {"a": @index0, "c": @index0, "d": @index1, "e": @index1}) -[BLOCK_RECURSION_DEPTH_9]: cel.@block([{"b": 1}, {"e": @index0}], {"a": @index0, "c": @index0, "d": @index1, "e": @index1}) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([{"e": {"b": 1}}, {"b": 1}], {"a": @index1, "c": @index1, "d": @index0, "e": @index0}) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([{"e": {"b": 1}}, {"b": 1}], {"a": @index1, "c": @index1, "d": @index0, "e": @index0}) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([{"e": {"b": 1}}, {"b": 1}], {"a": @index1, "c": @index1, "d": @index0, "e": @index0}) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([{"e": {"b": 1}}, {"b": 1}], {"a": @index1, "c": @index1, "d": @index0, "e": @index0}) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([{"e": {"b": 1}}, {"b": 1}], {"a": @index1, "c": @index1, "d": @index0, "e": @index0}) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([{"e": {"b": 1}}, {"b": 1}], {"a": @index1, "c": @index1, "d": @index0, "e": @index0}) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([{"e": {"b": 1}}, {"b": 1}], {"a": @index1, "c": @index1, "d": @index0, "e": @index0}) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([{"e": {"b": 1}}, {"b": 1}], {"a": @index1, "c": @index1, "d": @index0, "e": @index0}) Test case: NESTED_LIST_CONSTRUCTION Source: [1, [1,2,3,4], 2, [1,2,3,4], 5, [1,2,3,4], 7, [[1,2], [1,2,3,4]], [1,2]] =====> Result: [1, [1, 2, 3, 4], 2, [1, 2, 3, 4], 5, [1, 2, 3, 4], 7, [[1, 2], [1, 2, 3, 4]], [1, 2]] -[CASCADED_BINDS]: cel.bind(@r0, [1, 2, 3, 4], cel.bind(@r1, [1, 2], [1, @r0, 2, @r0, 5, @r0, 7, [@r1, @r0], @r1])) [BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([[1, 2, 3, 4], [1, 2]], [1, @index0, 2, @index0, 5, @index0, 7, [@index1, @index0], @index1]) [BLOCK_RECURSION_DEPTH_1]: cel.@block([[1, 2, 3, 4], [1, 2], [@index1, @index0]], [1, @index0, 2, @index0, 5, @index0, 7, @index2, @index1]) -[BLOCK_RECURSION_DEPTH_2]: cel.@block([[1, 2, 3, 4], [1, 2], [@index1, @index0]], [1, @index0, 2, @index0, 5, @index0, 7, @index2, @index1]) -[BLOCK_RECURSION_DEPTH_3]: cel.@block([[1, 2, 3, 4], [1, 2], [@index1, @index0]], [1, @index0, 2, @index0, 5, @index0, 7, @index2, @index1]) -[BLOCK_RECURSION_DEPTH_4]: cel.@block([[1, 2, 3, 4], [1, 2], [@index1, @index0]], [1, @index0, 2, @index0, 5, @index0, 7, @index2, @index1]) -[BLOCK_RECURSION_DEPTH_5]: cel.@block([[1, 2, 3, 4], [1, 2], [@index1, @index0]], [1, @index0, 2, @index0, 5, @index0, 7, @index2, @index1]) -[BLOCK_RECURSION_DEPTH_6]: cel.@block([[1, 2, 3, 4], [1, 2], [@index1, @index0]], [1, @index0, 2, @index0, 5, @index0, 7, @index2, @index1]) -[BLOCK_RECURSION_DEPTH_7]: cel.@block([[1, 2, 3, 4], [1, 2], [@index1, @index0]], [1, @index0, 2, @index0, 5, @index0, 7, @index2, @index1]) -[BLOCK_RECURSION_DEPTH_8]: cel.@block([[1, 2, 3, 4], [1, 2], [@index1, @index0]], [1, @index0, 2, @index0, 5, @index0, 7, @index2, @index1]) -[BLOCK_RECURSION_DEPTH_9]: cel.@block([[1, 2, 3, 4], [1, 2], [@index1, @index0]], [1, @index0, 2, @index0, 5, @index0, 7, @index2, @index1]) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([[1, 2, 3, 4], [1, 2]], [1, @index0, 2, @index0, 5, @index0, 7, [@index1, @index0], @index1]) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([[1, 2, 3, 4], [1, 2]], [1, @index0, 2, @index0, 5, @index0, 7, [@index1, @index0], @index1]) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([[1, 2, 3, 4], [1, 2]], [1, @index0, 2, @index0, 5, @index0, 7, [@index1, @index0], @index1]) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([[1, 2, 3, 4], [1, 2]], [1, @index0, 2, @index0, 5, @index0, 7, [@index1, @index0], @index1]) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([[1, 2, 3, 4], [1, 2]], [1, @index0, 2, @index0, 5, @index0, 7, [@index1, @index0], @index1]) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([[1, 2, 3, 4], [1, 2]], [1, @index0, 2, @index0, 5, @index0, 7, [@index1, @index0], @index1]) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([[1, 2, 3, 4], [1, 2]], [1, @index0, 2, @index0, 5, @index0, 7, [@index1, @index0], @index1]) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([[1, 2, 3, 4], [1, 2]], [1, @index0, 2, @index0, 5, @index0, 7, [@index1, @index0], @index1]) Test case: SELECT Source: msg.single_int64 + msg.single_int64 == 6 =====> Result: true -[CASCADED_BINDS]: cel.bind(@r0, msg.single_int64, @r0 + @r0) == 6 [BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([msg.single_int64], @index0 + @index0 == 6) [BLOCK_RECURSION_DEPTH_1]: cel.@block([msg.single_int64, @index0 + @index0], @index1 == 6) -[BLOCK_RECURSION_DEPTH_2]: cel.@block([msg.single_int64, @index0 + @index0], @index1 == 6) -[BLOCK_RECURSION_DEPTH_3]: cel.@block([msg.single_int64, @index0 + @index0], @index1 == 6) -[BLOCK_RECURSION_DEPTH_4]: cel.@block([msg.single_int64, @index0 + @index0], @index1 == 6) -[BLOCK_RECURSION_DEPTH_5]: cel.@block([msg.single_int64, @index0 + @index0], @index1 == 6) -[BLOCK_RECURSION_DEPTH_6]: cel.@block([msg.single_int64, @index0 + @index0], @index1 == 6) -[BLOCK_RECURSION_DEPTH_7]: cel.@block([msg.single_int64, @index0 + @index0], @index1 == 6) -[BLOCK_RECURSION_DEPTH_8]: cel.@block([msg.single_int64, @index0 + @index0], @index1 == 6) -[BLOCK_RECURSION_DEPTH_9]: cel.@block([msg.single_int64, @index0 + @index0], @index1 == 6) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([msg.single_int64], @index0 + @index0 == 6) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([msg.single_int64], @index0 + @index0 == 6) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([msg.single_int64], @index0 + @index0 == 6) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([msg.single_int64], @index0 + @index0 == 6) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([msg.single_int64], @index0 + @index0 == 6) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([msg.single_int64], @index0 + @index0 == 6) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([msg.single_int64], @index0 + @index0 == 6) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([msg.single_int64], @index0 + @index0 == 6) Test case: SELECT_NESTED_1 Source: msg.oneof_type.payload.single_int64 + msg.oneof_type.payload.single_int32 + msg.oneof_type.payload.single_int64 + msg.single_int64 + msg.oneof_type.payload.oneof_type.payload.single_int64 == 31 =====> Result: true -[CASCADED_BINDS]: cel.bind(@r0, msg.oneof_type.payload, cel.bind(@r1, @r0.single_int64, @r1 + @r0.single_int32 + @r1) + msg.single_int64 + @r0.oneof_type.payload.single_int64) == 31 [BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([msg.oneof_type.payload, @index0.single_int64], @index1 + @index0.single_int32 + @index1 + msg.single_int64 + @index0.oneof_type.payload.single_int64 == 31) -[BLOCK_RECURSION_DEPTH_1]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, @index1.oneof_type, @index3.payload, @index4.single_int64, msg.single_int64, @index1.single_int32, @index2 + @index7, @index8 + @index2, @index9 + @index6, @index10 + @index5], @index11 == 31) -[BLOCK_RECURSION_DEPTH_2]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, @index1.oneof_type.payload, @index3.single_int64, msg.single_int64, @index2 + @index1.single_int32, @index6 + @index2 + @index5, @index7 + @index4], @index8 == 31) -[BLOCK_RECURSION_DEPTH_3]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, @index1.oneof_type.payload.single_int64, msg.single_int64, @index2 + @index1.single_int32 + @index2, @index5 + @index4 + @index3], @index6 == 31) -[BLOCK_RECURSION_DEPTH_4]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, @index1.oneof_type.payload.single_int64, @index2 + @index1.single_int32 + @index2 + msg.single_int64, @index4 + @index3], @index5 == 31) -[BLOCK_RECURSION_DEPTH_5]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, @index2 + @index1.single_int32 + @index2 + msg.single_int64 + @index1.oneof_type.payload.single_int64], @index3 == 31) -[BLOCK_RECURSION_DEPTH_6]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, @index2 + @index1.single_int32 + @index2 + msg.single_int64 + @index1.oneof_type.payload.single_int64], @index3 == 31) -[BLOCK_RECURSION_DEPTH_7]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, @index2 + @index1.single_int32 + @index2 + msg.single_int64 + @index1.oneof_type.payload.single_int64], @index3 == 31) -[BLOCK_RECURSION_DEPTH_8]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, @index2 + @index1.single_int32 + @index2 + msg.single_int64 + @index1.oneof_type.payload.single_int64], @index3 == 31) -[BLOCK_RECURSION_DEPTH_9]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, @index2 + @index1.single_int32 + @index2 + msg.single_int64 + @index1.oneof_type.payload.single_int64], @index3 == 31) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, @index1.single_int32, @index2 + @index3, @index4 + @index2, msg.single_int64, @index5 + @index6, @index1.oneof_type, @index8.payload, @index9.single_int64, @index7 + @index10], @index11 == 31) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([msg.oneof_type.payload, @index0.single_int64, @index1 + @index0.single_int32, @index2 + @index1 + msg.single_int64, @index0.oneof_type.payload, @index3 + @index4.single_int64], @index5 == 31) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([msg.oneof_type.payload.single_int64, msg.oneof_type.payload, @index0 + @index1.single_int32 + @index0, @index1.oneof_type.payload.single_int64, @index2 + msg.single_int64 + @index3], @index4 == 31) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([msg.oneof_type.payload.single_int64, msg.oneof_type.payload, @index0 + @index1.single_int32 + @index0 + msg.single_int64, @index2 + @index1.oneof_type.payload.single_int64], @index3 == 31) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([msg.oneof_type.payload.single_int64, msg.oneof_type.payload, @index0 + @index1.single_int32 + @index0 + msg.single_int64 + @index1.oneof_type.payload.single_int64], @index2 == 31) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([msg.oneof_type.payload.single_int64, msg.oneof_type.payload], @index0 + @index1.single_int32 + @index0 + msg.single_int64 + @index1.oneof_type.payload.single_int64 == 31) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([msg.oneof_type.payload.single_int64, msg.oneof_type.payload], @index0 + @index1.single_int32 + @index0 + msg.single_int64 + @index1.oneof_type.payload.single_int64 == 31) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([msg.oneof_type.payload.single_int64, msg.oneof_type.payload], @index0 + @index1.single_int32 + @index0 + msg.single_int64 + @index1.oneof_type.payload.single_int64 == 31) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([msg.oneof_type.payload.single_int64, msg.oneof_type.payload], @index0 + @index1.single_int32 + @index0 + msg.single_int64 + @index1.oneof_type.payload.single_int64 == 31) Test case: SELECT_NESTED_2 Source: true || msg.oneof_type.payload.oneof_type.payload.oneof_type.payload.oneof_type.payload.single_bool || msg.oneof_type.payload.oneof_type.payload.oneof_type.child.child.payload.single_bool =====> Result: true -[CASCADED_BINDS]: cel.bind(@r0, msg.oneof_type.payload.oneof_type.payload.oneof_type, true || @r0.payload.oneof_type.payload.single_bool || @r0.child.child.payload.single_bool) [BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([msg.oneof_type.payload.oneof_type.payload.oneof_type], true || @index0.payload.oneof_type.payload.single_bool || @index0.child.child.payload.single_bool) -[BLOCK_RECURSION_DEPTH_1]: cel.@block([msg.oneof_type, @index0.payload, @index1.oneof_type, @index2.payload, @index3.oneof_type, @index4.child, @index5.child, @index6.payload, @index7.single_bool, @index4.payload, @index9.oneof_type, @index10.payload, @index11.single_bool, true || @index12], @index13 || @index8) -[BLOCK_RECURSION_DEPTH_2]: cel.@block([msg.oneof_type, @index0.payload, @index1.oneof_type, @index2.payload, @index3.oneof_type, @index4.child.child, @index5.payload.single_bool, @index4.payload.oneof_type, @index7.payload.single_bool, true || @index8], @index9 || @index6) -[BLOCK_RECURSION_DEPTH_3]: cel.@block([msg.oneof_type, @index0.payload, @index1.oneof_type, @index2.payload, @index3.oneof_type, @index4.child.child.payload, @index5.single_bool, @index4.payload.oneof_type.payload, true || @index7.single_bool], @index8 || @index6) -[BLOCK_RECURSION_DEPTH_4]: cel.@block([msg.oneof_type, @index0.payload, @index1.oneof_type, @index2.payload, @index3.oneof_type, @index4.child.child.payload.single_bool, @index4.payload.oneof_type.payload.single_bool, true || @index6], @index7 || @index5) -[BLOCK_RECURSION_DEPTH_5]: cel.@block([msg.oneof_type, @index0.payload, @index1.oneof_type, @index2.payload, @index3.oneof_type, @index4.child.child.payload.single_bool, true || @index4.payload.oneof_type.payload.single_bool], @index6 || @index5) -[BLOCK_RECURSION_DEPTH_6]: cel.@block([msg.oneof_type, @index0.payload, @index1.oneof_type, @index2.payload, @index3.oneof_type, @index4.child.child.payload.single_bool, true || @index4.payload.oneof_type.payload.single_bool], @index6 || @index5) -[BLOCK_RECURSION_DEPTH_7]: cel.@block([msg.oneof_type, @index0.payload, @index1.oneof_type, @index2.payload, @index3.oneof_type, @index4.child.child.payload.single_bool, true || @index4.payload.oneof_type.payload.single_bool], @index6 || @index5) -[BLOCK_RECURSION_DEPTH_8]: cel.@block([msg.oneof_type, @index0.payload, @index1.oneof_type, @index2.payload, @index3.oneof_type, @index4.child.child.payload.single_bool, true || @index4.payload.oneof_type.payload.single_bool], @index6 || @index5) -[BLOCK_RECURSION_DEPTH_9]: cel.@block([msg.oneof_type, @index0.payload, @index1.oneof_type, @index2.payload, @index3.oneof_type, @index4.child.child.payload.single_bool, true || @index4.payload.oneof_type.payload.single_bool], @index6 || @index5) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([msg.oneof_type, @index0.payload, @index1.oneof_type, @index2.payload, @index3.oneof_type, @index4.payload, @index5.oneof_type, @index6.payload, @index7.single_bool, true || @index8, @index4.child, @index10.child, @index11.payload, @index12.single_bool], @index9 || @index13) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([msg.oneof_type.payload, @index0.oneof_type.payload, @index1.oneof_type, @index2.payload.oneof_type, @index3.payload.single_bool, @index2.child.child, @index5.payload.single_bool], true || @index4 || @index6) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([msg.oneof_type.payload.oneof_type, @index0.payload.oneof_type, @index1.payload.oneof_type.payload, @index1.child.child.payload], true || @index2.single_bool || @index3.single_bool) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([msg.oneof_type.payload.oneof_type.payload, @index0.oneof_type, @index1.payload.oneof_type.payload.single_bool, @index1.child.child.payload.single_bool], true || @index2 || @index3) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([msg.oneof_type.payload.oneof_type.payload.oneof_type, true || @index0.payload.oneof_type.payload.single_bool], @index1 || @index0.child.child.payload.single_bool) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([msg.oneof_type.payload.oneof_type.payload.oneof_type], true || @index0.payload.oneof_type.payload.single_bool || @index0.child.child.payload.single_bool) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([msg.oneof_type.payload.oneof_type.payload.oneof_type], true || @index0.payload.oneof_type.payload.single_bool || @index0.child.child.payload.single_bool) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([msg.oneof_type.payload.oneof_type.payload.oneof_type], true || @index0.payload.oneof_type.payload.single_bool || @index0.child.child.payload.single_bool) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([msg.oneof_type.payload.oneof_type.payload.oneof_type], true || @index0.payload.oneof_type.payload.single_bool || @index0.child.child.payload.single_bool) Test case: SELECT_NESTED_MESSAGE_MAP_INDEX_1 Source: msg.oneof_type.payload.map_int32_int64[1] + msg.oneof_type.payload.map_int32_int64[1] + msg.oneof_type.payload.map_int32_int64[1] == 15 =====> Result: true -[CASCADED_BINDS]: cel.bind(@r0, msg.oneof_type.payload.map_int32_int64[1], @r0 + @r0 + @r0) == 15 [BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([msg.oneof_type.payload.map_int32_int64[1]], @index0 + @index0 + @index0 == 15) [BLOCK_RECURSION_DEPTH_1]: cel.@block([msg.oneof_type, @index0.payload, @index1.map_int32_int64, @index2[1], @index3 + @index3, @index4 + @index3], @index5 == 15) -[BLOCK_RECURSION_DEPTH_2]: cel.@block([msg.oneof_type, @index0.payload, @index1.map_int32_int64, @index2[1], @index3 + @index3 + @index3], @index4 == 15) -[BLOCK_RECURSION_DEPTH_3]: cel.@block([msg.oneof_type, @index0.payload, @index1.map_int32_int64, @index2[1], @index3 + @index3 + @index3], @index4 == 15) -[BLOCK_RECURSION_DEPTH_4]: cel.@block([msg.oneof_type, @index0.payload, @index1.map_int32_int64, @index2[1], @index3 + @index3 + @index3], @index4 == 15) -[BLOCK_RECURSION_DEPTH_5]: cel.@block([msg.oneof_type, @index0.payload, @index1.map_int32_int64, @index2[1], @index3 + @index3 + @index3], @index4 == 15) -[BLOCK_RECURSION_DEPTH_6]: cel.@block([msg.oneof_type, @index0.payload, @index1.map_int32_int64, @index2[1], @index3 + @index3 + @index3], @index4 == 15) -[BLOCK_RECURSION_DEPTH_7]: cel.@block([msg.oneof_type, @index0.payload, @index1.map_int32_int64, @index2[1], @index3 + @index3 + @index3], @index4 == 15) -[BLOCK_RECURSION_DEPTH_8]: cel.@block([msg.oneof_type, @index0.payload, @index1.map_int32_int64, @index2[1], @index3 + @index3 + @index3], @index4 == 15) -[BLOCK_RECURSION_DEPTH_9]: cel.@block([msg.oneof_type, @index0.payload, @index1.map_int32_int64, @index2[1], @index3 + @index3 + @index3], @index4 == 15) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([msg.oneof_type.payload, @index0.map_int32_int64[1], @index1 + @index1 + @index1], @index2 == 15) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([msg.oneof_type.payload.map_int32_int64, @index0[1]], @index1 + @index1 + @index1 == 15) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([msg.oneof_type.payload.map_int32_int64[1]], @index0 + @index0 + @index0 == 15) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([msg.oneof_type.payload.map_int32_int64[1]], @index0 + @index0 + @index0 == 15) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([msg.oneof_type.payload.map_int32_int64[1]], @index0 + @index0 + @index0 == 15) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([msg.oneof_type.payload.map_int32_int64[1]], @index0 + @index0 + @index0 == 15) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([msg.oneof_type.payload.map_int32_int64[1]], @index0 + @index0 + @index0 == 15) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([msg.oneof_type.payload.map_int32_int64[1]], @index0 + @index0 + @index0 == 15) Test case: SELECT_NESTED_MESSAGE_MAP_INDEX_2 Source: msg.oneof_type.payload.map_int32_int64[0] + msg.oneof_type.payload.map_int32_int64[1] + msg.oneof_type.payload.map_int32_int64[2] == 8 =====> Result: true -[CASCADED_BINDS]: cel.bind(@r0, msg.oneof_type.payload.map_int32_int64, @r0[0] + @r0[1] + @r0[2]) == 8 [BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([msg.oneof_type.payload.map_int32_int64], @index0[0] + @index0[1] + @index0[2] == 8) -[BLOCK_RECURSION_DEPTH_1]: cel.@block([msg.oneof_type, @index0.payload, @index1.map_int32_int64, @index2[2], @index2[1], @index2[0], @index5 + @index4, @index6 + @index3], @index7 == 8) -[BLOCK_RECURSION_DEPTH_2]: cel.@block([msg.oneof_type, @index0.payload, @index1.map_int32_int64, @index2[2], @index2[0] + @index2[1], @index4 + @index3], @index5 == 8) -[BLOCK_RECURSION_DEPTH_3]: cel.@block([msg.oneof_type, @index0.payload, @index1.map_int32_int64, @index2[0] + @index2[1] + @index2[2]], @index3 == 8) -[BLOCK_RECURSION_DEPTH_4]: cel.@block([msg.oneof_type, @index0.payload, @index1.map_int32_int64, @index2[0] + @index2[1] + @index2[2]], @index3 == 8) -[BLOCK_RECURSION_DEPTH_5]: cel.@block([msg.oneof_type, @index0.payload, @index1.map_int32_int64, @index2[0] + @index2[1] + @index2[2]], @index3 == 8) -[BLOCK_RECURSION_DEPTH_6]: cel.@block([msg.oneof_type, @index0.payload, @index1.map_int32_int64, @index2[0] + @index2[1] + @index2[2]], @index3 == 8) -[BLOCK_RECURSION_DEPTH_7]: cel.@block([msg.oneof_type, @index0.payload, @index1.map_int32_int64, @index2[0] + @index2[1] + @index2[2]], @index3 == 8) -[BLOCK_RECURSION_DEPTH_8]: cel.@block([msg.oneof_type, @index0.payload, @index1.map_int32_int64, @index2[0] + @index2[1] + @index2[2]], @index3 == 8) -[BLOCK_RECURSION_DEPTH_9]: cel.@block([msg.oneof_type, @index0.payload, @index1.map_int32_int64, @index2[0] + @index2[1] + @index2[2]], @index3 == 8) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([msg.oneof_type, @index0.payload, @index1.map_int32_int64, @index2[0], @index2[1], @index3 + @index4, @index2[2], @index5 + @index6], @index7 == 8) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([msg.oneof_type.payload, @index0.map_int32_int64, @index1[0] + @index1[1], @index2 + @index1[2]], @index3 == 8) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([msg.oneof_type.payload.map_int32_int64, @index0[0] + @index0[1] + @index0[2]], @index1 == 8) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([msg.oneof_type.payload.map_int32_int64], @index0[0] + @index0[1] + @index0[2] == 8) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([msg.oneof_type.payload.map_int32_int64], @index0[0] + @index0[1] + @index0[2] == 8) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([msg.oneof_type.payload.map_int32_int64], @index0[0] + @index0[1] + @index0[2] == 8) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([msg.oneof_type.payload.map_int32_int64], @index0[0] + @index0[1] + @index0[2] == 8) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([msg.oneof_type.payload.map_int32_int64], @index0[0] + @index0[1] + @index0[2] == 8) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([msg.oneof_type.payload.map_int32_int64], @index0[0] + @index0[1] + @index0[2] == 8) Test case: SELECT_NESTED_NO_COMMON_SUBEXPR Source: msg.oneof_type.payload.oneof_type.payload.oneof_type.payload.oneof_type.payload.single_int64 =====> Result: 0 -[CASCADED_BINDS]: msg.oneof_type.payload.oneof_type.payload.oneof_type.payload.oneof_type.payload.single_int64 [BLOCK_COMMON_SUBEXPR_ONLY]: msg.oneof_type.payload.oneof_type.payload.oneof_type.payload.oneof_type.payload.single_int64 [BLOCK_RECURSION_DEPTH_1]: cel.@block([msg.oneof_type, @index0.payload, @index1.oneof_type, @index2.payload, @index3.oneof_type, @index4.payload, @index5.oneof_type, @index6.payload], @index7.single_int64) [BLOCK_RECURSION_DEPTH_2]: cel.@block([msg.oneof_type.payload, @index0.oneof_type.payload, @index1.oneof_type.payload, @index2.oneof_type.payload], @index3.single_int64) -[BLOCK_RECURSION_DEPTH_3]: cel.@block([msg.oneof_type.payload.oneof_type, @index0.payload.oneof_type.payload, @index1.oneof_type.payload], @index2.single_int64) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([msg.oneof_type.payload.oneof_type, @index0.payload.oneof_type.payload], @index1.oneof_type.payload.single_int64) [BLOCK_RECURSION_DEPTH_4]: cel.@block([msg.oneof_type.payload.oneof_type.payload, @index0.oneof_type.payload.oneof_type.payload], @index1.single_int64) -[BLOCK_RECURSION_DEPTH_5]: cel.@block([msg.oneof_type.payload.oneof_type.payload.oneof_type, @index0.payload.oneof_type.payload], @index1.single_int64) -[BLOCK_RECURSION_DEPTH_6]: cel.@block([msg.oneof_type.payload.oneof_type.payload.oneof_type.payload, @index0.oneof_type.payload], @index1.single_int64) -[BLOCK_RECURSION_DEPTH_7]: cel.@block([msg.oneof_type.payload.oneof_type.payload.oneof_type.payload.oneof_type, @index0.payload], @index1.single_int64) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([msg.oneof_type.payload.oneof_type.payload.oneof_type], @index0.payload.oneof_type.payload.single_int64) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([msg.oneof_type.payload.oneof_type.payload.oneof_type.payload], @index0.oneof_type.payload.single_int64) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([msg.oneof_type.payload.oneof_type.payload.oneof_type.payload.oneof_type], @index0.payload.single_int64) [BLOCK_RECURSION_DEPTH_8]: cel.@block([msg.oneof_type.payload.oneof_type.payload.oneof_type.payload.oneof_type.payload], @index0.single_int64) -[BLOCK_RECURSION_DEPTH_9]: cel.@block([msg.oneof_type.payload.oneof_type.payload.oneof_type.payload.oneof_type.payload], @index0.single_int64) +[BLOCK_RECURSION_DEPTH_9]: msg.oneof_type.payload.oneof_type.payload.oneof_type.payload.oneof_type.payload.single_int64 Test case: TERNARY Source: (msg.single_int64 > 0 ? msg.single_int64 : 0) == 3 =====> Result: true -[CASCADED_BINDS]: cel.bind(@r0, msg.single_int64, (@r0 > 0) ? @r0 : 0) == 3 [BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([msg.single_int64], ((@index0 > 0) ? @index0 : 0) == 3) [BLOCK_RECURSION_DEPTH_1]: cel.@block([msg.single_int64, @index0 > 0, @index1 ? @index0 : 0], @index2 == 3) [BLOCK_RECURSION_DEPTH_2]: cel.@block([msg.single_int64, (@index0 > 0) ? @index0 : 0], @index1 == 3) -[BLOCK_RECURSION_DEPTH_3]: cel.@block([msg.single_int64, (@index0 > 0) ? @index0 : 0], @index1 == 3) -[BLOCK_RECURSION_DEPTH_4]: cel.@block([msg.single_int64, (@index0 > 0) ? @index0 : 0], @index1 == 3) -[BLOCK_RECURSION_DEPTH_5]: cel.@block([msg.single_int64, (@index0 > 0) ? @index0 : 0], @index1 == 3) -[BLOCK_RECURSION_DEPTH_6]: cel.@block([msg.single_int64, (@index0 > 0) ? @index0 : 0], @index1 == 3) -[BLOCK_RECURSION_DEPTH_7]: cel.@block([msg.single_int64, (@index0 > 0) ? @index0 : 0], @index1 == 3) -[BLOCK_RECURSION_DEPTH_8]: cel.@block([msg.single_int64, (@index0 > 0) ? @index0 : 0], @index1 == 3) -[BLOCK_RECURSION_DEPTH_9]: cel.@block([msg.single_int64, (@index0 > 0) ? @index0 : 0], @index1 == 3) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([msg.single_int64], ((@index0 > 0) ? @index0 : 0) == 3) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([msg.single_int64], ((@index0 > 0) ? @index0 : 0) == 3) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([msg.single_int64], ((@index0 > 0) ? @index0 : 0) == 3) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([msg.single_int64], ((@index0 > 0) ? @index0 : 0) == 3) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([msg.single_int64], ((@index0 > 0) ? @index0 : 0) == 3) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([msg.single_int64], ((@index0 > 0) ? @index0 : 0) == 3) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([msg.single_int64], ((@index0 > 0) ? @index0 : 0) == 3) Test case: TERNARY_BIND_RHS_ONLY Source: false ? false : (msg.single_int64) + ((msg.single_int64 + 1) * 2) == 11 =====> Result: true -[CASCADED_BINDS]: false ? false : (cel.bind(@r0, msg.single_int64, @r0 + (@r0 + 1) * 2) == 11) [BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([msg.single_int64], false ? false : (@index0 + (@index0 + 1) * 2 == 11)) [BLOCK_RECURSION_DEPTH_1]: cel.@block([msg.single_int64, @index0 + 1, @index1 * 2, @index0 + @index2, @index3 == 11], false ? false : @index4) [BLOCK_RECURSION_DEPTH_2]: cel.@block([msg.single_int64, (@index0 + 1) * 2, @index0 + @index1 == 11], false ? false : @index2) -[BLOCK_RECURSION_DEPTH_3]: cel.@block([msg.single_int64, @index0 + (@index0 + 1) * 2, @index1 == 11], false ? false : @index2) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([msg.single_int64, @index0 + (@index0 + 1) * 2], false ? false : (@index1 == 11)) [BLOCK_RECURSION_DEPTH_4]: cel.@block([msg.single_int64, @index0 + (@index0 + 1) * 2 == 11], false ? false : @index1) -[BLOCK_RECURSION_DEPTH_5]: cel.@block([msg.single_int64, @index0 + (@index0 + 1) * 2 == 11], false ? false : @index1) -[BLOCK_RECURSION_DEPTH_6]: cel.@block([msg.single_int64, @index0 + (@index0 + 1) * 2 == 11], false ? false : @index1) -[BLOCK_RECURSION_DEPTH_7]: cel.@block([msg.single_int64, @index0 + (@index0 + 1) * 2 == 11], false ? false : @index1) -[BLOCK_RECURSION_DEPTH_8]: cel.@block([msg.single_int64, @index0 + (@index0 + 1) * 2 == 11], false ? false : @index1) -[BLOCK_RECURSION_DEPTH_9]: cel.@block([msg.single_int64, @index0 + (@index0 + 1) * 2 == 11], false ? false : @index1) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([msg.single_int64], false ? false : (@index0 + (@index0 + 1) * 2 == 11)) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([msg.single_int64], false ? false : (@index0 + (@index0 + 1) * 2 == 11)) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([msg.single_int64], false ? false : (@index0 + (@index0 + 1) * 2 == 11)) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([msg.single_int64], false ? false : (@index0 + (@index0 + 1) * 2 == 11)) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([msg.single_int64], false ? false : (@index0 + (@index0 + 1) * 2 == 11)) Test case: NESTED_TERNARY Source: (msg.single_int64 > 0 ? (msg.single_int32 > 0 ? msg.single_int64 + msg.single_int32 : 0) : 0) == 8 =====> Result: true -[CASCADED_BINDS]: cel.bind(@r0, msg.single_int64, (@r0 > 0) ? cel.bind(@r1, msg.single_int32, (@r1 > 0) ? (@r0 + @r1) : 0) : 0) == 8 [BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([msg.single_int64, msg.single_int32], ((@index0 > 0) ? ((@index1 > 0) ? (@index0 + @index1) : 0) : 0) == 8) -[BLOCK_RECURSION_DEPTH_1]: cel.@block([msg.single_int64, msg.single_int32, @index0 + @index1, @index1 > 0, @index3 ? @index2 : 0, @index0 > 0, @index5 ? @index4 : 0], @index6 == 8) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([msg.single_int64, msg.single_int32, @index0 > 0, @index1 > 0, @index0 + @index1, @index3 ? @index4 : 0, @index2 ? @index5 : 0], @index6 == 8) [BLOCK_RECURSION_DEPTH_2]: cel.@block([msg.single_int64, msg.single_int32, (@index1 > 0) ? (@index0 + @index1) : 0, (@index0 > 0) ? @index2 : 0], @index3 == 8) [BLOCK_RECURSION_DEPTH_3]: cel.@block([msg.single_int64, msg.single_int32, (@index0 > 0) ? ((@index1 > 0) ? (@index0 + @index1) : 0) : 0], @index2 == 8) -[BLOCK_RECURSION_DEPTH_4]: cel.@block([msg.single_int64, msg.single_int32, (@index0 > 0) ? ((@index1 > 0) ? (@index0 + @index1) : 0) : 0], @index2 == 8) -[BLOCK_RECURSION_DEPTH_5]: cel.@block([msg.single_int64, msg.single_int32, (@index0 > 0) ? ((@index1 > 0) ? (@index0 + @index1) : 0) : 0], @index2 == 8) -[BLOCK_RECURSION_DEPTH_6]: cel.@block([msg.single_int64, msg.single_int32, (@index0 > 0) ? ((@index1 > 0) ? (@index0 + @index1) : 0) : 0], @index2 == 8) -[BLOCK_RECURSION_DEPTH_7]: cel.@block([msg.single_int64, msg.single_int32, (@index0 > 0) ? ((@index1 > 0) ? (@index0 + @index1) : 0) : 0], @index2 == 8) -[BLOCK_RECURSION_DEPTH_8]: cel.@block([msg.single_int64, msg.single_int32, (@index0 > 0) ? ((@index1 > 0) ? (@index0 + @index1) : 0) : 0], @index2 == 8) -[BLOCK_RECURSION_DEPTH_9]: cel.@block([msg.single_int64, msg.single_int32, (@index0 > 0) ? ((@index1 > 0) ? (@index0 + @index1) : 0) : 0], @index2 == 8) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([msg.single_int64, msg.single_int32], ((@index0 > 0) ? ((@index1 > 0) ? (@index0 + @index1) : 0) : 0) == 8) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([msg.single_int64, msg.single_int32], ((@index0 > 0) ? ((@index1 > 0) ? (@index0 + @index1) : 0) : 0) == 8) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([msg.single_int64, msg.single_int32], ((@index0 > 0) ? ((@index1 > 0) ? (@index0 + @index1) : 0) : 0) == 8) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([msg.single_int64, msg.single_int32], ((@index0 > 0) ? ((@index1 > 0) ? (@index0 + @index1) : 0) : 0) == 8) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([msg.single_int64, msg.single_int32], ((@index0 > 0) ? ((@index1 > 0) ? (@index0 + @index1) : 0) : 0) == 8) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([msg.single_int64, msg.single_int32], ((@index0 > 0) ? ((@index1 > 0) ? (@index0 + @index1) : 0) : 0) == 8) Test case: MULTIPLE_MACROS_1 Source: size([[1].exists(i, i > 0)]) + size([[1].exists(j, j > 0)]) + size([[2].exists(k, k > 1)]) + size([[2].exists(l, l > 1)]) == 4 =====> Result: true -[CASCADED_BINDS]: cel.bind(@r1, size([[2].exists(@c0:0, @c0:0 > 1)]), cel.bind(@r0, size([[1].exists(@c0:0, @c0:0 > 0)]), @r0 + @r0) + @r1 + @r1) == 4 -[BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([size([[1].exists(@c0:0, @c0:0 > 0)]), size([[2].exists(@c0:0, @c0:0 > 1)])], @index0 + @index0 + @index1 + @index1 == 4) -[BLOCK_RECURSION_DEPTH_1]: cel.@block([[1], @c0:0 > 0, @x0:0 || @index1, [2], @c0:0 > 1, @x0:0 || @index4], size([@index0.exists(@c0:0, @index1)]) + size([@index0.exists(@c0:0, @index1)]) + size([@index3.exists(@c0:0, @index4)]) + size([@index3.exists(@c0:0, @index4)]) == 4) -[BLOCK_RECURSION_DEPTH_2]: cel.@block([[1], @c0:0 > 0, @x0:0 || @index1, [2], @c0:0 > 1, @x0:0 || @index4], size([@index0.exists(@c0:0, @index1)]) + size([@index0.exists(@c0:0, @index1)]) + size([@index3.exists(@c0:0, @index4)]) + size([@index3.exists(@c0:0, @index4)]) == 4) -[BLOCK_RECURSION_DEPTH_3]: cel.@block([[1], @c0:0 > 0, @x0:0 || @index1, @index0.exists(@c0:0, @index1), [@index3], size(@index4), [2], @c0:0 > 1, @x0:0 || @index7, @index6.exists(@c0:0, @index7), [@index9], size(@index10), @index5 + @index5 + @index11 + @index11], @index12 == 4) -[BLOCK_RECURSION_DEPTH_4]: cel.@block([[1], @c0:0 > 0, @x0:0 || @index1, @index0.exists(@c0:0, @index1), [@index3], size(@index4), [2], @c0:0 > 1, @x0:0 || @index7, @index6.exists(@c0:0, @index7), [@index9], size(@index10), @index5 + @index5 + @index11 + @index11], @index12 == 4) -[BLOCK_RECURSION_DEPTH_5]: cel.@block([[1], @c0:0 > 0, @x0:0 || @index1, @index0.exists(@c0:0, @index1), [@index3], size(@index4), [2], @c0:0 > 1, @x0:0 || @index7, @index6.exists(@c0:0, @index7), [@index9], size(@index10), @index5 + @index5 + @index11 + @index11], @index12 == 4) -[BLOCK_RECURSION_DEPTH_6]: cel.@block([[1], @c0:0 > 0, @x0:0 || @index1, @index0.exists(@c0:0, @index1), [@index3], size(@index4), [2], @c0:0 > 1, @x0:0 || @index7, @index6.exists(@c0:0, @index7), [@index9], size(@index10), @index5 + @index5 + @index11 + @index11], @index12 == 4) -[BLOCK_RECURSION_DEPTH_7]: cel.@block([[1], @c0:0 > 0, @x0:0 || @index1, @index0.exists(@c0:0, @index1), [@index3], size(@index4), [2], @c0:0 > 1, @x0:0 || @index7, @index6.exists(@c0:0, @index7), [@index9], size(@index10), @index5 + @index5 + @index11 + @index11], @index12 == 4) -[BLOCK_RECURSION_DEPTH_8]: cel.@block([[1], @c0:0 > 0, @x0:0 || @index1, @index0.exists(@c0:0, @index1), [@index3], size(@index4), [2], @c0:0 > 1, @x0:0 || @index7, @index6.exists(@c0:0, @index7), [@index9], size(@index10), @index5 + @index5 + @index11 + @index11], @index12 == 4) -[BLOCK_RECURSION_DEPTH_9]: cel.@block([[1], @c0:0 > 0, @x0:0 || @index1, @index0.exists(@c0:0, @index1), [@index3], size(@index4), [2], @c0:0 > 1, @x0:0 || @index7, @index6.exists(@c0:0, @index7), [@index9], size(@index10), @index5 + @index5 + @index11 + @index11], @index12 == 4) +[BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([[1].exists(@it:0:0, @it:0:0 > 0), size([@index0]), [2].exists(@it:0:0, @it:0:0 > 1), size([@index2])], @index1 + @index1 + @index3 + @index3 == 4) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([[1], [2]], size([@index0.exists(@it:0:0, @it:0:0 > 0)]) + size([@index0.exists(@it:0:0, @it:0:0 > 0)]) + size([@index1.exists(@it:0:0, @it:0:0 > 1)]) + size([@index1.exists(@it:0:0, @it:0:0 > 1)]) == 4) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([[1], [2]], size([@index0.exists(@it:0:0, @it:0:0 > 0)]) + size([@index0.exists(@it:0:0, @it:0:0 > 0)]) + size([@index1.exists(@it:0:0, @it:0:0 > 1)]) + size([@index1.exists(@it:0:0, @it:0:0 > 1)]) == 4) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([[1].exists(@it:0:0, @it:0:0 > 0), [2].exists(@it:0:0, @it:0:0 > 1), size([@index0]), size([@index1]), @index2 + @index2 + @index3 + @index3], @index4 == 4) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([[1].exists(@it:0:0, @it:0:0 > 0), [2].exists(@it:0:0, @it:0:0 > 1), size([@index0]), size([@index1])], @index2 + @index2 + @index3 + @index3 == 4) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([[1].exists(@it:0:0, @it:0:0 > 0), [2].exists(@it:0:0, @it:0:0 > 1), size([@index0]), size([@index1])], @index2 + @index2 + @index3 + @index3 == 4) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([[1].exists(@it:0:0, @it:0:0 > 0), [2].exists(@it:0:0, @it:0:0 > 1), size([@index0]), size([@index1])], @index2 + @index2 + @index3 + @index3 == 4) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([[1].exists(@it:0:0, @it:0:0 > 0), [2].exists(@it:0:0, @it:0:0 > 1), size([@index0]), size([@index1])], @index2 + @index2 + @index3 + @index3 == 4) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([[1].exists(@it:0:0, @it:0:0 > 0), [2].exists(@it:0:0, @it:0:0 > 1), size([@index0]), size([@index1])], @index2 + @index2 + @index3 + @index3 == 4) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([[1].exists(@it:0:0, @it:0:0 > 0), [2].exists(@it:0:0, @it:0:0 > 1), size([@index0]), size([@index1])], @index2 + @index2 + @index3 + @index3 == 4) Test case: MULTIPLE_MACROS_2 Source: [[1].exists(i, i > 0)] + [[1].exists(j, j > 0)] + [['a'].exists(k, k == 'a')] + [['a'].exists(l, l == 'a')] == [true, true, true, true] =====> Result: true -[CASCADED_BINDS]: cel.bind(@r1, [["a"].exists(@c0:1, @c0:1 == "a")], cel.bind(@r0, [[1].exists(@c0:0, @c0:0 > 0)], @r0 + @r0) + @r1 + @r1) == [true, true, true, true] -[BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([[[1].exists(@c0:0, @c0:0 > 0)], [["a"].exists(@c0:1, @c0:1 == "a")]], @index0 + @index0 + @index1 + @index1 == [true, true, true, true]) -[BLOCK_RECURSION_DEPTH_1]: cel.@block([[1], @c0:0 > 0, @x0:0 || @index1, ["a"], @c0:1 == "a", @x0:1 || @index4, [true, true, true, true]], [@index0.exists(@c0:0, @index1)] + [@index0.exists(@c0:0, @index1)] + [@index3.exists(@c0:1, @index4)] + [@index3.exists(@c0:1, @index4)] == @index6) -[BLOCK_RECURSION_DEPTH_2]: cel.@block([[1], @c0:0 > 0, @x0:0 || @index1, ["a"], @c0:1 == "a", @x0:1 || @index4, [true, true, true, true]], [@index0.exists(@c0:0, @index1)] + [@index0.exists(@c0:0, @index1)] + [@index3.exists(@c0:1, @index4)] + [@index3.exists(@c0:1, @index4)] == @index6) -[BLOCK_RECURSION_DEPTH_3]: cel.@block([[1], @c0:0 > 0, @x0:0 || @index1, @index0.exists(@c0:0, @index1), [@index3], ["a"], @c0:1 == "a", @x0:1 || @index6, @index5.exists(@c0:1, @index6), [@index8], [true, true, true, true], @index4 + @index4 + @index9 + @index9], @index11 == @index10) -[BLOCK_RECURSION_DEPTH_4]: cel.@block([[1], @c0:0 > 0, @x0:0 || @index1, @index0.exists(@c0:0, @index1), [@index3], ["a"], @c0:1 == "a", @x0:1 || @index6, @index5.exists(@c0:1, @index6), [@index8], [true, true, true, true], @index4 + @index4 + @index9 + @index9], @index11 == @index10) -[BLOCK_RECURSION_DEPTH_5]: cel.@block([[1], @c0:0 > 0, @x0:0 || @index1, @index0.exists(@c0:0, @index1), [@index3], ["a"], @c0:1 == "a", @x0:1 || @index6, @index5.exists(@c0:1, @index6), [@index8], [true, true, true, true], @index4 + @index4 + @index9 + @index9], @index11 == @index10) -[BLOCK_RECURSION_DEPTH_6]: cel.@block([[1], @c0:0 > 0, @x0:0 || @index1, @index0.exists(@c0:0, @index1), [@index3], ["a"], @c0:1 == "a", @x0:1 || @index6, @index5.exists(@c0:1, @index6), [@index8], [true, true, true, true], @index4 + @index4 + @index9 + @index9], @index11 == @index10) -[BLOCK_RECURSION_DEPTH_7]: cel.@block([[1], @c0:0 > 0, @x0:0 || @index1, @index0.exists(@c0:0, @index1), [@index3], ["a"], @c0:1 == "a", @x0:1 || @index6, @index5.exists(@c0:1, @index6), [@index8], [true, true, true, true], @index4 + @index4 + @index9 + @index9], @index11 == @index10) -[BLOCK_RECURSION_DEPTH_8]: cel.@block([[1], @c0:0 > 0, @x0:0 || @index1, @index0.exists(@c0:0, @index1), [@index3], ["a"], @c0:1 == "a", @x0:1 || @index6, @index5.exists(@c0:1, @index6), [@index8], [true, true, true, true], @index4 + @index4 + @index9 + @index9], @index11 == @index10) -[BLOCK_RECURSION_DEPTH_9]: cel.@block([[1], @c0:0 > 0, @x0:0 || @index1, @index0.exists(@c0:0, @index1), [@index3], ["a"], @c0:1 == "a", @x0:1 || @index6, @index5.exists(@c0:1, @index6), [@index8], [true, true, true, true], @index4 + @index4 + @index9 + @index9], @index11 == @index10) +[BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([[1].exists(@it:0:0, @it:0:0 > 0), [@index0], ["a"].exists(@it:0:1, @it:0:1 == "a"), [@index2]], @index1 + @index1 + @index3 + @index3 == [true, true, true, true]) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([[1], ["a"], [true, true, true, true]], [@index0.exists(@it:0:0, @it:0:0 > 0)] + [@index0.exists(@it:0:0, @it:0:0 > 0)] + [@index1.exists(@it:0:1, @it:0:1 == "a")] + [@index1.exists(@it:0:1, @it:0:1 == "a")] == @index2) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([[1], ["a"], [true, true, true, true]], [@index0.exists(@it:0:0, @it:0:0 > 0)] + [@index0.exists(@it:0:0, @it:0:0 > 0)] + [@index1.exists(@it:0:1, @it:0:1 == "a")] + [@index1.exists(@it:0:1, @it:0:1 == "a")] == @index2) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([[1].exists(@it:0:0, @it:0:0 > 0), ["a"].exists(@it:0:1, @it:0:1 == "a"), [@index0], [@index1], @index2 + @index2 + @index3 + @index3], @index4 == [true, true, true, true]) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([[1].exists(@it:0:0, @it:0:0 > 0), ["a"].exists(@it:0:1, @it:0:1 == "a"), [@index0], [@index1]], @index2 + @index2 + @index3 + @index3 == [true, true, true, true]) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([[1].exists(@it:0:0, @it:0:0 > 0), ["a"].exists(@it:0:1, @it:0:1 == "a"), [@index0], [@index1]], @index2 + @index2 + @index3 + @index3 == [true, true, true, true]) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([[1].exists(@it:0:0, @it:0:0 > 0), ["a"].exists(@it:0:1, @it:0:1 == "a"), [@index0], [@index1]], @index2 + @index2 + @index3 + @index3 == [true, true, true, true]) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([[1].exists(@it:0:0, @it:0:0 > 0), ["a"].exists(@it:0:1, @it:0:1 == "a"), [@index0], [@index1]], @index2 + @index2 + @index3 + @index3 == [true, true, true, true]) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([[1].exists(@it:0:0, @it:0:0 > 0), ["a"].exists(@it:0:1, @it:0:1 == "a"), [@index0], [@index1]], @index2 + @index2 + @index3 + @index3 == [true, true, true, true]) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([[1].exists(@it:0:0, @it:0:0 > 0), ["a"].exists(@it:0:1, @it:0:1 == "a"), [@index0], [@index1]], @index2 + @index2 + @index3 + @index3 == [true, true, true, true]) + +Test case: MULTIPLE_MACROS_3 +Source: [1].exists(i, i > 0) && [1].exists(j, j > 0) && [1].exists(k, k > 1) && [2].exists(l, l > 1) +=====> +Result: false +[BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([[1].exists(@it:0:0, @it:0:0 > 0)], @index0 && @index0 && [1].exists(@it:0:0, @it:0:0 > 1) && [2].exists(@it:0:0, @it:0:0 > 1)) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([[1], [2]], @index0.exists(@it:0:0, @it:0:0 > 0) && @index0.exists(@it:0:0, @it:0:0 > 0) && @index0.exists(@it:0:0, @it:0:0 > 1) && @index1.exists(@it:0:0, @it:0:0 > 1)) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([[1], [2]], @index0.exists(@it:0:0, @it:0:0 > 0) && @index0.exists(@it:0:0, @it:0:0 > 0) && @index0.exists(@it:0:0, @it:0:0 > 1) && @index1.exists(@it:0:0, @it:0:0 > 1)) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([[1].exists(@it:0:0, @it:0:0 > 0)], @index0 && @index0 && [1].exists(@it:0:0, @it:0:0 > 1) && [2].exists(@it:0:0, @it:0:0 > 1)) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([[1].exists(@it:0:0, @it:0:0 > 0)], @index0 && @index0 && [1].exists(@it:0:0, @it:0:0 > 1) && [2].exists(@it:0:0, @it:0:0 > 1)) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([[1].exists(@it:0:0, @it:0:0 > 0)], @index0 && @index0 && [1].exists(@it:0:0, @it:0:0 > 1) && [2].exists(@it:0:0, @it:0:0 > 1)) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([[1].exists(@it:0:0, @it:0:0 > 0)], @index0 && @index0 && [1].exists(@it:0:0, @it:0:0 > 1) && [2].exists(@it:0:0, @it:0:0 > 1)) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([[1].exists(@it:0:0, @it:0:0 > 0)], @index0 && @index0 && [1].exists(@it:0:0, @it:0:0 > 1) && [2].exists(@it:0:0, @it:0:0 > 1)) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([[1].exists(@it:0:0, @it:0:0 > 0)], @index0 && @index0 && [1].exists(@it:0:0, @it:0:0 > 1) && [2].exists(@it:0:0, @it:0:0 > 1)) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([[1].exists(@it:0:0, @it:0:0 > 0)], @index0 && @index0 && [1].exists(@it:0:0, @it:0:0 > 1) && [2].exists(@it:0:0, @it:0:0 > 1)) + +Test case: MULTIPLE_MACROS_COMP_V2_1 +Source: size([[1].exists(i,x,i > 0 && x >= 0)]) + size([[1].exists(j, y, j > 0 && y >= 0)]) + size([[2].exists(k,z,k > 1 && z >= 0)]) + size([[2].exists(l, w, l > 1 && w >= 0)]) == 4 +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([[1].exists(@it:0:0, @it2:0:0, @it:0:0 > 0 && @it2:0:0 >= 0), size([@index0]), [2].exists(@it:0:0, @it2:0:0, @it:0:0 > 1 && @it2:0:0 >= 0), size([@index2])], @index1 + @index1 + @index3 + @index3 == 4) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([[1], [2]], size([@index0.exists(@it:0:0, @it2:0:0, @it:0:0 > 0 && @it2:0:0 >= 0)]) + size([@index0.exists(@it:0:0, @it2:0:0, @it:0:0 > 0 && @it2:0:0 >= 0)]) + size([@index1.exists(@it:0:0, @it2:0:0, @it:0:0 > 1 && @it2:0:0 >= 0)]) + size([@index1.exists(@it:0:0, @it2:0:0, @it:0:0 > 1 && @it2:0:0 >= 0)]) == 4) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([[1], [2]], size([@index0.exists(@it:0:0, @it2:0:0, @it:0:0 > 0 && @it2:0:0 >= 0)]) + size([@index0.exists(@it:0:0, @it2:0:0, @it:0:0 > 0 && @it2:0:0 >= 0)]) + size([@index1.exists(@it:0:0, @it2:0:0, @it:0:0 > 1 && @it2:0:0 >= 0)]) + size([@index1.exists(@it:0:0, @it2:0:0, @it:0:0 > 1 && @it2:0:0 >= 0)]) == 4) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([[1], [2]], size([@index0.exists(@it:0:0, @it2:0:0, @it:0:0 > 0 && @it2:0:0 >= 0)]) + size([@index0.exists(@it:0:0, @it2:0:0, @it:0:0 > 0 && @it2:0:0 >= 0)]) + size([@index1.exists(@it:0:0, @it2:0:0, @it:0:0 > 1 && @it2:0:0 >= 0)]) + size([@index1.exists(@it:0:0, @it2:0:0, @it:0:0 > 1 && @it2:0:0 >= 0)]) == 4) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([[1].exists(@it:0:0, @it2:0:0, @it:0:0 > 0 && @it2:0:0 >= 0), [2].exists(@it:0:0, @it2:0:0, @it:0:0 > 1 && @it2:0:0 >= 0), size([@index0]), size([@index1])], @index2 + @index2 + @index3 + @index3 == 4) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([[1].exists(@it:0:0, @it2:0:0, @it:0:0 > 0 && @it2:0:0 >= 0), [2].exists(@it:0:0, @it2:0:0, @it:0:0 > 1 && @it2:0:0 >= 0), size([@index0]), size([@index1])], @index2 + @index2 + @index3 + @index3 == 4) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([[1].exists(@it:0:0, @it2:0:0, @it:0:0 > 0 && @it2:0:0 >= 0), [2].exists(@it:0:0, @it2:0:0, @it:0:0 > 1 && @it2:0:0 >= 0), size([@index0]), size([@index1])], @index2 + @index2 + @index3 + @index3 == 4) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([[1].exists(@it:0:0, @it2:0:0, @it:0:0 > 0 && @it2:0:0 >= 0), [2].exists(@it:0:0, @it2:0:0, @it:0:0 > 1 && @it2:0:0 >= 0), size([@index0]), size([@index1])], @index2 + @index2 + @index3 + @index3 == 4) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([[1].exists(@it:0:0, @it2:0:0, @it:0:0 > 0 && @it2:0:0 >= 0), [2].exists(@it:0:0, @it2:0:0, @it:0:0 > 1 && @it2:0:0 >= 0), size([@index0]), size([@index1])], @index2 + @index2 + @index3 + @index3 == 4) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([[1].exists(@it:0:0, @it2:0:0, @it:0:0 > 0 && @it2:0:0 >= 0), [2].exists(@it:0:0, @it2:0:0, @it:0:0 > 1 && @it2:0:0 >= 0), size([@index0]), size([@index1])], @index2 + @index2 + @index3 + @index3 == 4) + +Test case: MULTIPLE_MACROS_COMP_V2_2 +Source: [1].exists(i, x, i > 0 && x > 0) && [1].exists(j, y, j > 0 && y > 0) && [1].exists(k, z, k > 1 && z > 0) && [2].exists(l, w, l > 1 && w > 0) +=====> +Result: false +[BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([[1].exists(@it:0:0, @it2:0:0, @it:0:0 > 0 && @it2:0:0 > 0)], @index0 && @index0 && [1].exists(@it:0:0, @it2:0:0, @it:0:0 > 1 && @it2:0:0 > 0) && [2].exists(@it:0:0, @it2:0:0, @it:0:0 > 1 && @it2:0:0 > 0)) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([[1], [2]], @index0.exists(@it:0:0, @it2:0:0, @it:0:0 > 0 && @it2:0:0 > 0) && @index0.exists(@it:0:0, @it2:0:0, @it:0:0 > 0 && @it2:0:0 > 0) && @index0.exists(@it:0:0, @it2:0:0, @it:0:0 > 1 && @it2:0:0 > 0) && @index1.exists(@it:0:0, @it2:0:0, @it:0:0 > 1 && @it2:0:0 > 0)) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([[1], [2]], @index0.exists(@it:0:0, @it2:0:0, @it:0:0 > 0 && @it2:0:0 > 0) && @index0.exists(@it:0:0, @it2:0:0, @it:0:0 > 0 && @it2:0:0 > 0) && @index0.exists(@it:0:0, @it2:0:0, @it:0:0 > 1 && @it2:0:0 > 0) && @index1.exists(@it:0:0, @it2:0:0, @it:0:0 > 1 && @it2:0:0 > 0)) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([[1], [2]], @index0.exists(@it:0:0, @it2:0:0, @it:0:0 > 0 && @it2:0:0 > 0) && @index0.exists(@it:0:0, @it2:0:0, @it:0:0 > 0 && @it2:0:0 > 0) && @index0.exists(@it:0:0, @it2:0:0, @it:0:0 > 1 && @it2:0:0 > 0) && @index1.exists(@it:0:0, @it2:0:0, @it:0:0 > 1 && @it2:0:0 > 0)) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([[1].exists(@it:0:0, @it2:0:0, @it:0:0 > 0 && @it2:0:0 > 0)], @index0 && @index0 && [1].exists(@it:0:0, @it2:0:0, @it:0:0 > 1 && @it2:0:0 > 0) && [2].exists(@it:0:0, @it2:0:0, @it:0:0 > 1 && @it2:0:0 > 0)) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([[1].exists(@it:0:0, @it2:0:0, @it:0:0 > 0 && @it2:0:0 > 0)], @index0 && @index0 && [1].exists(@it:0:0, @it2:0:0, @it:0:0 > 1 && @it2:0:0 > 0) && [2].exists(@it:0:0, @it2:0:0, @it:0:0 > 1 && @it2:0:0 > 0)) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([[1].exists(@it:0:0, @it2:0:0, @it:0:0 > 0 && @it2:0:0 > 0)], @index0 && @index0 && [1].exists(@it:0:0, @it2:0:0, @it:0:0 > 1 && @it2:0:0 > 0) && [2].exists(@it:0:0, @it2:0:0, @it:0:0 > 1 && @it2:0:0 > 0)) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([[1].exists(@it:0:0, @it2:0:0, @it:0:0 > 0 && @it2:0:0 > 0)], @index0 && @index0 && [1].exists(@it:0:0, @it2:0:0, @it:0:0 > 1 && @it2:0:0 > 0) && [2].exists(@it:0:0, @it2:0:0, @it:0:0 > 1 && @it2:0:0 > 0)) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([[1].exists(@it:0:0, @it2:0:0, @it:0:0 > 0 && @it2:0:0 > 0)], @index0 && @index0 && [1].exists(@it:0:0, @it2:0:0, @it:0:0 > 1 && @it2:0:0 > 0) && [2].exists(@it:0:0, @it2:0:0, @it:0:0 > 1 && @it2:0:0 > 0)) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([[1].exists(@it:0:0, @it2:0:0, @it:0:0 > 0 && @it2:0:0 > 0)], @index0 && @index0 && [1].exists(@it:0:0, @it2:0:0, @it:0:0 > 1 && @it2:0:0 > 0) && [2].exists(@it:0:0, @it2:0:0, @it:0:0 > 1 && @it2:0:0 > 0)) Test case: NESTED_MACROS Source: [1,2,3].map(i, [1, 2, 3].map(i, i + 1)) == [[2, 3, 4], [2, 3, 4], [2, 3, 4]] =====> Result: true -[CASCADED_BINDS]: cel.bind(@r0, [1, 2, 3], @r0.map(@c0:0, @r0.map(@c1:0, @c1:0 + 1))) == cel.bind(@r1, [2, 3, 4], [@r1, @r1, @r1]) -[BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([[1, 2, 3], [2, 3, 4]], @index0.map(@c0:0, @index0.map(@c1:0, @c1:0 + 1)) == [@index1, @index1, @index1]) -[BLOCK_RECURSION_DEPTH_1]: cel.@block([[1, 2, 3], [2, 3, 4], [@index1, @index1, @index1], @c1:0 + 1, [@index3], @x1:0 + @index4], @index0.map(@c0:0, @index0.map(@c1:0, @index3)) == @index2) -[BLOCK_RECURSION_DEPTH_2]: cel.@block([[1, 2, 3], [2, 3, 4], [@index1, @index1, @index1], [@c1:0 + 1], @index0.map(@c1:0, @c1:0 + 1), @x0:0 + [@index4], @index0.map(@c0:0, @index4)], @index6 == @index2) -[BLOCK_RECURSION_DEPTH_3]: Unparse Error: java.lang.IllegalArgumentException: unexpected expr kind: NOT_SET -[BLOCK_RECURSION_DEPTH_4]: cel.@block([[1, 2, 3], [2, 3, 4], [@index1, @index1, @index1], @index0.map(@c1:0, @c1:0 + 1), @index0.map(@c0:0, @index3)], @index4 == @index2) -[BLOCK_RECURSION_DEPTH_5]: Unparse Error: java.lang.IllegalArgumentException: unexpected expr kind: NOT_SET -[BLOCK_RECURSION_DEPTH_6]: Unparse Error: java.lang.IllegalArgumentException: unexpected expr kind: NOT_SET -[BLOCK_RECURSION_DEPTH_7]: cel.@block([[1, 2, 3], [2, 3, 4], [@index1, @index1, @index1], @index0.map(@c0:0, @index0.map(@c1:0, @c1:0 + 1))], @index3 == @index2) -[BLOCK_RECURSION_DEPTH_8]: cel.@block([[1, 2, 3], [2, 3, 4], [@index1, @index1, @index1], @index0.map(@c0:0, @index0.map(@c1:0, @c1:0 + 1))], @index3 == @index2) -[BLOCK_RECURSION_DEPTH_9]: cel.@block([[1, 2, 3], [2, 3, 4], [@index1, @index1, @index1], @index0.map(@c0:0, @index0.map(@c1:0, @c1:0 + 1))], @index3 == @index2) +[BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([[1, 2, 3], [2, 3, 4]], @index0.map(@it:1:0, @index0.map(@it:0:0, @it:0:0 + 1)) == [@index1, @index1, @index1]) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([[1, 2, 3], [2, 3, 4], [@index1, @index1, @index1]], @index0.map(@it:1:0, @index0.map(@it:0:0, @it:0:0 + 1)) == @index2) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([[1, 2, 3], [2, 3, 4], [@index1, @index1, @index1]], @index0.map(@it:1:0, @index0.map(@it:0:0, @it:0:0 + 1)) == @index2) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([[1, 2, 3], [2, 3, 4], [@index1, @index1, @index1]], @index0.map(@it:1:0, @index0.map(@it:0:0, @it:0:0 + 1)) == @index2) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([[1, 2, 3], [2, 3, 4], @index0.map(@it:0:0, @it:0:0 + 1)], @index0.map(@it:1:0, @index2) == [@index1, @index1, @index1]) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([[1, 2, 3], [2, 3, 4], @index0.map(@it:0:0, @it:0:0 + 1)], @index0.map(@it:1:0, @index2) == [@index1, @index1, @index1]) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([[1, 2, 3], [2, 3, 4], @index0.map(@it:0:0, @it:0:0 + 1)], @index0.map(@it:1:0, @index2) == [@index1, @index1, @index1]) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([[1, 2, 3], [2, 3, 4]], @index0.map(@it:1:0, @index0.map(@it:0:0, @it:0:0 + 1)) == [@index1, @index1, @index1]) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([[1, 2, 3], [2, 3, 4]], @index0.map(@it:1:0, @index0.map(@it:0:0, @it:0:0 + 1)) == [@index1, @index1, @index1]) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([[1, 2, 3], [2, 3, 4]], @index0.map(@it:1:0, @index0.map(@it:0:0, @it:0:0 + 1)) == [@index1, @index1, @index1]) Test case: NESTED_MACROS_2 Source: [1, 2].map(y, [1, 2, 3].filter(x, x == y)) == [[1], [2]] =====> Result: true -[CASCADED_BINDS]: [1, 2].map(@c0:0, [1, 2, 3].filter(@c1:0, @c1:0 == @c0:0)) == [[1], [2]] -[BLOCK_COMMON_SUBEXPR_ONLY]: [1, 2].map(@c0:0, [1, 2, 3].filter(@c1:0, @c1:0 == @c0:0)) == [[1], [2]] -[BLOCK_RECURSION_DEPTH_1]: cel.@block([[2], [1], [@index1, @index0], [@c1:0], @x1:0 + @index3, @c1:0 == @c0:0, @index5 ? @index4 : @x1:0, [1, 2, 3], [1, 2]], @index8.map(@c0:0, @index7.filter(@c1:0, @index5)) == @index2) -[BLOCK_RECURSION_DEPTH_2]: cel.@block([[[1], [2]], @x1:0 + [@c1:0], (@c1:0 == @c0:0) ? @index1 : @x1:0, [1, 2, 3].filter(@c1:0, @c1:0 == @c0:0), @x0:0 + [@index3], [1, 2].map(@c0:0, @index3)], @index5 == @index0) -[BLOCK_RECURSION_DEPTH_3]: Unparse Error: java.lang.IllegalArgumentException: unexpected expr kind: NOT_SET -[BLOCK_RECURSION_DEPTH_4]: cel.@block([[[1], [2]], [1, 2, 3].filter(@c1:0, @c1:0 == @c0:0), [1, 2].map(@c0:0, @index1)], @index2 == @index0) -[BLOCK_RECURSION_DEPTH_5]: Unparse Error: java.lang.IllegalArgumentException: unexpected expr kind: NOT_SET -[BLOCK_RECURSION_DEPTH_6]: Unparse Error: java.lang.IllegalArgumentException: unexpected expr kind: NOT_SET -[BLOCK_RECURSION_DEPTH_7]: cel.@block([[[1], [2]], [1, 2].map(@c0:0, [1, 2, 3].filter(@c1:0, @c1:0 == @c0:0))], @index1 == @index0) -[BLOCK_RECURSION_DEPTH_8]: cel.@block([[[1], [2]], [1, 2].map(@c0:0, [1, 2, 3].filter(@c1:0, @c1:0 == @c0:0))], @index1 == @index0) -[BLOCK_RECURSION_DEPTH_9]: cel.@block([[[1], [2]], [1, 2].map(@c0:0, [1, 2, 3].filter(@c1:0, @c1:0 == @c0:0))], @index1 == @index0) +[BLOCK_COMMON_SUBEXPR_ONLY]: [1, 2].map(y, [1, 2, 3].filter(x, x == y)) == [[1], [2]] +[BLOCK_RECURSION_DEPTH_1]: cel.@block([[1, 2], [1, 2, 3], [1], [2], [@index2, @index3]], @index0.map(@it:1:0, @index1.filter(@it:0:0, @it:0:0 == @it:1:0)) == @index4) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([[[1], [2]], [1, 2], [1, 2, 3]], @index1.map(@it:1:0, @index2.filter(@it:0:0, @it:0:0 == @it:1:0)) == @index0) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([[[1], [2]], [1, 2], [1, 2, 3]], @index1.map(@it:1:0, @index2.filter(@it:0:0, @it:0:0 == @it:1:0)) == @index0) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([[[1], [2]], [1, 2], [1, 2, 3]], @index1.map(@it:1:0, @index2.filter(@it:0:0, @it:0:0 == @it:1:0)) == @index0) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([[[1], [2]], [1, 2], [1, 2, 3]], @index1.map(@it:1:0, @index2.filter(@it:0:0, @it:0:0 == @it:1:0)) == @index0) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([[[1], [2]], [1, 2], [1, 2, 3]], @index1.map(@it:1:0, @index2.filter(@it:0:0, @it:0:0 == @it:1:0)) == @index0) +[BLOCK_RECURSION_DEPTH_7]: [1, 2].map(y, [1, 2, 3].filter(x, x == y)) == [[1], [2]] +[BLOCK_RECURSION_DEPTH_8]: [1, 2].map(y, [1, 2, 3].filter(x, x == y)) == [[1], [2]] +[BLOCK_RECURSION_DEPTH_9]: [1, 2].map(y, [1, 2, 3].filter(x, x == y)) == [[1], [2]] + +Test case: NESTED_MACROS_3 +Source: [1,2,3].map(i, i * 2) + [1,2,3].map(i, i * 2).map(i, i * 2) +=====> +Result: [2, 4, 6, 4, 8, 12] +[BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([[1, 2, 3].map(@it:0:0, @it:0:0 * 2)], @index0 + @index0.map(@it:1:0, @it:1:0 * 2)) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([[1, 2, 3]], @index0.map(@it:0:0, @it:0:0 * 2) + @index0.map(@it:0:0, @it:0:0 * 2).map(@it:1:0, @it:1:0 * 2)) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([[1, 2, 3]], @index0.map(@it:0:0, @it:0:0 * 2) + @index0.map(@it:0:0, @it:0:0 * 2).map(@it:1:0, @it:1:0 * 2)) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([[1, 2, 3]], @index0.map(@it:0:0, @it:0:0 * 2) + @index0.map(@it:0:0, @it:0:0 * 2).map(@it:1:0, @it:1:0 * 2)) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([[1, 2, 3].map(@it:0:0, @it:0:0 * 2)], @index0 + @index0.map(@it:1:0, @it:1:0 * 2)) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([[1, 2, 3].map(@it:0:0, @it:0:0 * 2)], @index0 + @index0.map(@it:1:0, @it:1:0 * 2)) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([[1, 2, 3].map(@it:0:0, @it:0:0 * 2)], @index0 + @index0.map(@it:1:0, @it:1:0 * 2)) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([[1, 2, 3].map(@it:0:0, @it:0:0 * 2)], @index0 + @index0.map(@it:1:0, @it:1:0 * 2)) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([[1, 2, 3].map(@it:0:0, @it:0:0 * 2)], @index0 + @index0.map(@it:1:0, @it:1:0 * 2)) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([[1, 2, 3].map(@it:0:0, @it:0:0 * 2)], @index0 + @index0.map(@it:1:0, @it:1:0 * 2)) + +Test case: NESTED_MACROS_COMP_V2_1 +Source: [1,2,3].transformList(i, v, [1, 2, 3].transformList(i, v, i + v + 1)) == [[2, 4, 6], [2, 4, 6], [2, 4, 6]] +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([[1, 2, 3], [2, 4, 6]], @index0.transformList(@it:1:0, @it2:1:0, @index0.transformList(@it:0:0, @it2:0:0, @it:0:0 + @it2:0:0 + 1)) == [@index1, @index1, @index1]) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([[1, 2, 3], [2, 4, 6], [@index1, @index1, @index1]], @index0.transformList(@it:1:0, @it2:1:0, @index0.transformList(@it:0:0, @it2:0:0, @it:0:0 + @it2:0:0 + 1)) == @index2) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([[1, 2, 3], [2, 4, 6], [@index1, @index1, @index1]], @index0.transformList(@it:1:0, @it2:1:0, @index0.transformList(@it:0:0, @it2:0:0, @it:0:0 + @it2:0:0 + 1)) == @index2) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([[1, 2, 3], [2, 4, 6], [@index1, @index1, @index1]], @index0.transformList(@it:1:0, @it2:1:0, @index0.transformList(@it:0:0, @it2:0:0, @it:0:0 + @it2:0:0 + 1)) == @index2) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([[1, 2, 3], [2, 4, 6], [@index1, @index1, @index1]], @index0.transformList(@it:1:0, @it2:1:0, @index0.transformList(@it:0:0, @it2:0:0, @it:0:0 + @it2:0:0 + 1)) == @index2) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([[1, 2, 3], [2, 4, 6], @index0.transformList(@it:0:0, @it2:0:0, @it:0:0 + @it2:0:0 + 1)], @index0.transformList(@it:1:0, @it2:1:0, @index2) == [@index1, @index1, @index1]) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([[1, 2, 3], [2, 4, 6], @index0.transformList(@it:0:0, @it2:0:0, @it:0:0 + @it2:0:0 + 1)], @index0.transformList(@it:1:0, @it2:1:0, @index2) == [@index1, @index1, @index1]) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([[1, 2, 3], [2, 4, 6], @index0.transformList(@it:0:0, @it2:0:0, @it:0:0 + @it2:0:0 + 1)], @index0.transformList(@it:1:0, @it2:1:0, @index2) == [@index1, @index1, @index1]) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([[1, 2, 3], [2, 4, 6]], @index0.transformList(@it:1:0, @it2:1:0, @index0.transformList(@it:0:0, @it2:0:0, @it:0:0 + @it2:0:0 + 1)) == [@index1, @index1, @index1]) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([[1, 2, 3], [2, 4, 6]], @index0.transformList(@it:1:0, @it2:1:0, @index0.transformList(@it:0:0, @it2:0:0, @it:0:0 + @it2:0:0 + 1)) == [@index1, @index1, @index1]) + +Test case: NESTED_MACROS_COMP_V2_2 +Source: [1, 2].transformList(i, y, [1, 2, 3].filter(x, x == y && i < y)) == [[1], [2]] +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: [1, 2].transformList(i, y, [1, 2, 3].filter(x, x == y && i < y)) == [[1], [2]] +[BLOCK_RECURSION_DEPTH_1]: cel.@block([[1, 2], [1, 2, 3], [1], [2], [@index2, @index3]], @index0.transformList(@it:1:0, @it2:1:0, @index1.filter(@it:0:0, @it:0:0 == @it2:1:0 && @it:1:0 < @it2:1:0)) == @index4) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([[[1], [2]], [1, 2], [1, 2, 3]], @index1.transformList(@it:1:0, @it2:1:0, @index2.filter(@it:0:0, @it:0:0 == @it2:1:0 && @it:1:0 < @it2:1:0)) == @index0) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([[[1], [2]], [1, 2], [1, 2, 3]], @index1.transformList(@it:1:0, @it2:1:0, @index2.filter(@it:0:0, @it:0:0 == @it2:1:0 && @it:1:0 < @it2:1:0)) == @index0) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([[[1], [2]], [1, 2], [1, 2, 3]], @index1.transformList(@it:1:0, @it2:1:0, @index2.filter(@it:0:0, @it:0:0 == @it2:1:0 && @it:1:0 < @it2:1:0)) == @index0) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([[[1], [2]], [1, 2], [1, 2, 3]], @index1.transformList(@it:1:0, @it2:1:0, @index2.filter(@it:0:0, @it:0:0 == @it2:1:0 && @it:1:0 < @it2:1:0)) == @index0) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([[[1], [2]], [1, 2], [1, 2, 3]], @index1.transformList(@it:1:0, @it2:1:0, @index2.filter(@it:0:0, @it:0:0 == @it2:1:0 && @it:1:0 < @it2:1:0)) == @index0) +[BLOCK_RECURSION_DEPTH_7]: [1, 2].transformList(i, y, [1, 2, 3].filter(x, x == y && i < y)) == [[1], [2]] +[BLOCK_RECURSION_DEPTH_8]: [1, 2].transformList(i, y, [1, 2, 3].filter(x, x == y && i < y)) == [[1], [2]] +[BLOCK_RECURSION_DEPTH_9]: [1, 2].transformList(i, y, [1, 2, 3].filter(x, x == y && i < y)) == [[1], [2]] + +Test case: ADJACENT_NESTED_MACROS +Source: [1,2,3].map(i, [1, 2, 3].map(i, i + 1)) == [1,2,3].map(j, [1, 2, 3].map(j, j + 1)) +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([[1, 2, 3], @index0.map(@it:1:0, @index0.map(@it:0:0, @it:0:0 + 1))], @index1 == @index1) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([[1, 2, 3]], @index0.map(@it:1:0, @index0.map(@it:0:0, @it:0:0 + 1)) == @index0.map(@it:1:0, @index0.map(@it:0:0, @it:0:0 + 1))) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([[1, 2, 3]], @index0.map(@it:1:0, @index0.map(@it:0:0, @it:0:0 + 1)) == @index0.map(@it:1:0, @index0.map(@it:0:0, @it:0:0 + 1))) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([[1, 2, 3]], @index0.map(@it:1:0, @index0.map(@it:0:0, @it:0:0 + 1)) == @index0.map(@it:1:0, @index0.map(@it:0:0, @it:0:0 + 1))) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([[1, 2, 3].map(@it:0:0, @it:0:0 + 1), [1, 2, 3].map(@it:1:0, @index0)], @index1 == @index1) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([[1, 2, 3].map(@it:0:0, @it:0:0 + 1), [1, 2, 3].map(@it:1:0, @index0)], @index1 == @index1) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([[1, 2, 3].map(@it:0:0, @it:0:0 + 1), [1, 2, 3].map(@it:1:0, @index0)], @index1 == @index1) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([[1, 2, 3].map(@it:1:0, [1, 2, 3].map(@it:0:0, @it:0:0 + 1))], @index0 == @index0) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([[1, 2, 3].map(@it:1:0, [1, 2, 3].map(@it:0:0, @it:0:0 + 1))], @index0 == @index0) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([[1, 2, 3].map(@it:1:0, [1, 2, 3].map(@it:0:0, @it:0:0 + 1))], @index0 == @index0) + +Test case: ADJACENT_NESTED_MACROS_COMP_V2 +Source: [1,2,3].transformMap(i, x, [1, 2, 3].transformMap(i, x, i + x + 1)) == [1,2,3].transformMap(j, y, [1, 2, 3].transformMap(j, y, j + y + 1)) +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([[1, 2, 3], @index0.transformMap(@it:1:0, @it2:1:0, @index0.transformMap(@it:0:0, @it2:0:0, @it:0:0 + @it2:0:0 + 1))], @index1 == @index1) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([[1, 2, 3]], @index0.transformMap(@it:1:0, @it2:1:0, @index0.transformMap(@it:0:0, @it2:0:0, @it:0:0 + @it2:0:0 + 1)) == @index0.transformMap(@it:1:0, @it2:1:0, @index0.transformMap(@it:0:0, @it2:0:0, @it:0:0 + @it2:0:0 + 1))) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([[1, 2, 3]], @index0.transformMap(@it:1:0, @it2:1:0, @index0.transformMap(@it:0:0, @it2:0:0, @it:0:0 + @it2:0:0 + 1)) == @index0.transformMap(@it:1:0, @it2:1:0, @index0.transformMap(@it:0:0, @it2:0:0, @it:0:0 + @it2:0:0 + 1))) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([[1, 2, 3]], @index0.transformMap(@it:1:0, @it2:1:0, @index0.transformMap(@it:0:0, @it2:0:0, @it:0:0 + @it2:0:0 + 1)) == @index0.transformMap(@it:1:0, @it2:1:0, @index0.transformMap(@it:0:0, @it2:0:0, @it:0:0 + @it2:0:0 + 1))) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([[1, 2, 3].transformMap(@it:0:0, @it2:0:0, @it:0:0 + @it2:0:0 + 1), [1, 2, 3].transformMap(@it:1:0, @it2:1:0, @index0)], @index1 == @index1) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([[1, 2, 3].transformMap(@it:0:0, @it2:0:0, @it:0:0 + @it2:0:0 + 1), [1, 2, 3].transformMap(@it:1:0, @it2:1:0, @index0)], @index1 == @index1) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([[1, 2, 3].transformMap(@it:1:0, @it2:1:0, [1, 2, 3].transformMap(@it:0:0, @it2:0:0, @it:0:0 + @it2:0:0 + 1))], @index0 == @index0) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([[1, 2, 3].transformMap(@it:1:0, @it2:1:0, [1, 2, 3].transformMap(@it:0:0, @it2:0:0, @it:0:0 + @it2:0:0 + 1))], @index0 == @index0) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([[1, 2, 3].transformMap(@it:1:0, @it2:1:0, [1, 2, 3].transformMap(@it:0:0, @it2:0:0, @it:0:0 + @it2:0:0 + 1))], @index0 == @index0) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([[1, 2, 3].transformMap(@it:1:0, @it2:1:0, [1, 2, 3].transformMap(@it:0:0, @it2:0:0, @it:0:0 + @it2:0:0 + 1))], @index0 == @index0) Test case: INCLUSION_LIST Source: 1 in [1,2,3] && 2 in [1,2,3] && 3 in [3, [1,2,3]] && 1 in [1,2,3] =====> Result: true -[CASCADED_BINDS]: cel.bind(@r0, [1, 2, 3], cel.bind(@r1, 1 in @r0, @r1 && 2 in @r0 && 3 in [3, @r0] && @r1)) [BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([[1, 2, 3], 1 in @index0], @index1 && 2 in @index0 && 3 in [3, @index0] && @index1) -[BLOCK_RECURSION_DEPTH_1]: cel.@block([[1, 2, 3], 1 in @index0, [3, @index0], 3 in @index2, @index3 && @index1, 2 in @index0, @index1 && @index5], @index6 && @index4) -[BLOCK_RECURSION_DEPTH_2]: cel.@block([[1, 2, 3], 1 in @index0, 3 in [3, @index0], @index2 && @index1, @index1 && 2 in @index0], @index4 && @index3) -[BLOCK_RECURSION_DEPTH_3]: cel.@block([[1, 2, 3], 1 in @index0, 3 in [3, @index0] && @index1, @index1 && 2 in @index0], @index3 && @index2) -[BLOCK_RECURSION_DEPTH_4]: cel.@block([[1, 2, 3], 1 in @index0, 3 in [3, @index0] && @index1, @index1 && 2 in @index0], @index3 && @index2) -[BLOCK_RECURSION_DEPTH_5]: cel.@block([[1, 2, 3], 1 in @index0, 3 in [3, @index0] && @index1, @index1 && 2 in @index0], @index3 && @index2) -[BLOCK_RECURSION_DEPTH_6]: cel.@block([[1, 2, 3], 1 in @index0, 3 in [3, @index0] && @index1, @index1 && 2 in @index0], @index3 && @index2) -[BLOCK_RECURSION_DEPTH_7]: cel.@block([[1, 2, 3], 1 in @index0, 3 in [3, @index0] && @index1, @index1 && 2 in @index0], @index3 && @index2) -[BLOCK_RECURSION_DEPTH_8]: cel.@block([[1, 2, 3], 1 in @index0, 3 in [3, @index0] && @index1, @index1 && 2 in @index0], @index3 && @index2) -[BLOCK_RECURSION_DEPTH_9]: cel.@block([[1, 2, 3], 1 in @index0, 3 in [3, @index0] && @index1, @index1 && 2 in @index0], @index3 && @index2) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([[1, 2, 3], 1 in @index0, 2 in @index0, @index1 && @index2, [3, @index0], 3 in @index4, @index5 && @index1], @index3 && @index6) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([1 in [1, 2, 3], [1, 2, 3], @index0 && 2 in @index1, 3 in [3, @index1]], @index2 && @index3 && @index0) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([1 in [1, 2, 3], [1, 2, 3], 3 in [3, @index1] && @index0], @index0 && 2 in @index1 && @index2) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([1 in [1, 2, 3], [1, 2, 3]], @index0 && 2 in @index1 && 3 in [3, @index1] && @index0) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([1 in [1, 2, 3], [1, 2, 3]], @index0 && 2 in @index1 && 3 in [3, @index1] && @index0) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([1 in [1, 2, 3], [1, 2, 3]], @index0 && 2 in @index1 && 3 in [3, @index1] && @index0) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([1 in [1, 2, 3], [1, 2, 3]], @index0 && 2 in @index1 && 3 in [3, @index1] && @index0) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([1 in [1, 2, 3], [1, 2, 3]], @index0 && 2 in @index1 && 3 in [3, @index1] && @index0) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([1 in [1, 2, 3], [1, 2, 3]], @index0 && 2 in @index1 && 3 in [3, @index1] && @index0) Test case: INCLUSION_MAP Source: 2 in {'a': 1, 2: {true: false}, 3: {true: false}} =====> Result: true -[CASCADED_BINDS]: 2 in cel.bind(@r0, {true: false}, {"a": 1, 2: @r0, 3: @r0}) [BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([{true: false}], 2 in {"a": 1, 2: @index0, 3: @index0}) [BLOCK_RECURSION_DEPTH_1]: cel.@block([{true: false}, {"a": 1, 2: @index0, 3: @index0}], 2 in @index1) -[BLOCK_RECURSION_DEPTH_2]: cel.@block([{true: false}, {"a": 1, 2: @index0, 3: @index0}], 2 in @index1) -[BLOCK_RECURSION_DEPTH_3]: cel.@block([{true: false}, {"a": 1, 2: @index0, 3: @index0}], 2 in @index1) -[BLOCK_RECURSION_DEPTH_4]: cel.@block([{true: false}, {"a": 1, 2: @index0, 3: @index0}], 2 in @index1) -[BLOCK_RECURSION_DEPTH_5]: cel.@block([{true: false}, {"a": 1, 2: @index0, 3: @index0}], 2 in @index1) -[BLOCK_RECURSION_DEPTH_6]: cel.@block([{true: false}, {"a": 1, 2: @index0, 3: @index0}], 2 in @index1) -[BLOCK_RECURSION_DEPTH_7]: cel.@block([{true: false}, {"a": 1, 2: @index0, 3: @index0}], 2 in @index1) -[BLOCK_RECURSION_DEPTH_8]: cel.@block([{true: false}, {"a": 1, 2: @index0, 3: @index0}], 2 in @index1) -[BLOCK_RECURSION_DEPTH_9]: cel.@block([{true: false}, {"a": 1, 2: @index0, 3: @index0}], 2 in @index1) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([{true: false}], 2 in {"a": 1, 2: @index0, 3: @index0}) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([{true: false}], 2 in {"a": 1, 2: @index0, 3: @index0}) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([{true: false}], 2 in {"a": 1, 2: @index0, 3: @index0}) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([{true: false}], 2 in {"a": 1, 2: @index0, 3: @index0}) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([{true: false}], 2 in {"a": 1, 2: @index0, 3: @index0}) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([{true: false}], 2 in {"a": 1, 2: @index0, 3: @index0}) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([{true: false}], 2 in {"a": 1, 2: @index0, 3: @index0}) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([{true: false}], 2 in {"a": 1, 2: @index0, 3: @index0}) Test case: MACRO_ITER_VAR_NOT_REFERENCED Source: [1,2].map(i, [1, 2].map(i, [3,4])) == [[[3, 4], [3, 4]], [[3, 4], [3, 4]]] =====> Result: true -[CASCADED_BINDS]: cel.bind(@r1, [3, 4], cel.bind(@r0, [1, 2], @r0.map(@c0:0, @r0.map(@c1:0, @r1))) == cel.bind(@r2, [@r1, @r1], [@r2, @r2])) -[BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([[1, 2], [3, 4], [@index1, @index1]], @index0.map(@c0:0, @index0.map(@c1:0, @index1)) == [@index2, @index2]) -[BLOCK_RECURSION_DEPTH_1]: cel.@block([[1, 2], [3, 4], [@index1, @index1], [@index2, @index2], [@index1], @x1:0 + @index4], @index0.map(@c0:0, @index0.map(@c1:0, @index1)) == @index3) -[BLOCK_RECURSION_DEPTH_2]: cel.@block([[1, 2], [3, 4], [@index1, @index1], [@index2, @index2], @x1:0 + [@index1], @index0.map(@c1:0, @index1), @x0:0 + [@index5], @index0.map(@c0:0, @index5)], @index7 == @index3) -[BLOCK_RECURSION_DEPTH_3]: cel.@block([[1, 2], [3, 4], [@index1, @index1], [@index2, @index2], @index0.map(@c1:0, @index1), @index0.map(@c0:0, @index4)], @index5 == @index3) -[BLOCK_RECURSION_DEPTH_4]: Unparse Error: java.lang.IllegalArgumentException: unexpected expr kind: NOT_SET -[BLOCK_RECURSION_DEPTH_5]: Unparse Error: java.lang.IllegalArgumentException: unexpected expr kind: NOT_SET -[BLOCK_RECURSION_DEPTH_6]: cel.@block([[1, 2], [3, 4], [@index1, @index1], [@index2, @index2], @index0.map(@c0:0, @index0.map(@c1:0, @index1))], @index4 == @index3) -[BLOCK_RECURSION_DEPTH_7]: cel.@block([[1, 2], [3, 4], [@index1, @index1], [@index2, @index2], @index0.map(@c0:0, @index0.map(@c1:0, @index1))], @index4 == @index3) -[BLOCK_RECURSION_DEPTH_8]: cel.@block([[1, 2], [3, 4], [@index1, @index1], [@index2, @index2], @index0.map(@c0:0, @index0.map(@c1:0, @index1))], @index4 == @index3) -[BLOCK_RECURSION_DEPTH_9]: cel.@block([[1, 2], [3, 4], [@index1, @index1], [@index2, @index2], @index0.map(@c0:0, @index0.map(@c1:0, @index1))], @index4 == @index3) +[BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([[1, 2], [3, 4], [@index1, @index1]], @index0.map(@it:1:0, @index0.map(@it:0:0, @index1)) == [@index2, @index2]) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([[1, 2], [3, 4], [@index1, @index1], [@index1], [@index2, @index2]], @index0.map(@it:1:0, @index0.map(@it:0:0, @index1)) == @index4) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([[[3, 4], [3, 4]], [1, 2], [[3, 4]], @index1.map(@it:0:0, [3, 4]), [@index3]], @index1.map(@it:1:0, @index3) == [@index0, @index0]) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([[[3, 4], [3, 4]], [1, 2], [[3, 4]], @index1.map(@it:0:0, [3, 4])], @index1.map(@it:1:0, @index3) == [@index0, @index0]) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([[[3, 4], [3, 4]], [1, 2], @index1.map(@it:0:0, [3, 4])], @index1.map(@it:1:0, @index2) == [@index0, @index0]) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([[[3, 4], [3, 4]], [1, 2], @index1.map(@it:0:0, [3, 4])], @index1.map(@it:1:0, @index2) == [@index0, @index0]) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([[[3, 4], [3, 4]], [1, 2], @index1.map(@it:0:0, [3, 4])], @index1.map(@it:1:0, @index2) == [@index0, @index0]) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([[[3, 4], [3, 4]], [1, 2]], @index1.map(@it:1:0, @index1.map(@it:0:0, [3, 4])) == [@index0, @index0]) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([[[3, 4], [3, 4]], [1, 2]], @index1.map(@it:1:0, @index1.map(@it:0:0, [3, 4])) == [@index0, @index0]) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([[[3, 4], [3, 4]], [1, 2]], @index1.map(@it:1:0, @index1.map(@it:0:0, [3, 4])) == [@index0, @index0]) + +Test case: MACRO_ITER_VAR2_NOT_REFERENCED +Source: [1,2].transformList(i, v, [1, 2].transformList(i, v, [3,4])) == [[[3, 4], [3, 4]], [[3, 4], [3, 4]]] +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([[1, 2], [3, 4], [@index1, @index1]], @index0.transformList(@it:1:0, @it2:1:0, @index0.transformList(@it:0:0, @it2:0:0, @index1)) == [@index2, @index2]) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([[1, 2], [3, 4], [@index1, @index1], [@index1], [@index2, @index2]], @index0.transformList(@it:1:0, @it2:1:0, @index0.transformList(@it:0:0, @it2:0:0, @index1)) == @index4) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([[[3, 4], [3, 4]], [1, 2], [[3, 4]], @index1.transformList(@it:0:0, @it2:0:0, [3, 4]), [@index3]], @index1.transformList(@it:1:0, @it2:1:0, @index3) == [@index0, @index0]) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([[[3, 4], [3, 4]], [1, 2], [[3, 4]], @index1.transformList(@it:0:0, @it2:0:0, [3, 4])], @index1.transformList(@it:1:0, @it2:1:0, @index3) == [@index0, @index0]) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([[[3, 4], [3, 4]], [1, 2], @index1.transformList(@it:0:0, @it2:0:0, [3, 4])], @index1.transformList(@it:1:0, @it2:1:0, @index2) == [@index0, @index0]) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([[[3, 4], [3, 4]], [1, 2], @index1.transformList(@it:0:0, @it2:0:0, [3, 4])], @index1.transformList(@it:1:0, @it2:1:0, @index2) == [@index0, @index0]) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([[[3, 4], [3, 4]], [1, 2], @index1.transformList(@it:0:0, @it2:0:0, [3, 4])], @index1.transformList(@it:1:0, @it2:1:0, @index2) == [@index0, @index0]) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([[[3, 4], [3, 4]], [1, 2]], @index1.transformList(@it:1:0, @it2:1:0, @index1.transformList(@it:0:0, @it2:0:0, [3, 4])) == [@index0, @index0]) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([[[3, 4], [3, 4]], [1, 2]], @index1.transformList(@it:1:0, @it2:1:0, @index1.transformList(@it:0:0, @it2:0:0, [3, 4])) == [@index0, @index0]) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([[[3, 4], [3, 4]], [1, 2]], @index1.transformList(@it:1:0, @it2:1:0, @index1.transformList(@it:0:0, @it2:0:0, [3, 4])) == [@index0, @index0]) Test case: MACRO_SHADOWED_VARIABLE Source: [x - 1 > 3 ? x - 1 : 5].exists(x, x - 1 > 3) || x - 1 > 3 =====> Result: true -[CASCADED_BINDS]: cel.bind(@r0, x - 1, cel.bind(@r1, @r0 > 3, [@r1 ? @r0 : 5].exists(@c0:0, @c0:0 - 1 > 3) || @r1)) -[BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([x - 1, @index0 > 3], [@index1 ? @index0 : 5].exists(@c0:0, @c0:0 - 1 > 3) || @index1) -[BLOCK_RECURSION_DEPTH_1]: cel.@block([x - 1, @index0 > 3, @c0:0 - 1, @index2 > 3, @x0:0 || @index3, @index1 ? @index0 : 5, [@index5]], @index6.exists(@c0:0, @index3) || @index1) -[BLOCK_RECURSION_DEPTH_2]: cel.@block([x - 1, @index0 > 3, @c0:0 - 1 > 3, @x0:0 || @index2, [@index1 ? @index0 : 5]], @index4.exists(@c0:0, @index2) || @index1) -[BLOCK_RECURSION_DEPTH_3]: cel.@block([x - 1, @index0 > 3, @x0:0 || @c0:0 - 1 > 3, [@index1 ? @index0 : 5].exists(@c0:0, @c0:0 - 1 > 3)], @index3 || @index1) -[BLOCK_RECURSION_DEPTH_4]: cel.@block([x - 1, @index0 > 3, [@index1 ? @index0 : 5].exists(@c0:0, @c0:0 - 1 > 3)], @index2 || @index1) -[BLOCK_RECURSION_DEPTH_5]: cel.@block([x - 1, @index0 > 3, [@index1 ? @index0 : 5].exists(@c0:0, @c0:0 - 1 > 3)], @index2 || @index1) -[BLOCK_RECURSION_DEPTH_6]: cel.@block([x - 1, @index0 > 3, [@index1 ? @index0 : 5].exists(@c0:0, @c0:0 - 1 > 3)], @index2 || @index1) -[BLOCK_RECURSION_DEPTH_7]: cel.@block([x - 1, @index0 > 3, [@index1 ? @index0 : 5].exists(@c0:0, @c0:0 - 1 > 3)], @index2 || @index1) -[BLOCK_RECURSION_DEPTH_8]: cel.@block([x - 1, @index0 > 3, [@index1 ? @index0 : 5].exists(@c0:0, @c0:0 - 1 > 3)], @index2 || @index1) -[BLOCK_RECURSION_DEPTH_9]: cel.@block([x - 1, @index0 > 3, [@index1 ? @index0 : 5].exists(@c0:0, @c0:0 - 1 > 3)], @index2 || @index1) +[BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([x - 1, @index0 > 3], [@index1 ? @index0 : 5].exists(@it:0:0, @it:0:0 - 1 > 3) || @index1) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([x - 1, @index0 > 3, @index1 ? @index0 : 5, [@index2]], @index3.exists(@it:0:0, @it:0:0 - 1 > 3) || @index1) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([x - 1 > 3, @index0 ? (x - 1) : 5, [@index1]], @index2.exists(@it:0:0, @it:0:0 - 1 > 3) || @index0) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([x - 1 > 3, [@index0 ? (x - 1) : 5]], @index1.exists(@it:0:0, @it:0:0 - 1 > 3) || @index0) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([x - 1 > 3], [@index0 ? (x - 1) : 5].exists(@it:0:0, @it:0:0 - 1 > 3) || @index0) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([x - 1 > 3], [@index0 ? (x - 1) : 5].exists(@it:0:0, @it:0:0 - 1 > 3) || @index0) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([x - 1 > 3], [@index0 ? (x - 1) : 5].exists(@it:0:0, @it:0:0 - 1 > 3) || @index0) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([x - 1 > 3], [@index0 ? (x - 1) : 5].exists(@it:0:0, @it:0:0 - 1 > 3) || @index0) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([x - 1 > 3], [@index0 ? (x - 1) : 5].exists(@it:0:0, @it:0:0 - 1 > 3) || @index0) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([x - 1 > 3], [@index0 ? (x - 1) : 5].exists(@it:0:0, @it:0:0 - 1 > 3) || @index0) Test case: MACRO_SHADOWED_VARIABLE_2 Source: ["foo", "bar"].map(x, [x + x, x + x]).map(x, [x + x, x + x]) =====> Result: [[[foofoo, foofoo, foofoo, foofoo], [foofoo, foofoo, foofoo, foofoo]], [[barbar, barbar, barbar, barbar], [barbar, barbar, barbar, barbar]]] -[CASCADED_BINDS]: ["foo", "bar"].map(@c1:0, cel.bind(@r0, @c1:0 + @c1:0, [@r0, @r0])).map(@c0:0, cel.bind(@r1, @c0:0 + @c0:0, [@r1, @r1])) -[BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([@c1:0 + @c1:0, @c0:0 + @c0:0], ["foo", "bar"].map(@c1:0, [@index0, @index0]).map(@c0:0, [@index1, @index1])) -[BLOCK_RECURSION_DEPTH_1]: cel.@block([@c1:0 + @c1:0, @c0:0 + @c0:0, [@index1, @index1], [@index2], @x0:0 + @index3, [@index0, @index0], [@index5], @x1:0 + @index6, ["foo", "bar"]], @index8.map(@c1:0, @index5).map(@c0:0, @index2)) -[BLOCK_RECURSION_DEPTH_2]: cel.@block([@c1:0 + @c1:0, @c0:0 + @c0:0, [[@index1, @index1]], @x0:0 + @index2, [[@index0, @index0]], ["foo", "bar"].map(@c1:0, [@index0, @index0])], @index5.map(@c0:0, [@index1, @index1])) -[BLOCK_RECURSION_DEPTH_3]: cel.@block([@c1:0 + @c1:0, @c0:0 + @c0:0, @x0:0 + [[@index1, @index1]], @x1:0 + [[@index0, @index0]], ["foo", "bar"].map(@c1:0, [@index0, @index0])], @index4.map(@c0:0, [@index1, @index1])) -[BLOCK_RECURSION_DEPTH_4]: cel.@block([@c1:0 + @c1:0, @c0:0 + @c0:0, @x0:0 + [[@index1, @index1]], ["foo", "bar"].map(@c1:0, [@index0, @index0])], @index3.map(@c0:0, [@index1, @index1])) -[BLOCK_RECURSION_DEPTH_5]: cel.@block([@c1:0 + @c1:0, @c0:0 + @c0:0, @x0:0 + [[@index1, @index1]], ["foo", "bar"].map(@c1:0, [@index0, @index0])], @index3.map(@c0:0, [@index1, @index1])) -[BLOCK_RECURSION_DEPTH_6]: cel.@block([@c1:0 + @c1:0, @c0:0 + @c0:0, @x0:0 + [[@index1, @index1]], ["foo", "bar"].map(@c1:0, [@index0, @index0])], @index3.map(@c0:0, [@index1, @index1])) -[BLOCK_RECURSION_DEPTH_7]: cel.@block([@c1:0 + @c1:0, @c0:0 + @c0:0, @x0:0 + [[@index1, @index1]], ["foo", "bar"].map(@c1:0, [@index0, @index0])], @index3.map(@c0:0, [@index1, @index1])) -[BLOCK_RECURSION_DEPTH_8]: cel.@block([@c1:0 + @c1:0, @c0:0 + @c0:0, @x0:0 + [[@index1, @index1]], ["foo", "bar"].map(@c1:0, [@index0, @index0])], @index3.map(@c0:0, [@index1, @index1])) -[BLOCK_RECURSION_DEPTH_9]: cel.@block([@c1:0 + @c1:0, @c0:0 + @c0:0, @x0:0 + [[@index1, @index1]], ["foo", "bar"].map(@c1:0, [@index0, @index0])], @index3.map(@c0:0, [@index1, @index1])) +[BLOCK_COMMON_SUBEXPR_ONLY]: ["foo", "bar"].map(x, [x + x, x + x]).map(x, [x + x, x + x]) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([["foo", "bar"]], @index0.map(@it:0:0, [@it:0:0 + @it:0:0, @it:0:0 + @it:0:0]).map(@it:1:0, [@it:1:0 + @it:1:0, @it:1:0 + @it:1:0])) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([["foo", "bar"]], @index0.map(@it:0:0, [@it:0:0 + @it:0:0, @it:0:0 + @it:0:0]).map(@it:1:0, [@it:1:0 + @it:1:0, @it:1:0 + @it:1:0])) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([["foo", "bar"]], @index0.map(@it:0:0, [@it:0:0 + @it:0:0, @it:0:0 + @it:0:0]).map(@it:1:0, [@it:1:0 + @it:1:0, @it:1:0 + @it:1:0])) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([["foo", "bar"]], @index0.map(@it:0:0, [@it:0:0 + @it:0:0, @it:0:0 + @it:0:0]).map(@it:1:0, [@it:1:0 + @it:1:0, @it:1:0 + @it:1:0])) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([["foo", "bar"].map(@it:0:0, [@it:0:0 + @it:0:0, @it:0:0 + @it:0:0])], @index0.map(@it:1:0, [@it:1:0 + @it:1:0, @it:1:0 + @it:1:0])) +[BLOCK_RECURSION_DEPTH_6]: ["foo", "bar"].map(x, [x + x, x + x]).map(x, [x + x, x + x]) +[BLOCK_RECURSION_DEPTH_7]: ["foo", "bar"].map(x, [x + x, x + x]).map(x, [x + x, x + x]) +[BLOCK_RECURSION_DEPTH_8]: ["foo", "bar"].map(x, [x + x, x + x]).map(x, [x + x, x + x]) +[BLOCK_RECURSION_DEPTH_9]: ["foo", "bar"].map(x, [x + x, x + x]).map(x, [x + x, x + x]) + +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: 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) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([x - y - 1 > 3, @index0 ? (x - y - 1) : 5, [@index1]], @index2.exists(@it:0:0, @it2:0:0, @it:0:0 - @it2:0:0 - 1 > 3) || @index0) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([x - y - 1 > 3, [@index0 ? (x - y - 1) : 5]], @index1.exists(@it:0:0, @it2:0:0, @it:0:0 - @it2:0:0 - 1 > 3) || @index0) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([x - y - 1 > 3], [@index0 ? (x - y - 1) : 5].exists(@it:0:0, @it2:0:0, @it:0:0 - @it2:0:0 - 1 > 3) || @index0) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([x - y - 1 > 3], [@index0 ? (x - y - 1) : 5].exists(@it:0:0, @it2:0:0, @it:0:0 - @it2:0:0 - 1 > 3) || @index0) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([x - y - 1 > 3], [@index0 ? (x - y - 1) : 5].exists(@it:0:0, @it2:0:0, @it:0:0 - @it2:0:0 - 1 > 3) || @index0) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([x - y - 1 > 3], [@index0 ? (x - y - 1) : 5].exists(@it:0:0, @it2:0:0, @it:0:0 - @it2:0:0 - 1 > 3) || @index0) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([x - y - 1 > 3], [@index0 ? (x - y - 1) : 5].exists(@it:0:0, @it2:0:0, @it:0:0 - @it2:0:0 - 1 > 3) || @index0) + +Test case: MACRO_SHADOWED_VARIABLE_COMP_V2_2 +Source: ["foo", "bar"].transformMap(x, y, [x + x, y + y]).transformMap(x, y, [x + x, y + y]) +=====> +Result: {0=[0, [0, foofoo, 0, foofoo]], 1=[2, [2, barbar, 2, barbar]]} +[BLOCK_COMMON_SUBEXPR_ONLY]: ["foo", "bar"].transformMap(x, y, [x + x, y + y]).transformMap(x, y, [x + x, y + y]) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([["foo", "bar"]], @index0.transformMap(@it:0:0, @it2:0:0, [@it:0:0 + @it:0:0, @it2:0:0 + @it2:0:0]).transformMap(@it:1:0, @it2:1:0, [@it:1:0 + @it:1:0, @it2:1:0 + @it2:1:0])) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([["foo", "bar"]], @index0.transformMap(@it:0:0, @it2:0:0, [@it:0:0 + @it:0:0, @it2:0:0 + @it2:0:0]).transformMap(@it:1:0, @it2:1:0, [@it:1:0 + @it:1:0, @it2:1:0 + @it2:1:0])) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([["foo", "bar"]], @index0.transformMap(@it:0:0, @it2:0:0, [@it:0:0 + @it:0:0, @it2:0:0 + @it2:0:0]).transformMap(@it:1:0, @it2:1:0, [@it:1:0 + @it:1:0, @it2:1:0 + @it2:1:0])) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([["foo", "bar"].transformMap(@it:0:0, @it2:0:0, [@it:0:0 + @it:0:0, @it2:0:0 + @it2:0:0])], @index0.transformMap(@it:1:0, @it2:1:0, [@it:1:0 + @it:1:0, @it2:1:0 + @it2:1:0])) +[BLOCK_RECURSION_DEPTH_5]: ["foo", "bar"].transformMap(x, y, [x + x, y + y]).transformMap(x, y, [x + x, y + y]) +[BLOCK_RECURSION_DEPTH_6]: ["foo", "bar"].transformMap(x, y, [x + x, y + y]).transformMap(x, y, [x + x, y + y]) +[BLOCK_RECURSION_DEPTH_7]: ["foo", "bar"].transformMap(x, y, [x + x, y + y]).transformMap(x, y, [x + x, y + y]) +[BLOCK_RECURSION_DEPTH_8]: ["foo", "bar"].transformMap(x, y, [x + x, y + y]).transformMap(x, y, [x + x, y + y]) +[BLOCK_RECURSION_DEPTH_9]: ["foo", "bar"].transformMap(x, y, [x + x, y + y]).transformMap(x, y, [x + x, y + y]) Test case: PRESENCE_TEST Source: has({'a': true}.a) && {'a':true}['a'] =====> Result: true -[CASCADED_BINDS]: cel.bind(@r0, {"a": true}, has(@r0.a) && @r0["a"]) [BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([{"a": true}], has(@index0.a) && @index0["a"]) -[BLOCK_RECURSION_DEPTH_1]: cel.@block([{"a": true}, @index0["a"]], has(@index0.a) && @index1) -[BLOCK_RECURSION_DEPTH_2]: cel.@block([{"a": true}, @index0["a"]], has(@index0.a) && @index1) -[BLOCK_RECURSION_DEPTH_3]: cel.@block([{"a": true}, @index0["a"]], has(@index0.a) && @index1) -[BLOCK_RECURSION_DEPTH_4]: cel.@block([{"a": true}, @index0["a"]], has(@index0.a) && @index1) -[BLOCK_RECURSION_DEPTH_5]: cel.@block([{"a": true}, @index0["a"]], has(@index0.a) && @index1) -[BLOCK_RECURSION_DEPTH_6]: cel.@block([{"a": true}, @index0["a"]], has(@index0.a) && @index1) -[BLOCK_RECURSION_DEPTH_7]: cel.@block([{"a": true}, @index0["a"]], has(@index0.a) && @index1) -[BLOCK_RECURSION_DEPTH_8]: cel.@block([{"a": true}, @index0["a"]], has(@index0.a) && @index1) -[BLOCK_RECURSION_DEPTH_9]: cel.@block([{"a": true}, @index0["a"]], has(@index0.a) && @index1) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([{"a": true}, has(@index0.a), @index0["a"]], @index1 && @index2) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([{"a": true}], has(@index0.a) && @index0["a"]) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([{"a": true}], has(@index0.a) && @index0["a"]) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([{"a": true}], has(@index0.a) && @index0["a"]) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([{"a": true}], has(@index0.a) && @index0["a"]) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([{"a": true}], has(@index0.a) && @index0["a"]) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([{"a": true}], has(@index0.a) && @index0["a"]) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([{"a": true}], has(@index0.a) && @index0["a"]) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([{"a": true}], has(@index0.a) && @index0["a"]) + +Test case: PRESENCE_TEST_2 +Source: has({'a': true}.a) && has({'a': true}.a) +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([has({"a": true}.a)], @index0 && @index0) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([{"a": true}, has(@index0.a)], @index1 && @index1) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([has({"a": true}.a)], @index0 && @index0) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([has({"a": true}.a)], @index0 && @index0) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([has({"a": true}.a)], @index0 && @index0) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([has({"a": true}.a)], @index0 && @index0) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([has({"a": true}.a)], @index0 && @index0) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([has({"a": true}.a)], @index0 && @index0) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([has({"a": true}.a)], @index0 && @index0) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([has({"a": true}.a)], @index0 && @index0) Test case: PRESENCE_TEST_WITH_TERNARY Source: (has(msg.oneof_type.payload) ? msg.oneof_type.payload.single_int64 : 0) == 10 =====> Result: true -[CASCADED_BINDS]: cel.bind(@r0, msg.oneof_type, has(@r0.payload) ? @r0.payload.single_int64 : 0) == 10 [BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([msg.oneof_type], (has(@index0.payload) ? @index0.payload.single_int64 : 0) == 10) -[BLOCK_RECURSION_DEPTH_1]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64], (has(@index0.payload) ? @index2 : 0) == 10) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([msg.oneof_type, has(@index0.payload), @index0.payload, @index2.single_int64, @index1 ? @index3 : 0], @index4 == 10) [BLOCK_RECURSION_DEPTH_2]: cel.@block([msg.oneof_type, @index0.payload.single_int64, has(@index0.payload) ? @index1 : 0], @index2 == 10) [BLOCK_RECURSION_DEPTH_3]: cel.@block([msg.oneof_type, has(@index0.payload) ? @index0.payload.single_int64 : 0], @index1 == 10) -[BLOCK_RECURSION_DEPTH_4]: cel.@block([msg.oneof_type, has(@index0.payload) ? @index0.payload.single_int64 : 0], @index1 == 10) -[BLOCK_RECURSION_DEPTH_5]: cel.@block([msg.oneof_type, has(@index0.payload) ? @index0.payload.single_int64 : 0], @index1 == 10) -[BLOCK_RECURSION_DEPTH_6]: cel.@block([msg.oneof_type, has(@index0.payload) ? @index0.payload.single_int64 : 0], @index1 == 10) -[BLOCK_RECURSION_DEPTH_7]: cel.@block([msg.oneof_type, has(@index0.payload) ? @index0.payload.single_int64 : 0], @index1 == 10) -[BLOCK_RECURSION_DEPTH_8]: cel.@block([msg.oneof_type, has(@index0.payload) ? @index0.payload.single_int64 : 0], @index1 == 10) -[BLOCK_RECURSION_DEPTH_9]: cel.@block([msg.oneof_type, has(@index0.payload) ? @index0.payload.single_int64 : 0], @index1 == 10) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([msg.oneof_type], (has(@index0.payload) ? @index0.payload.single_int64 : 0) == 10) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([msg.oneof_type], (has(@index0.payload) ? @index0.payload.single_int64 : 0) == 10) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([msg.oneof_type], (has(@index0.payload) ? @index0.payload.single_int64 : 0) == 10) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([msg.oneof_type], (has(@index0.payload) ? @index0.payload.single_int64 : 0) == 10) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([msg.oneof_type], (has(@index0.payload) ? @index0.payload.single_int64 : 0) == 10) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([msg.oneof_type], (has(@index0.payload) ? @index0.payload.single_int64 : 0) == 10) Test case: PRESENCE_TEST_WITH_TERNARY_2 Source: (has(msg.oneof_type.payload) ? msg.oneof_type.payload.single_int64 : msg.oneof_type.payload.single_int64 * 0) == 10 =====> Result: true -[CASCADED_BINDS]: cel.bind(@r0, msg.oneof_type, cel.bind(@r1, @r0.payload.single_int64, has(@r0.payload) ? @r1 : (@r1 * 0))) == 10 [BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([msg.oneof_type, @index0.payload.single_int64], (has(@index0.payload) ? @index1 : (@index1 * 0)) == 10) -[BLOCK_RECURSION_DEPTH_1]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, @index2 * 0], (has(@index0.payload) ? @index2 : @index3) == 10) -[BLOCK_RECURSION_DEPTH_2]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, has(@index0.payload) ? @index2 : (@index2 * 0)], @index3 == 10) -[BLOCK_RECURSION_DEPTH_3]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, has(@index0.payload) ? @index2 : (@index2 * 0)], @index3 == 10) -[BLOCK_RECURSION_DEPTH_4]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, has(@index0.payload) ? @index2 : (@index2 * 0)], @index3 == 10) -[BLOCK_RECURSION_DEPTH_5]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, has(@index0.payload) ? @index2 : (@index2 * 0)], @index3 == 10) -[BLOCK_RECURSION_DEPTH_6]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, has(@index0.payload) ? @index2 : (@index2 * 0)], @index3 == 10) -[BLOCK_RECURSION_DEPTH_7]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, has(@index0.payload) ? @index2 : (@index2 * 0)], @index3 == 10) -[BLOCK_RECURSION_DEPTH_8]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, has(@index0.payload) ? @index2 : (@index2 * 0)], @index3 == 10) -[BLOCK_RECURSION_DEPTH_9]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, has(@index0.payload) ? @index2 : (@index2 * 0)], @index3 == 10) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, has(@index0.payload), @index2 * 0, @index3 ? @index2 : @index4], @index5 == 10) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([msg.oneof_type.payload, @index0.single_int64, has(msg.oneof_type.payload), @index2 ? @index1 : (@index1 * 0)], @index3 == 10) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([msg.oneof_type.payload.single_int64, has(msg.oneof_type.payload) ? @index0 : (@index0 * 0)], @index1 == 10) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([msg.oneof_type.payload.single_int64], (has(msg.oneof_type.payload) ? @index0 : (@index0 * 0)) == 10) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([msg.oneof_type.payload.single_int64], (has(msg.oneof_type.payload) ? @index0 : (@index0 * 0)) == 10) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([msg.oneof_type.payload.single_int64], (has(msg.oneof_type.payload) ? @index0 : (@index0 * 0)) == 10) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([msg.oneof_type.payload.single_int64], (has(msg.oneof_type.payload) ? @index0 : (@index0 * 0)) == 10) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([msg.oneof_type.payload.single_int64], (has(msg.oneof_type.payload) ? @index0 : (@index0 * 0)) == 10) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([msg.oneof_type.payload.single_int64], (has(msg.oneof_type.payload) ? @index0 : (@index0 * 0)) == 10) Test case: PRESENCE_TEST_WITH_TERNARY_3 Source: (has(msg.oneof_type.payload.single_int64) ? msg.oneof_type.payload.single_int64 : msg.oneof_type.payload.single_int64 * 0) == 10 =====> Result: true -[CASCADED_BINDS]: cel.bind(@r0, msg.oneof_type.payload, cel.bind(@r1, @r0.single_int64, has(@r0.single_int64) ? @r1 : (@r1 * 0))) == 10 [BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([msg.oneof_type.payload, @index0.single_int64], (has(@index0.single_int64) ? @index1 : (@index1 * 0)) == 10) -[BLOCK_RECURSION_DEPTH_1]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, @index2 * 0], (has(@index1.single_int64) ? @index2 : @index3) == 10) -[BLOCK_RECURSION_DEPTH_2]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, has(@index1.single_int64) ? @index2 : (@index2 * 0)], @index3 == 10) -[BLOCK_RECURSION_DEPTH_3]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, has(@index1.single_int64) ? @index2 : (@index2 * 0)], @index3 == 10) -[BLOCK_RECURSION_DEPTH_4]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, has(@index1.single_int64) ? @index2 : (@index2 * 0)], @index3 == 10) -[BLOCK_RECURSION_DEPTH_5]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, has(@index1.single_int64) ? @index2 : (@index2 * 0)], @index3 == 10) -[BLOCK_RECURSION_DEPTH_6]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, has(@index1.single_int64) ? @index2 : (@index2 * 0)], @index3 == 10) -[BLOCK_RECURSION_DEPTH_7]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, has(@index1.single_int64) ? @index2 : (@index2 * 0)], @index3 == 10) -[BLOCK_RECURSION_DEPTH_8]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, has(@index1.single_int64) ? @index2 : (@index2 * 0)], @index3 == 10) -[BLOCK_RECURSION_DEPTH_9]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, has(@index1.single_int64) ? @index2 : (@index2 * 0)], @index3 == 10) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, has(@index1.single_int64), @index2 * 0, @index3 ? @index2 : @index4], @index5 == 10) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([msg.oneof_type.payload, @index0.single_int64, has(@index0.single_int64) ? @index1 : (@index1 * 0)], @index2 == 10) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([msg.oneof_type.payload.single_int64, has(msg.oneof_type.payload.single_int64)], (@index1 ? @index0 : (@index0 * 0)) == 10) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([msg.oneof_type.payload.single_int64, has(msg.oneof_type.payload.single_int64) ? @index0 : (@index0 * 0)], @index1 == 10) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([msg.oneof_type.payload.single_int64], (has(msg.oneof_type.payload.single_int64) ? @index0 : (@index0 * 0)) == 10) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([msg.oneof_type.payload.single_int64], (has(msg.oneof_type.payload.single_int64) ? @index0 : (@index0 * 0)) == 10) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([msg.oneof_type.payload.single_int64], (has(msg.oneof_type.payload.single_int64) ? @index0 : (@index0 * 0)) == 10) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([msg.oneof_type.payload.single_int64], (has(msg.oneof_type.payload.single_int64) ? @index0 : (@index0 * 0)) == 10) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([msg.oneof_type.payload.single_int64], (has(msg.oneof_type.payload.single_int64) ? @index0 : (@index0 * 0)) == 10) Test case: PRESENCE_TEST_WITH_TERNARY_NESTED Source: (has(msg.oneof_type) && has(msg.oneof_type.payload) && has(msg.oneof_type.payload.single_int64)) ? ((has(msg.oneof_type.payload.map_string_string) && has(msg.oneof_type.payload.map_string_string.key)) ? msg.oneof_type.payload.map_string_string.key == 'A' : false) : false =====> Result: true -[CASCADED_BINDS]: cel.bind(@r0, msg.oneof_type, cel.bind(@r1, @r0.payload, (has(msg.oneof_type) && has(@r0.payload) && has(@r1.single_int64)) ? cel.bind(@r2, @r1.map_string_string, (has(@r1.map_string_string) && has(@r2.key)) ? (@r2.key == "A") : false) : false)) [BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([msg.oneof_type, @index0.payload, @index1.map_string_string], (has(msg.oneof_type) && has(@index0.payload) && has(@index1.single_int64)) ? ((has(@index1.map_string_string) && has(@index2.key)) ? (@index2.key == "A") : false) : false) -[BLOCK_RECURSION_DEPTH_1]: cel.@block([msg.oneof_type, @index0.payload, @index1.map_string_string, @index2.key, @index3 == "A"], (has(msg.oneof_type) && has(@index0.payload) && has(@index1.single_int64)) ? ((has(@index1.map_string_string) && has(@index2.key)) ? @index4 : false) : false) -[BLOCK_RECURSION_DEPTH_2]: cel.@block([msg.oneof_type, @index0.payload, @index1.map_string_string, @index2.key == "A", has(@index1.map_string_string) && has(@index2.key), @index4 ? @index3 : false, has(msg.oneof_type) && has(@index0.payload), @index6 && has(@index1.single_int64)], @index7 ? @index5 : false) -[BLOCK_RECURSION_DEPTH_3]: cel.@block([msg.oneof_type, @index0.payload, @index1.map_string_string, (has(@index1.map_string_string) && has(@index2.key)) ? (@index2.key == "A") : false, has(msg.oneof_type) && has(@index0.payload) && has(@index1.single_int64)], @index4 ? @index3 : false) -[BLOCK_RECURSION_DEPTH_4]: cel.@block([msg.oneof_type, @index0.payload, @index1.map_string_string, (has(@index1.map_string_string) && has(@index2.key)) ? (@index2.key == "A") : false, has(msg.oneof_type) && has(@index0.payload) && has(@index1.single_int64)], @index4 ? @index3 : false) -[BLOCK_RECURSION_DEPTH_5]: cel.@block([msg.oneof_type, @index0.payload, @index1.map_string_string, (has(@index1.map_string_string) && has(@index2.key)) ? (@index2.key == "A") : false, has(msg.oneof_type) && has(@index0.payload) && has(@index1.single_int64)], @index4 ? @index3 : false) -[BLOCK_RECURSION_DEPTH_6]: cel.@block([msg.oneof_type, @index0.payload, @index1.map_string_string, (has(@index1.map_string_string) && has(@index2.key)) ? (@index2.key == "A") : false, has(msg.oneof_type) && has(@index0.payload) && has(@index1.single_int64)], @index4 ? @index3 : false) -[BLOCK_RECURSION_DEPTH_7]: cel.@block([msg.oneof_type, @index0.payload, @index1.map_string_string, (has(@index1.map_string_string) && has(@index2.key)) ? (@index2.key == "A") : false, has(msg.oneof_type) && has(@index0.payload) && has(@index1.single_int64)], @index4 ? @index3 : false) -[BLOCK_RECURSION_DEPTH_8]: cel.@block([msg.oneof_type, @index0.payload, @index1.map_string_string, (has(@index1.map_string_string) && has(@index2.key)) ? (@index2.key == "A") : false, has(msg.oneof_type) && has(@index0.payload) && has(@index1.single_int64)], @index4 ? @index3 : false) -[BLOCK_RECURSION_DEPTH_9]: cel.@block([msg.oneof_type, @index0.payload, @index1.map_string_string, (has(@index1.map_string_string) && has(@index2.key)) ? (@index2.key == "A") : false, has(msg.oneof_type) && has(@index0.payload) && has(@index1.single_int64)], @index4 ? @index3 : false) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([msg.oneof_type, @index0.payload, @index1.map_string_string, has(msg.oneof_type), has(@index0.payload), @index3 && @index4, has(@index1.single_int64), @index5 && @index6, has(@index1.map_string_string), has(@index2.key), @index8 && @index9, @index2.key, @index11 == "A", @index10 ? @index12 : false], @index7 ? @index13 : false) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([msg.oneof_type.payload, @index0.map_string_string, has(msg.oneof_type.payload), has(msg.oneof_type) && @index2, @index3 && has(@index0.single_int64), has(@index0.map_string_string) && has(@index1.key), @index1.key == "A"], @index4 ? (@index5 ? @index6 : false) : false) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([msg.oneof_type.payload.map_string_string, msg.oneof_type.payload, has(msg.oneof_type) && has(msg.oneof_type.payload), (has(@index1.map_string_string) && has(@index0.key)) ? (@index0.key == "A") : false], (@index2 && has(@index1.single_int64)) ? @index3 : false) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([msg.oneof_type.payload.map_string_string, msg.oneof_type.payload, has(msg.oneof_type) && has(msg.oneof_type.payload) && has(@index1.single_int64)], @index2 ? ((has(@index1.map_string_string) && has(@index0.key)) ? (@index0.key == "A") : false) : false) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([msg.oneof_type.payload.map_string_string, msg.oneof_type.payload], (has(msg.oneof_type) && has(msg.oneof_type.payload) && has(@index1.single_int64)) ? ((has(@index1.map_string_string) && has(@index0.key)) ? (@index0.key == "A") : false) : false) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([msg.oneof_type.payload.map_string_string, msg.oneof_type.payload], (has(msg.oneof_type) && has(msg.oneof_type.payload) && has(@index1.single_int64)) ? ((has(@index1.map_string_string) && has(@index0.key)) ? (@index0.key == "A") : false) : false) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([msg.oneof_type.payload.map_string_string, msg.oneof_type.payload], (has(msg.oneof_type) && has(msg.oneof_type.payload) && has(@index1.single_int64)) ? ((has(@index1.map_string_string) && has(@index0.key)) ? (@index0.key == "A") : false) : false) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([msg.oneof_type.payload.map_string_string, msg.oneof_type.payload], (has(msg.oneof_type) && has(msg.oneof_type.payload) && has(@index1.single_int64)) ? ((has(@index1.map_string_string) && has(@index0.key)) ? (@index0.key == "A") : false) : false) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([msg.oneof_type.payload.map_string_string, msg.oneof_type.payload], (has(msg.oneof_type) && has(msg.oneof_type.payload) && has(@index1.single_int64)) ? ((has(@index1.map_string_string) && has(@index0.key)) ? (@index0.key == "A") : false) : false) Test case: OPTIONAL_LIST Source: [10, ?optional.none(), [?optional.none(), ?opt_x], [?optional.none(), ?opt_x]] == [10, [5], [5]] =====> Result: true -[CASCADED_BINDS]: cel.bind(@r0, optional.none(), cel.bind(@r1, [?@r0, ?opt_x], [10, ?@r0, @r1, @r1])) == cel.bind(@r2, [5], [10, @r2, @r2]) [BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([optional.none(), [?@index0, ?opt_x], [5]], [10, ?@index0, @index1, @index1] == [10, @index2, @index2]) -[BLOCK_RECURSION_DEPTH_1]: cel.@block([optional.none(), [?@index0, ?opt_x], [5], [10, @index2, @index2], [10, ?@index0, @index1, @index1]], @index4 == @index3) -[BLOCK_RECURSION_DEPTH_2]: cel.@block([optional.none(), [?@index0, ?opt_x], [5], [10, @index2, @index2], [10, ?@index0, @index1, @index1]], @index4 == @index3) -[BLOCK_RECURSION_DEPTH_3]: cel.@block([optional.none(), [?@index0, ?opt_x], [5], [10, @index2, @index2], [10, ?@index0, @index1, @index1]], @index4 == @index3) -[BLOCK_RECURSION_DEPTH_4]: cel.@block([optional.none(), [?@index0, ?opt_x], [5], [10, @index2, @index2], [10, ?@index0, @index1, @index1]], @index4 == @index3) -[BLOCK_RECURSION_DEPTH_5]: cel.@block([optional.none(), [?@index0, ?opt_x], [5], [10, @index2, @index2], [10, ?@index0, @index1, @index1]], @index4 == @index3) -[BLOCK_RECURSION_DEPTH_6]: cel.@block([optional.none(), [?@index0, ?opt_x], [5], [10, @index2, @index2], [10, ?@index0, @index1, @index1]], @index4 == @index3) -[BLOCK_RECURSION_DEPTH_7]: cel.@block([optional.none(), [?@index0, ?opt_x], [5], [10, @index2, @index2], [10, ?@index0, @index1, @index1]], @index4 == @index3) -[BLOCK_RECURSION_DEPTH_8]: cel.@block([optional.none(), [?@index0, ?opt_x], [5], [10, @index2, @index2], [10, ?@index0, @index1, @index1]], @index4 == @index3) -[BLOCK_RECURSION_DEPTH_9]: cel.@block([optional.none(), [?@index0, ?opt_x], [5], [10, @index2, @index2], [10, ?@index0, @index1, @index1]], @index4 == @index3) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([optional.none(), [?@index0, ?opt_x], [5], [10, ?@index0, @index1, @index1], [10, @index2, @index2]], @index3 == @index4) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([[?optional.none(), ?opt_x], [5], [10, ?optional.none(), @index0, @index0]], @index2 == [10, @index1, @index1]) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([[?optional.none(), ?opt_x], [5]], [10, ?optional.none(), @index0, @index0] == [10, @index1, @index1]) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([[?optional.none(), ?opt_x], [5]], [10, ?optional.none(), @index0, @index0] == [10, @index1, @index1]) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([[?optional.none(), ?opt_x], [5]], [10, ?optional.none(), @index0, @index0] == [10, @index1, @index1]) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([[?optional.none(), ?opt_x], [5]], [10, ?optional.none(), @index0, @index0] == [10, @index1, @index1]) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([[?optional.none(), ?opt_x], [5]], [10, ?optional.none(), @index0, @index0] == [10, @index1, @index1]) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([[?optional.none(), ?opt_x], [5]], [10, ?optional.none(), @index0, @index0] == [10, @index1, @index1]) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([[?optional.none(), ?opt_x], [5]], [10, ?optional.none(), @index0, @index0] == [10, @index1, @index1]) Test case: OPTIONAL_MAP Source: {?'hello': optional.of('hello')}['hello'] + {?'hello': optional.of('hello')}['hello'] == 'hellohello' =====> Result: true -[CASCADED_BINDS]: cel.bind(@r0, {?"hello": optional.of("hello")}["hello"], @r0 + @r0) == "hellohello" [BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([{?"hello": optional.of("hello")}["hello"]], @index0 + @index0 == "hellohello") [BLOCK_RECURSION_DEPTH_1]: cel.@block([optional.of("hello"), {?"hello": @index0}, @index1["hello"], @index2 + @index2], @index3 == "hellohello") -[BLOCK_RECURSION_DEPTH_2]: cel.@block([optional.of("hello"), {?"hello": @index0}, @index1["hello"], @index2 + @index2], @index3 == "hellohello") -[BLOCK_RECURSION_DEPTH_3]: cel.@block([optional.of("hello"), {?"hello": @index0}, @index1["hello"], @index2 + @index2], @index3 == "hellohello") -[BLOCK_RECURSION_DEPTH_4]: cel.@block([optional.of("hello"), {?"hello": @index0}, @index1["hello"], @index2 + @index2], @index3 == "hellohello") -[BLOCK_RECURSION_DEPTH_5]: cel.@block([optional.of("hello"), {?"hello": @index0}, @index1["hello"], @index2 + @index2], @index3 == "hellohello") -[BLOCK_RECURSION_DEPTH_6]: cel.@block([optional.of("hello"), {?"hello": @index0}, @index1["hello"], @index2 + @index2], @index3 == "hellohello") -[BLOCK_RECURSION_DEPTH_7]: cel.@block([optional.of("hello"), {?"hello": @index0}, @index1["hello"], @index2 + @index2], @index3 == "hellohello") -[BLOCK_RECURSION_DEPTH_8]: cel.@block([optional.of("hello"), {?"hello": @index0}, @index1["hello"], @index2 + @index2], @index3 == "hellohello") -[BLOCK_RECURSION_DEPTH_9]: cel.@block([optional.of("hello"), {?"hello": @index0}, @index1["hello"], @index2 + @index2], @index3 == "hellohello") +[BLOCK_RECURSION_DEPTH_2]: cel.@block([{?"hello": optional.of("hello")}, @index0["hello"]], @index1 + @index1 == "hellohello") +[BLOCK_RECURSION_DEPTH_3]: cel.@block([{?"hello": optional.of("hello")}["hello"]], @index0 + @index0 == "hellohello") +[BLOCK_RECURSION_DEPTH_4]: cel.@block([{?"hello": optional.of("hello")}["hello"]], @index0 + @index0 == "hellohello") +[BLOCK_RECURSION_DEPTH_5]: cel.@block([{?"hello": optional.of("hello")}["hello"]], @index0 + @index0 == "hellohello") +[BLOCK_RECURSION_DEPTH_6]: cel.@block([{?"hello": optional.of("hello")}["hello"]], @index0 + @index0 == "hellohello") +[BLOCK_RECURSION_DEPTH_7]: cel.@block([{?"hello": optional.of("hello")}["hello"]], @index0 + @index0 == "hellohello") +[BLOCK_RECURSION_DEPTH_8]: cel.@block([{?"hello": optional.of("hello")}["hello"]], @index0 + @index0 == "hellohello") +[BLOCK_RECURSION_DEPTH_9]: cel.@block([{?"hello": optional.of("hello")}["hello"]], @index0 + @index0 == "hellohello") Test case: OPTIONAL_MAP_CHAINED Source: {?'key': optional.of('test')}[?'bogus'].or({'key': 'test'}[?'bogus']).orValue({'key': 'test'}['key']) == 'test' =====> Result: true -[CASCADED_BINDS]: cel.bind(@r0, {"key": "test"}, {?"key": optional.of("test")}[?"bogus"].or(@r0[?"bogus"]).orValue(@r0["key"])) == "test" [BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([{"key": "test"}], {?"key": optional.of("test")}[?"bogus"].or(@index0[?"bogus"]).orValue(@index0["key"]) == "test") -[BLOCK_RECURSION_DEPTH_1]: cel.@block([{"key": "test"}, @index0["key"], @index0[?"bogus"], optional.of("test"), {?"key": @index3}, @index4[?"bogus"], @index5.or(@index2), @index6.orValue(@index1)], @index7 == "test") -[BLOCK_RECURSION_DEPTH_2]: cel.@block([{"key": "test"}, @index0["key"], @index0[?"bogus"], {?"key": optional.of("test")}, @index3[?"bogus"].or(@index2), @index4.orValue(@index1)], @index5 == "test") -[BLOCK_RECURSION_DEPTH_3]: cel.@block([{"key": "test"}, @index0["key"], @index0[?"bogus"], {?"key": optional.of("test")}[?"bogus"], @index3.or(@index2).orValue(@index1)], @index4 == "test") -[BLOCK_RECURSION_DEPTH_4]: cel.@block([{"key": "test"}, @index0["key"], {?"key": optional.of("test")}[?"bogus"].or(@index0[?"bogus"]), @index2.orValue(@index1)], @index3 == "test") +[BLOCK_RECURSION_DEPTH_1]: cel.@block([{"key": "test"}, optional.of("test"), {?"key": @index1}, @index2[?"bogus"], @index0[?"bogus"], @index3.or(@index4), @index0["key"], @index5.orValue(@index6)], @index7 == "test") +[BLOCK_RECURSION_DEPTH_2]: cel.@block([{"key": "test"}, {?"key": optional.of("test")}, @index1[?"bogus"].or(@index0[?"bogus"]), @index2.orValue(@index0["key"])], @index3 == "test") +[BLOCK_RECURSION_DEPTH_3]: cel.@block([{"key": "test"}, {?"key": optional.of("test")}[?"bogus"], @index1.or(@index0[?"bogus"]).orValue(@index0["key"])], @index2 == "test") +[BLOCK_RECURSION_DEPTH_4]: cel.@block([{"key": "test"}, {?"key": optional.of("test")}[?"bogus"].or(@index0[?"bogus"])], @index1.orValue(@index0["key"]) == "test") [BLOCK_RECURSION_DEPTH_5]: cel.@block([{"key": "test"}, {?"key": optional.of("test")}[?"bogus"].or(@index0[?"bogus"]).orValue(@index0["key"])], @index1 == "test") -[BLOCK_RECURSION_DEPTH_6]: cel.@block([{"key": "test"}, {?"key": optional.of("test")}[?"bogus"].or(@index0[?"bogus"]).orValue(@index0["key"])], @index1 == "test") -[BLOCK_RECURSION_DEPTH_7]: cel.@block([{"key": "test"}, {?"key": optional.of("test")}[?"bogus"].or(@index0[?"bogus"]).orValue(@index0["key"])], @index1 == "test") -[BLOCK_RECURSION_DEPTH_8]: cel.@block([{"key": "test"}, {?"key": optional.of("test")}[?"bogus"].or(@index0[?"bogus"]).orValue(@index0["key"])], @index1 == "test") -[BLOCK_RECURSION_DEPTH_9]: cel.@block([{"key": "test"}, {?"key": optional.of("test")}[?"bogus"].or(@index0[?"bogus"]).orValue(@index0["key"])], @index1 == "test") +[BLOCK_RECURSION_DEPTH_6]: cel.@block([{"key": "test"}], {?"key": optional.of("test")}[?"bogus"].or(@index0[?"bogus"]).orValue(@index0["key"]) == "test") +[BLOCK_RECURSION_DEPTH_7]: cel.@block([{"key": "test"}], {?"key": optional.of("test")}[?"bogus"].or(@index0[?"bogus"]).orValue(@index0["key"]) == "test") +[BLOCK_RECURSION_DEPTH_8]: cel.@block([{"key": "test"}], {?"key": optional.of("test")}[?"bogus"].or(@index0[?"bogus"]).orValue(@index0["key"]) == "test") +[BLOCK_RECURSION_DEPTH_9]: cel.@block([{"key": "test"}], {?"key": optional.of("test")}[?"bogus"].or(@index0[?"bogus"]).orValue(@index0["key"]) == "test") Test case: OPTIONAL_MESSAGE Source: TestAllTypes{?single_int64: optional.ofNonZeroValue(1), ?single_int32: optional.of(4)}.single_int32 + TestAllTypes{?single_int64: optional.ofNonZeroValue(1), ?single_int32: optional.of(4)}.single_int64 == 5 =====> Result: true -[CASCADED_BINDS]: cel.bind(@r0, TestAllTypes{?single_int64: optional.ofNonZeroValue(1), ?single_int32: optional.of(4)}, @r0.single_int32 + @r0.single_int64) == 5 -[BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([TestAllTypes{?single_int64: optional.ofNonZeroValue(1), ?single_int32: optional.of(4)}], @index0.single_int32 + @index0.single_int64 == 5) -[BLOCK_RECURSION_DEPTH_1]: cel.@block([optional.ofNonZeroValue(1), optional.of(4), TestAllTypes{?single_int64: @index0, ?single_int32: @index1}, @index2.single_int64, @index2.single_int32, @index4 + @index3], @index5 == 5) -[BLOCK_RECURSION_DEPTH_2]: cel.@block([optional.ofNonZeroValue(1), optional.of(4), TestAllTypes{?single_int64: @index0, ?single_int32: @index1}, @index2.single_int32 + @index2.single_int64], @index3 == 5) -[BLOCK_RECURSION_DEPTH_3]: cel.@block([optional.ofNonZeroValue(1), optional.of(4), TestAllTypes{?single_int64: @index0, ?single_int32: @index1}, @index2.single_int32 + @index2.single_int64], @index3 == 5) -[BLOCK_RECURSION_DEPTH_4]: cel.@block([optional.ofNonZeroValue(1), optional.of(4), TestAllTypes{?single_int64: @index0, ?single_int32: @index1}, @index2.single_int32 + @index2.single_int64], @index3 == 5) -[BLOCK_RECURSION_DEPTH_5]: cel.@block([optional.ofNonZeroValue(1), optional.of(4), TestAllTypes{?single_int64: @index0, ?single_int32: @index1}, @index2.single_int32 + @index2.single_int64], @index3 == 5) -[BLOCK_RECURSION_DEPTH_6]: cel.@block([optional.ofNonZeroValue(1), optional.of(4), TestAllTypes{?single_int64: @index0, ?single_int32: @index1}, @index2.single_int32 + @index2.single_int64], @index3 == 5) -[BLOCK_RECURSION_DEPTH_7]: cel.@block([optional.ofNonZeroValue(1), optional.of(4), TestAllTypes{?single_int64: @index0, ?single_int32: @index1}, @index2.single_int32 + @index2.single_int64], @index3 == 5) -[BLOCK_RECURSION_DEPTH_8]: cel.@block([optional.ofNonZeroValue(1), optional.of(4), TestAllTypes{?single_int64: @index0, ?single_int32: @index1}, @index2.single_int32 + @index2.single_int64], @index3 == 5) -[BLOCK_RECURSION_DEPTH_9]: cel.@block([optional.ofNonZeroValue(1), optional.of(4), TestAllTypes{?single_int64: @index0, ?single_int32: @index1}, @index2.single_int32 + @index2.single_int64], @index3 == 5) +[BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([cel.expr.conformance.proto3.TestAllTypes{?single_int64: optional.ofNonZeroValue(1), ?single_int32: optional.of(4)}], @index0.single_int32 + @index0.single_int64 == 5) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([optional.ofNonZeroValue(1), optional.of(4), cel.expr.conformance.proto3.TestAllTypes{?single_int64: @index0, ?single_int32: @index1}, @index2.single_int32, @index2.single_int64, @index3 + @index4], @index5 == 5) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([cel.expr.conformance.proto3.TestAllTypes{?single_int64: optional.ofNonZeroValue(1), ?single_int32: optional.of(4)}, @index0.single_int32 + @index0.single_int64], @index1 == 5) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([cel.expr.conformance.proto3.TestAllTypes{?single_int64: optional.ofNonZeroValue(1), ?single_int32: optional.of(4)}], @index0.single_int32 + @index0.single_int64 == 5) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([cel.expr.conformance.proto3.TestAllTypes{?single_int64: optional.ofNonZeroValue(1), ?single_int32: optional.of(4)}], @index0.single_int32 + @index0.single_int64 == 5) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([cel.expr.conformance.proto3.TestAllTypes{?single_int64: optional.ofNonZeroValue(1), ?single_int32: optional.of(4)}], @index0.single_int32 + @index0.single_int64 == 5) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([cel.expr.conformance.proto3.TestAllTypes{?single_int64: optional.ofNonZeroValue(1), ?single_int32: optional.of(4)}], @index0.single_int32 + @index0.single_int64 == 5) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([cel.expr.conformance.proto3.TestAllTypes{?single_int64: optional.ofNonZeroValue(1), ?single_int32: optional.of(4)}], @index0.single_int32 + @index0.single_int64 == 5) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([cel.expr.conformance.proto3.TestAllTypes{?single_int64: optional.ofNonZeroValue(1), ?single_int32: optional.of(4)}], @index0.single_int32 + @index0.single_int64 == 5) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([cel.expr.conformance.proto3.TestAllTypes{?single_int64: optional.ofNonZeroValue(1), ?single_int32: optional.of(4)}], @index0.single_int32 + @index0.single_int64 == 5) Test case: CALL Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('h' + 'e' + 'l' + 'l' + 'o') =====> Result: true -[CASCADED_BINDS]: cel.bind(@r0, "h" + "e" + "l" + "l" + "o", (@r0 + " world").matches(@r0)) [BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block(["h" + "e" + "l" + "l" + "o"], (@index0 + " world").matches(@index0)) [BLOCK_RECURSION_DEPTH_1]: cel.@block(["h" + "e", @index0 + "l", @index1 + "l", @index2 + "o", @index3 + " world"], @index4.matches(@index3)) -[BLOCK_RECURSION_DEPTH_2]: cel.@block(["h" + "e", @index0 + "l", @index1 + "l", @index2 + "o", @index3 + " world"], @index4.matches(@index3)) -[BLOCK_RECURSION_DEPTH_3]: cel.@block(["h" + "e", @index0 + "l", @index1 + "l", @index2 + "o", @index3 + " world"], @index4.matches(@index3)) -[BLOCK_RECURSION_DEPTH_4]: cel.@block(["h" + "e", @index0 + "l", @index1 + "l", @index2 + "o", @index3 + " world"], @index4.matches(@index3)) -[BLOCK_RECURSION_DEPTH_5]: cel.@block(["h" + "e", @index0 + "l", @index1 + "l", @index2 + "o", @index3 + " world"], @index4.matches(@index3)) -[BLOCK_RECURSION_DEPTH_6]: cel.@block(["h" + "e", @index0 + "l", @index1 + "l", @index2 + "o", @index3 + " world"], @index4.matches(@index3)) -[BLOCK_RECURSION_DEPTH_7]: cel.@block(["h" + "e", @index0 + "l", @index1 + "l", @index2 + "o", @index3 + " world"], @index4.matches(@index3)) -[BLOCK_RECURSION_DEPTH_8]: cel.@block(["h" + "e", @index0 + "l", @index1 + "l", @index2 + "o", @index3 + " world"], @index4.matches(@index3)) -[BLOCK_RECURSION_DEPTH_9]: cel.@block(["h" + "e", @index0 + "l", @index1 + "l", @index2 + "o", @index3 + " world"], @index4.matches(@index3)) +[BLOCK_RECURSION_DEPTH_2]: cel.@block(["h" + "e" + "l", @index0 + "l" + "o"], (@index1 + " world").matches(@index1)) +[BLOCK_RECURSION_DEPTH_3]: cel.@block(["h" + "e" + "l" + "l", @index0 + "o"], (@index1 + " world").matches(@index1)) +[BLOCK_RECURSION_DEPTH_4]: cel.@block(["h" + "e" + "l" + "l" + "o"], (@index0 + " world").matches(@index0)) +[BLOCK_RECURSION_DEPTH_5]: cel.@block(["h" + "e" + "l" + "l" + "o"], (@index0 + " world").matches(@index0)) +[BLOCK_RECURSION_DEPTH_6]: cel.@block(["h" + "e" + "l" + "l" + "o"], (@index0 + " world").matches(@index0)) +[BLOCK_RECURSION_DEPTH_7]: cel.@block(["h" + "e" + "l" + "l" + "o"], (@index0 + " world").matches(@index0)) +[BLOCK_RECURSION_DEPTH_8]: cel.@block(["h" + "e" + "l" + "l" + "o"], (@index0 + " world").matches(@index0)) +[BLOCK_RECURSION_DEPTH_9]: cel.@block(["h" + "e" + "l" + "l" + "o"], (@index0 + " world").matches(@index0)) Test case: CALL_ARGUMENT_NESTED_NO_COMMON_SUBEXPR Source: 'hello world'.matches('h' + 'e' + 'l' + 'l' + 'o') =====> Result: true -[CASCADED_BINDS]: "hello world".matches("h" + "e" + "l" + "l" + "o") [BLOCK_COMMON_SUBEXPR_ONLY]: "hello world".matches("h" + "e" + "l" + "l" + "o") [BLOCK_RECURSION_DEPTH_1]: cel.@block(["h" + "e", @index0 + "l", @index1 + "l", @index2 + "o"], "hello world".matches(@index3)) [BLOCK_RECURSION_DEPTH_2]: cel.@block(["h" + "e" + "l", @index0 + "l" + "o"], "hello world".matches(@index1)) -[BLOCK_RECURSION_DEPTH_3]: cel.@block(["h" + "e" + "l" + "l", @index0 + "o"], "hello world".matches(@index1)) +[BLOCK_RECURSION_DEPTH_3]: cel.@block(["h" + "e" + "l" + "l"], "hello world".matches(@index0 + "o")) [BLOCK_RECURSION_DEPTH_4]: cel.@block(["h" + "e" + "l" + "l" + "o"], "hello world".matches(@index0)) -[BLOCK_RECURSION_DEPTH_5]: cel.@block(["h" + "e" + "l" + "l" + "o"], "hello world".matches(@index0)) -[BLOCK_RECURSION_DEPTH_6]: cel.@block(["h" + "e" + "l" + "l" + "o"], "hello world".matches(@index0)) -[BLOCK_RECURSION_DEPTH_7]: cel.@block(["h" + "e" + "l" + "l" + "o"], "hello world".matches(@index0)) -[BLOCK_RECURSION_DEPTH_8]: cel.@block(["h" + "e" + "l" + "l" + "o"], "hello world".matches(@index0)) -[BLOCK_RECURSION_DEPTH_9]: cel.@block(["h" + "e" + "l" + "l" + "o"], "hello world".matches(@index0)) +[BLOCK_RECURSION_DEPTH_5]: "hello world".matches("h" + "e" + "l" + "l" + "o") +[BLOCK_RECURSION_DEPTH_6]: "hello world".matches("h" + "e" + "l" + "l" + "o") +[BLOCK_RECURSION_DEPTH_7]: "hello world".matches("h" + "e" + "l" + "l" + "o") +[BLOCK_RECURSION_DEPTH_8]: "hello world".matches("h" + "e" + "l" + "l" + "o") +[BLOCK_RECURSION_DEPTH_9]: "hello world".matches("h" + "e" + "l" + "l" + "o") Test case: CALL_TARGET_NESTED_NO_COMMON_SUBEXPR Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('hello') =====> Result: true -[CASCADED_BINDS]: ("h" + "e" + "l" + "l" + "o" + " world").matches("hello") [BLOCK_COMMON_SUBEXPR_ONLY]: ("h" + "e" + "l" + "l" + "o" + " world").matches("hello") [BLOCK_RECURSION_DEPTH_1]: cel.@block(["h" + "e", @index0 + "l", @index1 + "l", @index2 + "o", @index3 + " world"], @index4.matches("hello")) -[BLOCK_RECURSION_DEPTH_2]: cel.@block(["h" + "e" + "l", @index0 + "l" + "o", @index1 + " world"], @index2.matches("hello")) -[BLOCK_RECURSION_DEPTH_3]: cel.@block(["h" + "e" + "l" + "l", @index0 + "o" + " world"], @index1.matches("hello")) -[BLOCK_RECURSION_DEPTH_4]: cel.@block(["h" + "e" + "l" + "l" + "o", @index0 + " world"], @index1.matches("hello")) +[BLOCK_RECURSION_DEPTH_2]: cel.@block(["h" + "e" + "l", @index0 + "l" + "o"], (@index1 + " world").matches("hello")) +[BLOCK_RECURSION_DEPTH_3]: cel.@block(["h" + "e" + "l" + "l"], (@index0 + "o" + " world").matches("hello")) +[BLOCK_RECURSION_DEPTH_4]: cel.@block(["h" + "e" + "l" + "l" + "o"], (@index0 + " world").matches("hello")) [BLOCK_RECURSION_DEPTH_5]: cel.@block(["h" + "e" + "l" + "l" + "o" + " world"], @index0.matches("hello")) -[BLOCK_RECURSION_DEPTH_6]: cel.@block(["h" + "e" + "l" + "l" + "o" + " world"], @index0.matches("hello")) -[BLOCK_RECURSION_DEPTH_7]: cel.@block(["h" + "e" + "l" + "l" + "o" + " world"], @index0.matches("hello")) -[BLOCK_RECURSION_DEPTH_8]: cel.@block(["h" + "e" + "l" + "l" + "o" + " world"], @index0.matches("hello")) -[BLOCK_RECURSION_DEPTH_9]: cel.@block(["h" + "e" + "l" + "l" + "o" + " world"], @index0.matches("hello")) +[BLOCK_RECURSION_DEPTH_6]: ("h" + "e" + "l" + "l" + "o" + " world").matches("hello") +[BLOCK_RECURSION_DEPTH_7]: ("h" + "e" + "l" + "l" + "o" + " world").matches("hello") +[BLOCK_RECURSION_DEPTH_8]: ("h" + "e" + "l" + "l" + "o" + " world").matches("hello") +[BLOCK_RECURSION_DEPTH_9]: ("h" + "e" + "l" + "l" + "o" + " world").matches("hello") Test case: CALL_BOTH_ARGUMENT_TARGET_NESTED_NO_COMMON_SUBEXPR Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('w' + 'o' + 'r' + 'l' + 'd') =====> Result: true -[CASCADED_BINDS]: ("h" + "e" + "l" + "l" + "o" + " world").matches("w" + "o" + "r" + "l" + "d") [BLOCK_COMMON_SUBEXPR_ONLY]: ("h" + "e" + "l" + "l" + "o" + " world").matches("w" + "o" + "r" + "l" + "d") -[BLOCK_RECURSION_DEPTH_1]: cel.@block(["w" + "o", @index0 + "r", @index1 + "l", @index2 + "d", "h" + "e", @index4 + "l", @index5 + "l", @index6 + "o", @index7 + " world"], @index8.matches(@index3)) -[BLOCK_RECURSION_DEPTH_2]: cel.@block(["w" + "o" + "r", @index0 + "l" + "d", "h" + "e" + "l", @index2 + "l" + "o", @index3 + " world"], @index4.matches(@index1)) -[BLOCK_RECURSION_DEPTH_3]: cel.@block(["w" + "o" + "r" + "l", @index0 + "d", "h" + "e" + "l" + "l", @index2 + "o" + " world"], @index3.matches(@index1)) -[BLOCK_RECURSION_DEPTH_4]: cel.@block(["w" + "o" + "r" + "l" + "d", "h" + "e" + "l" + "l" + "o", @index1 + " world"], @index2.matches(@index0)) -[BLOCK_RECURSION_DEPTH_5]: cel.@block(["w" + "o" + "r" + "l" + "d", "h" + "e" + "l" + "l" + "o" + " world"], @index1.matches(@index0)) -[BLOCK_RECURSION_DEPTH_6]: cel.@block(["w" + "o" + "r" + "l" + "d", "h" + "e" + "l" + "l" + "o" + " world"], @index1.matches(@index0)) -[BLOCK_RECURSION_DEPTH_7]: cel.@block(["w" + "o" + "r" + "l" + "d", "h" + "e" + "l" + "l" + "o" + " world"], @index1.matches(@index0)) -[BLOCK_RECURSION_DEPTH_8]: cel.@block(["w" + "o" + "r" + "l" + "d", "h" + "e" + "l" + "l" + "o" + " world"], @index1.matches(@index0)) -[BLOCK_RECURSION_DEPTH_9]: cel.@block(["w" + "o" + "r" + "l" + "d", "h" + "e" + "l" + "l" + "o" + " world"], @index1.matches(@index0)) +[BLOCK_RECURSION_DEPTH_1]: cel.@block(["h" + "e", @index0 + "l", @index1 + "l", @index2 + "o", @index3 + " world", "w" + "o", @index5 + "r", @index6 + "l", @index7 + "d"], @index4.matches(@index8)) +[BLOCK_RECURSION_DEPTH_2]: cel.@block(["h" + "e" + "l", @index0 + "l" + "o", "w" + "o" + "r", @index2 + "l" + "d"], (@index1 + " world").matches(@index3)) +[BLOCK_RECURSION_DEPTH_3]: cel.@block(["h" + "e" + "l" + "l", "w" + "o" + "r" + "l"], (@index0 + "o" + " world").matches(@index1 + "d")) +[BLOCK_RECURSION_DEPTH_4]: cel.@block(["h" + "e" + "l" + "l" + "o", "w" + "o" + "r" + "l" + "d"], (@index0 + " world").matches(@index1)) +[BLOCK_RECURSION_DEPTH_5]: cel.@block(["h" + "e" + "l" + "l" + "o" + " world"], @index0.matches("w" + "o" + "r" + "l" + "d")) +[BLOCK_RECURSION_DEPTH_6]: ("h" + "e" + "l" + "l" + "o" + " world").matches("w" + "o" + "r" + "l" + "d") +[BLOCK_RECURSION_DEPTH_7]: ("h" + "e" + "l" + "l" + "o" + " world").matches("w" + "o" + "r" + "l" + "d") +[BLOCK_RECURSION_DEPTH_8]: ("h" + "e" + "l" + "l" + "o" + " world").matches("w" + "o" + "r" + "l" + "d") +[BLOCK_RECURSION_DEPTH_9]: ("h" + "e" + "l" + "l" + "o" + " world").matches("w" + "o" + "r" + "l" + "d") Test case: CUSTOM_FUNCTION_INELIMINABLE Source: non_pure_custom_func(msg.oneof_type.payload.single_int64) + non_pure_custom_func(msg.oneof_type.payload.single_int32) + non_pure_custom_func(msg.oneof_type.payload.single_int64) + non_pure_custom_func(msg.single_int64) =====> Result: 31 -[CASCADED_BINDS]: cel.bind(@r0, msg.oneof_type.payload, cel.bind(@r1, @r0.single_int64, non_pure_custom_func(@r1) + non_pure_custom_func(@r0.single_int32) + non_pure_custom_func(@r1))) + non_pure_custom_func(msg.single_int64) [BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([msg.oneof_type.payload, @index0.single_int64], non_pure_custom_func(@index1) + non_pure_custom_func(@index0.single_int32) + non_pure_custom_func(@index1) + non_pure_custom_func(msg.single_int64)) -[BLOCK_RECURSION_DEPTH_1]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, msg.single_int64, @index1.single_int32], non_pure_custom_func(@index2) + non_pure_custom_func(@index4) + non_pure_custom_func(@index2) + non_pure_custom_func(@index3)) -[BLOCK_RECURSION_DEPTH_2]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, msg.single_int64, @index1.single_int32], non_pure_custom_func(@index2) + non_pure_custom_func(@index4) + non_pure_custom_func(@index2) + non_pure_custom_func(@index3)) -[BLOCK_RECURSION_DEPTH_3]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, msg.single_int64, @index1.single_int32], non_pure_custom_func(@index2) + non_pure_custom_func(@index4) + non_pure_custom_func(@index2) + non_pure_custom_func(@index3)) -[BLOCK_RECURSION_DEPTH_4]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, msg.single_int64, @index1.single_int32], non_pure_custom_func(@index2) + non_pure_custom_func(@index4) + non_pure_custom_func(@index2) + non_pure_custom_func(@index3)) -[BLOCK_RECURSION_DEPTH_5]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, msg.single_int64, @index1.single_int32], non_pure_custom_func(@index2) + non_pure_custom_func(@index4) + non_pure_custom_func(@index2) + non_pure_custom_func(@index3)) -[BLOCK_RECURSION_DEPTH_6]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, msg.single_int64, @index1.single_int32], non_pure_custom_func(@index2) + non_pure_custom_func(@index4) + non_pure_custom_func(@index2) + non_pure_custom_func(@index3)) -[BLOCK_RECURSION_DEPTH_7]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, msg.single_int64, @index1.single_int32], non_pure_custom_func(@index2) + non_pure_custom_func(@index4) + non_pure_custom_func(@index2) + non_pure_custom_func(@index3)) -[BLOCK_RECURSION_DEPTH_8]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, msg.single_int64, @index1.single_int32], non_pure_custom_func(@index2) + non_pure_custom_func(@index4) + non_pure_custom_func(@index2) + non_pure_custom_func(@index3)) -[BLOCK_RECURSION_DEPTH_9]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, msg.single_int64, @index1.single_int32], non_pure_custom_func(@index2) + non_pure_custom_func(@index4) + non_pure_custom_func(@index2) + non_pure_custom_func(@index3)) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64], non_pure_custom_func(@index2) + non_pure_custom_func(@index1.single_int32) + non_pure_custom_func(@index2) + non_pure_custom_func(msg.single_int64)) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([msg.oneof_type.payload, @index0.single_int64], non_pure_custom_func(@index1) + non_pure_custom_func(@index0.single_int32) + non_pure_custom_func(@index1) + non_pure_custom_func(msg.single_int64)) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([msg.oneof_type.payload.single_int64], non_pure_custom_func(@index0) + non_pure_custom_func(msg.oneof_type.payload.single_int32) + non_pure_custom_func(@index0) + non_pure_custom_func(msg.single_int64)) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([msg.oneof_type.payload.single_int64], non_pure_custom_func(@index0) + non_pure_custom_func(msg.oneof_type.payload.single_int32) + non_pure_custom_func(@index0) + non_pure_custom_func(msg.single_int64)) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([msg.oneof_type.payload.single_int64], non_pure_custom_func(@index0) + non_pure_custom_func(msg.oneof_type.payload.single_int32) + non_pure_custom_func(@index0) + non_pure_custom_func(msg.single_int64)) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([msg.oneof_type.payload.single_int64], non_pure_custom_func(@index0) + non_pure_custom_func(msg.oneof_type.payload.single_int32) + non_pure_custom_func(@index0) + non_pure_custom_func(msg.single_int64)) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([msg.oneof_type.payload.single_int64], non_pure_custom_func(@index0) + non_pure_custom_func(msg.oneof_type.payload.single_int32) + non_pure_custom_func(@index0) + non_pure_custom_func(msg.single_int64)) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([msg.oneof_type.payload.single_int64], non_pure_custom_func(@index0) + non_pure_custom_func(msg.oneof_type.payload.single_int32) + non_pure_custom_func(@index0) + non_pure_custom_func(msg.single_int64)) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([msg.oneof_type.payload.single_int64], non_pure_custom_func(@index0) + non_pure_custom_func(msg.oneof_type.payload.single_int32) + non_pure_custom_func(@index0) + non_pure_custom_func(msg.single_int64)) Test case: CUSTOM_FUNCTION_ELIMINABLE Source: pure_custom_func(msg.oneof_type.payload.single_int64) + pure_custom_func(msg.oneof_type.payload.single_int32) + pure_custom_func(msg.oneof_type.payload.single_int64) + pure_custom_func(msg.single_int64) =====> Result: 31 -[CASCADED_BINDS]: cel.bind(@r0, msg.oneof_type.payload, cel.bind(@r1, pure_custom_func(@r0.single_int64), @r1 + pure_custom_func(@r0.single_int32) + @r1)) + pure_custom_func(msg.single_int64) [BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([msg.oneof_type.payload, pure_custom_func(@index0.single_int64)], @index1 + pure_custom_func(@index0.single_int32) + @index1 + pure_custom_func(msg.single_int64)) -[BLOCK_RECURSION_DEPTH_1]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, pure_custom_func(@index2), msg.single_int64, pure_custom_func(@index4), @index1.single_int32, pure_custom_func(@index6), @index3 + @index7, @index8 + @index3], @index9 + @index5) -[BLOCK_RECURSION_DEPTH_2]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, pure_custom_func(@index2), pure_custom_func(msg.single_int64), pure_custom_func(@index1.single_int32), @index3 + @index5 + @index3], @index6 + @index4) -[BLOCK_RECURSION_DEPTH_3]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, pure_custom_func(@index2), pure_custom_func(msg.single_int64), @index3 + pure_custom_func(@index1.single_int32), @index5 + @index3], @index6 + @index4) -[BLOCK_RECURSION_DEPTH_4]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, pure_custom_func(@index2), pure_custom_func(msg.single_int64), @index3 + pure_custom_func(@index1.single_int32) + @index3], @index5 + @index4) -[BLOCK_RECURSION_DEPTH_5]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, pure_custom_func(@index2), pure_custom_func(msg.single_int64), @index3 + pure_custom_func(@index1.single_int32) + @index3], @index5 + @index4) -[BLOCK_RECURSION_DEPTH_6]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, pure_custom_func(@index2), pure_custom_func(msg.single_int64), @index3 + pure_custom_func(@index1.single_int32) + @index3], @index5 + @index4) -[BLOCK_RECURSION_DEPTH_7]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, pure_custom_func(@index2), pure_custom_func(msg.single_int64), @index3 + pure_custom_func(@index1.single_int32) + @index3], @index5 + @index4) -[BLOCK_RECURSION_DEPTH_8]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, pure_custom_func(@index2), pure_custom_func(msg.single_int64), @index3 + pure_custom_func(@index1.single_int32) + @index3], @index5 + @index4) -[BLOCK_RECURSION_DEPTH_9]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, pure_custom_func(@index2), pure_custom_func(msg.single_int64), @index3 + pure_custom_func(@index1.single_int32) + @index3], @index5 + @index4) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([msg.oneof_type, @index0.payload, @index1.single_int64, pure_custom_func(@index2), @index1.single_int32, pure_custom_func(@index4), @index3 + @index5, @index6 + @index3, msg.single_int64, pure_custom_func(@index8)], @index7 + @index9) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([msg.oneof_type.payload, pure_custom_func(@index0.single_int64), pure_custom_func(@index0.single_int32), @index1 + @index2 + @index1, pure_custom_func(msg.single_int64)], @index3 + @index4) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([msg.oneof_type.payload.single_int64, pure_custom_func(@index0), msg.oneof_type.payload.single_int32, @index1 + pure_custom_func(@index2) + @index1], @index3 + pure_custom_func(msg.single_int64)) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([pure_custom_func(msg.oneof_type.payload.single_int64), pure_custom_func(msg.oneof_type.payload.single_int32)], @index0 + @index1 + @index0 + pure_custom_func(msg.single_int64)) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([pure_custom_func(msg.oneof_type.payload.single_int64), @index0 + pure_custom_func(msg.oneof_type.payload.single_int32)], @index1 + @index0 + pure_custom_func(msg.single_int64)) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([pure_custom_func(msg.oneof_type.payload.single_int64), @index0 + pure_custom_func(msg.oneof_type.payload.single_int32) + @index0], @index1 + pure_custom_func(msg.single_int64)) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([pure_custom_func(msg.oneof_type.payload.single_int64)], @index0 + pure_custom_func(msg.oneof_type.payload.single_int32) + @index0 + pure_custom_func(msg.single_int64)) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([pure_custom_func(msg.oneof_type.payload.single_int64)], @index0 + pure_custom_func(msg.oneof_type.payload.single_int32) + @index0 + pure_custom_func(msg.single_int64)) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([pure_custom_func(msg.oneof_type.payload.single_int64)], @index0 + pure_custom_func(msg.oneof_type.payload.single_int32) + @index0 + pure_custom_func(msg.single_int64)) \ No newline at end of file diff --git a/parser/BUILD.bazel b/parser/BUILD.bazel index ba31cd46a..1e662c3c5 100644 --- a/parser/BUILD.bazel +++ b/parser/BUILD.bazel @@ -1,3 +1,5 @@ +load("@rules_java//java:defs.bzl", "java_library") + package( default_applicable_licenses = ["//:license"], default_visibility = ["//visibility:public"], @@ -5,27 +7,33 @@ package( java_library( name = "parser", + visibility = ["//:internal"], exports = ["//parser/src/main/java/dev/cel/parser"], ) +java_library( + name = "parser_factory", + exports = ["//parser/src/main/java/dev/cel/parser:parser_factory"], +) + java_library( name = "parser_builder", exports = ["//parser/src/main/java/dev/cel/parser:parser_builder"], ) java_library( - name = "macro", - exports = ["//parser/src/main/java/dev/cel/parser:macro"], + name = "unparser_visitor", + exports = ["//parser/src/main/java/dev/cel/parser:unparser_visitor"], ) java_library( - name = "operator", - exports = ["//parser/src/main/java/dev/cel/parser:operator"], + name = "macro", + exports = ["//parser/src/main/java/dev/cel/parser:macro"], ) java_library( name = "cel_g4_visitors", - visibility = ["//visibility:public"], + visibility = ["//:internal"], exports = ["//parser/src/main/java/dev/cel/parser/gen:cel_g4_visitors"], ) diff --git a/parser/src/main/java/dev/cel/parser/BUILD.bazel b/parser/src/main/java/dev/cel/parser/BUILD.bazel index f49628e34..e32c50ee8 100644 --- a/parser/src/main/java/dev/cel/parser/BUILD.bazel +++ b/parser/src/main/java/dev/cel/parser/BUILD.bazel @@ -1,3 +1,5 @@ +load("@rules_java//java:defs.bzl", "java_library") + package( default_applicable_licenses = ["//:license"], default_visibility = [ @@ -8,7 +10,6 @@ package( # keep sorted PARSER_SOURCES = [ - "CelParserFactory.java", "CelParserImpl.java", "ExpressionBalancer.java", "Parser.java", @@ -36,6 +37,18 @@ UNPARSER_SOURCES = [ "CelUnparserImpl.java", ] +java_library( + name = "parser_factory", + srcs = ["CelParserFactory.java"], + tags = [ + ], + deps = [ + ":parser", + "//common:options", + "//parser:parser_builder", + ], +) + java_library( name = "parser", srcs = PARSER_SOURCES, @@ -43,14 +56,18 @@ java_library( ], deps = [ ":macro", - ":operator", ":parser_builder", - "//common", + "//common:cel_ast", + "//common:cel_source", "//common:compiler_common", + "//common:operator", "//common:options", + "//common:source_location", "//common/annotations", "//common/ast", "//common/internal", + "//common/internal:code_point_stream", + "//common/internal:env_visitor", "//parser:cel_g4_visitors", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", @@ -65,7 +82,7 @@ java_library( ], deps = [ ":macro", - "//common", + "//common:cel_source", "//common:compiler_common", "//common:options", "@maven//:com_google_errorprone_error_prone_annotations", @@ -78,26 +95,14 @@ java_library( tags = [ ], deps = [ - ":operator", "//:auto_value", - "//common", "//common:compiler_common", + "//common:operator", + "//common:source_location", "//common/ast", "//common/ast:expr_factory", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", - "@maven//:org_jspecify_jspecify", - ], -) - -java_library( - name = "operator", - srcs = ["Operator.java"], - tags = [ - ], - deps = [ - "//common/ast", - "@maven//:com_google_guava_guava", ], ) @@ -108,7 +113,7 @@ java_library( ], deps = [ ":unparser_visitor", - "//common", + "//common:cel_ast", "//common:options", ], ) @@ -119,10 +124,13 @@ java_library( tags = [ ], deps = [ - ":operator", - "//common", + "//common:cel_ast", + "//common:cel_source", + "//common:operator", "//common/ast", "//common/ast:cel_expr_visitor", - "@maven//:com_google_protobuf_protobuf_java", + "//common/values:cel_byte_string", + "@maven//:com_google_guava_guava", + "@maven//:com_google_re2j_re2j", ], ) diff --git a/parser/src/main/java/dev/cel/parser/CelMacroExprFactory.java b/parser/src/main/java/dev/cel/parser/CelMacroExprFactory.java index 9a01d7968..e0dcb5888 100644 --- a/parser/src/main/java/dev/cel/parser/CelMacroExprFactory.java +++ b/parser/src/main/java/dev/cel/parser/CelMacroExprFactory.java @@ -51,11 +51,109 @@ public final CelExpr reportError(String message) { /** Reports a {@link CelIssue} and returns a sentinel {@link CelExpr} that indicates an error. */ public abstract CelExpr reportError(CelIssue error); + /** Returns the default accumulator variable name used by macros implementing comprehensions. */ + public abstract String getAccumulatorVarName(); + /** Retrieves the source location for the given {@link CelExpr} ID. */ public final CelSourceLocation getSourceLocation(CelExpr expr) { return getSourceLocation(expr.id()); } + /** Duplicates {@link CelExpr} with a brand new set of identifiers. */ + public final CelExpr copy(CelExpr expr) { + CelExpr.Builder builder = CelExpr.newBuilder().setId(copyExprId(expr.id())); + switch (expr.exprKind().getKind()) { + case CONSTANT: + builder.setConstant(expr.constant()); + break; + case IDENT: + builder.setIdent(expr.ident()); + break; + case SELECT: + builder.setSelect( + CelExpr.CelSelect.newBuilder() + .setOperand(copy(expr.select().operand())) + .setField(expr.select().field()) + .setTestOnly(expr.select().testOnly()) + .build()); + break; + case CALL: + { + CelExpr.CelCall.Builder callBuilder = + CelExpr.CelCall.newBuilder().setFunction(expr.call().function()); + expr.call().target().ifPresent(target -> callBuilder.setTarget(copy(target))); + for (CelExpr arg : expr.call().args()) { + callBuilder.addArgs(copy(arg)); + } + builder.setCall(callBuilder.build()); + } + break; + case LIST: + { + CelExpr.CelList.Builder listBuilder = + CelExpr.CelList.newBuilder().addOptionalIndices(expr.list().optionalIndices()); + for (CelExpr element : expr.list().elements()) { + listBuilder.addElements(copy(element)); + } + builder.setList(listBuilder.build()); + } + break; + case STRUCT: + { + CelExpr.CelStruct.Builder structBuilder = + CelExpr.CelStruct.newBuilder().setMessageName(expr.struct().messageName()); + for (CelExpr.CelStruct.Entry entry : expr.struct().entries()) { + structBuilder.addEntries( + CelExpr.CelStruct.Entry.newBuilder() + .setId(copyExprId(entry.id())) + .setFieldKey(entry.fieldKey()) + .setValue(copy(entry.value())) + .setOptionalEntry(entry.optionalEntry()) + .build()); + } + builder.setStruct(structBuilder.build()); + } + break; + case MAP: + { + CelExpr.CelMap.Builder mapBuilder = CelExpr.CelMap.newBuilder(); + for (CelExpr.CelMap.Entry entry : expr.map().entries()) { + mapBuilder.addEntries( + CelExpr.CelMap.Entry.newBuilder() + .setId(copyExprId(entry.id())) + .setKey(copy(entry.key())) + .setValue(copy(entry.value())) + .setOptionalEntry(entry.optionalEntry()) + .build()); + } + builder.setMap(mapBuilder.build()); + } + break; + case COMPREHENSION: + builder.setComprehension( + CelExpr.CelComprehension.newBuilder() + .setIterVar(expr.comprehension().iterVar()) + .setIterVar2(expr.comprehension().iterVar2()) + .setIterRange(copy(expr.comprehension().iterRange())) + .setAccuVar(expr.comprehension().accuVar()) + .setAccuInit(copy(expr.comprehension().accuInit())) + .setLoopCondition(copy(expr.comprehension().loopCondition())) + .setLoopStep(copy(expr.comprehension().loopStep())) + .setResult(copy(expr.comprehension().result())) + .build()); + break; + case NOT_SET: + break; + } + return builder.build(); + } + + /** + * Returns the next unique expression ID which is associated with the same metadata (i.e. source + * location, types, references, etc.) as `id`. + */ + protected abstract long copyExprId(long id); + /** Retrieves the source location for the given {@link CelExpr} ID. */ protected abstract CelSourceLocation getSourceLocation(long exprId); diff --git a/parser/src/main/java/dev/cel/parser/CelParserImpl.java b/parser/src/main/java/dev/cel/parser/CelParserImpl.java index f630f7137..615b073ef 100644 --- a/parser/src/main/java/dev/cel/parser/CelParserImpl.java +++ b/parser/src/main/java/dev/cel/parser/CelParserImpl.java @@ -15,23 +15,30 @@ 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; import dev.cel.common.CelSource; import dev.cel.common.CelValidationResult; 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. @@ -41,7 +48,7 @@ */ @Immutable @Internal -public final class CelParserImpl implements CelParser { +public final class CelParserImpl implements CelParser, EnvVisitable { // Common feature flags to be used with all calls. @@ -51,9 +58,10 @@ public final class CelParserImpl implements CelParser { // Specific options for limits on parsing power. private final CelOptions options; - // Builder is mutable by design. APIs must make defensive copies in and out of this class. - @SuppressWarnings("Immutable") - private final Builder parserBuilder; + private final ImmutableList standardMacros; + + @SuppressWarnings("Immutable") // Interface not marked as immutable, however it should be. + private final ImmutableSet parserLibraries; /** Creates a new {@link Builder}. */ public static CelParserBuilder newBuilder() { @@ -72,7 +80,20 @@ public CelValidationResult parse(CelSource source) { @Override public CelParserBuilder toParserBuilder() { - return new Builder(parserBuilder); + 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) { @@ -135,6 +156,7 @@ public CelParserBuilder addLibraries(Iterable librar return this; } + @CanIgnoreReturnValue @Override public Builder setOptions(CelOptions options) { this.options = checkNotNull(options); @@ -171,12 +193,17 @@ public CelParserImpl build() { // Add libraries, such as extensions parserLibrarySet.forEach(celLibrary -> celLibrary.setParserOptions(this)); - ImmutableMap.Builder builder = ImmutableMap.builder(); - builder.putAll(macros); + ImmutableMap.Builder macroMapBuilder = ImmutableMap.builder(); + macroMapBuilder.putAll(macros); standardMacros.stream() .map(CelStandardMacro::getDefinition) - .forEach(celMacro -> builder.put(celMacro.getKey(), celMacro)); - return new CelParserImpl(builder.buildOrThrow(), checkNotNull(options), this); + .forEach(celMacro -> macroMapBuilder.put(celMacro.getKey(), celMacro)); + + return new CelParserImpl( + macroMapBuilder.buildOrThrow(), + options, + ImmutableList.copyOf(standardMacros), + celParserLibraries.build()); } private Builder() { @@ -184,23 +211,21 @@ private Builder() { this.celParserLibraries = ImmutableSet.builder(); this.standardMacros = new ArrayList<>(); } - - private Builder(Builder builder) { - // The following properties are either immutable or simple primitives, thus can be assigned - // directly. - this.options = builder.options; - // The following needs to be deep copied as they are collection builders - this.macros = new HashMap<>(builder.macros); - this.standardMacros = new ArrayList<>(builder.standardMacros); - this.celParserLibraries = ImmutableSet.builder(); - this.celParserLibraries.addAll(builder.celParserLibraries.build()); - } } private CelParserImpl( - ImmutableMap macros, CelOptions options, Builder parserBuilder) { + ImmutableMap macros, + CelOptions options, + ImmutableList standardMacros, + ImmutableSet parserLibraries) { this.macros = macros; - this.options = options; - this.parserBuilder = new Builder(parserBuilder); + this.options = checkNotNull(options); + this.standardMacros = standardMacros; + this.parserLibraries = parserLibraries; + } + + @Override + public void accept(EnvVisitor visitor) { + macros.forEach((name, macro) -> visitor.visitMacro(macro)); } } diff --git a/parser/src/main/java/dev/cel/parser/CelStandardMacro.java b/parser/src/main/java/dev/cel/parser/CelStandardMacro.java index 3d93e8531..275159569 100644 --- a/parser/src/main/java/dev/cel/parser/CelStandardMacro.java +++ b/parser/src/main/java/dev/cel/parser/CelStandardMacro.java @@ -20,6 +20,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import dev.cel.common.CelIssue; +import dev.cel.common.Operator; import dev.cel.common.ast.CelExpr; import java.util.Optional; @@ -53,6 +54,14 @@ public enum CelStandardMacro { CelMacro.newReceiverMacro( Operator.EXISTS_ONE.getFunction(), 2, CelStandardMacro::expandExistsOneMacro)), + /** + * Boolean comprehension which asserts that a predicate holds true for exactly one element in the + * input range. + */ + EXISTS_ONE_NEW( + CelMacro.newReceiverMacro( + Operator.EXISTS_ONE_NEW.getFunction(), 2, CelStandardMacro::expandExistsOneMacro)), + /** * Comprehension which applies a transform to each element in the input range and produces a list * of equivalent size as output. @@ -78,8 +87,6 @@ public enum CelStandardMacro { public static final ImmutableSet STANDARD_MACROS = ImmutableSet.of(HAS, ALL, EXISTS, EXISTS_ONE, MAP, MAP_FILTER, FILTER); - private static final String ACCUMULATOR_VAR = "__result__"; - private final CelMacro macro; CelStandardMacro(CelMacro macro) { @@ -115,22 +122,31 @@ 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); CelExpr condition = exprFactory.newGlobalCall( - Operator.NOT_STRICTLY_FALSE.getFunction(), exprFactory.newIdentifier(ACCUMULATOR_VAR)); + Operator.NOT_STRICTLY_FALSE.getFunction(), + exprFactory.newIdentifier(exprFactory.getAccumulatorVarName())); CelExpr step = exprFactory.newGlobalCall( - Operator.LOGICAL_AND.getFunction(), exprFactory.newIdentifier(ACCUMULATOR_VAR), arg1); - CelExpr result = exprFactory.newIdentifier(ACCUMULATOR_VAR); + Operator.LOGICAL_AND.getFunction(), + exprFactory.newIdentifier(exprFactory.getAccumulatorVarName()), + arg1); + CelExpr result = exprFactory.newIdentifier(exprFactory.getAccumulatorVarName()); return Optional.of( exprFactory.fold( - arg0.ident().name(), target, ACCUMULATOR_VAR, accuInit, condition, step, result)); + arg0.ident().name(), + target, + exprFactory.getAccumulatorVarName(), + accuInit, + condition, + step, + result)); } // CelMacroExpander implementation for CEL's exists() macro. @@ -139,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); @@ -149,14 +165,23 @@ private static Optional expandExistsMacro( exprFactory.newGlobalCall( Operator.NOT_STRICTLY_FALSE.getFunction(), exprFactory.newGlobalCall( - Operator.LOGICAL_NOT.getFunction(), exprFactory.newIdentifier(ACCUMULATOR_VAR))); + Operator.LOGICAL_NOT.getFunction(), + exprFactory.newIdentifier(exprFactory.getAccumulatorVarName()))); CelExpr step = exprFactory.newGlobalCall( - Operator.LOGICAL_OR.getFunction(), exprFactory.newIdentifier(ACCUMULATOR_VAR), arg1); - CelExpr result = exprFactory.newIdentifier(ACCUMULATOR_VAR); + Operator.LOGICAL_OR.getFunction(), + exprFactory.newIdentifier(exprFactory.getAccumulatorVarName()), + arg1); + CelExpr result = exprFactory.newIdentifier(exprFactory.getAccumulatorVarName()); return Optional.of( exprFactory.fold( - arg0.ident().name(), target, ACCUMULATOR_VAR, accuInit, condition, step, result)); + arg0.ident().name(), + target, + exprFactory.getAccumulatorVarName(), + accuInit, + condition, + step, + result)); } // CelMacroExpander implementation for CEL's exists_one() macro. @@ -165,28 +190,36 @@ 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 zeroExpr = exprFactory.newIntLiteral(0); - CelExpr oneExpr = exprFactory.newIntLiteral(1); - CelExpr accuInit = zeroExpr; + CelExpr accuInit = exprFactory.newIntLiteral(0); CelExpr condition = exprFactory.newBoolLiteral(true); CelExpr step = exprFactory.newGlobalCall( Operator.CONDITIONAL.getFunction(), arg1, exprFactory.newGlobalCall( - Operator.ADD.getFunction(), exprFactory.newIdentifier(ACCUMULATOR_VAR), oneExpr), - exprFactory.newIdentifier(ACCUMULATOR_VAR)); + Operator.ADD.getFunction(), + exprFactory.newIdentifier(exprFactory.getAccumulatorVarName()), + exprFactory.newIntLiteral(1)), + exprFactory.newIdentifier(exprFactory.getAccumulatorVarName())); CelExpr result = exprFactory.newGlobalCall( - Operator.EQUALS.getFunction(), exprFactory.newIdentifier(ACCUMULATOR_VAR), oneExpr); + Operator.EQUALS.getFunction(), + exprFactory.newIdentifier(exprFactory.getAccumulatorVarName()), + exprFactory.newIntLiteral(1)); return Optional.of( exprFactory.fold( - arg0.ident().name(), target, ACCUMULATOR_VAR, accuInit, condition, step, result)); + arg0.ident().name(), + target, + exprFactory.getAccumulatorVarName(), + accuInit, + condition, + step, + result)); } // CelMacroExpander implementation for CEL's map() macro. @@ -195,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; @@ -216,7 +246,7 @@ private static Optional expandMapMacro( CelExpr step = exprFactory.newGlobalCall( Operator.ADD.getFunction(), - exprFactory.newIdentifier(ACCUMULATOR_VAR), + exprFactory.newIdentifier(exprFactory.getAccumulatorVarName()), exprFactory.newList(arg1)); if (arg2 != null) { step = @@ -224,17 +254,17 @@ private static Optional expandMapMacro( Operator.CONDITIONAL.getFunction(), arg2, step, - exprFactory.newIdentifier(ACCUMULATOR_VAR)); + exprFactory.newIdentifier(exprFactory.getAccumulatorVarName())); } return Optional.of( exprFactory.fold( arg0.ident().name(), target, - ACCUMULATOR_VAR, + exprFactory.getAccumulatorVarName(), accuInit, condition, step, - exprFactory.newIdentifier(ACCUMULATOR_VAR))); + exprFactory.newIdentifier(exprFactory.getAccumulatorVarName()))); } // CelMacroExpander implementation for CEL's filter() macro. @@ -243,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(); @@ -253,23 +283,41 @@ private static Optional expandFilterMacro( CelExpr step = exprFactory.newGlobalCall( Operator.ADD.getFunction(), - exprFactory.newIdentifier(ACCUMULATOR_VAR), + exprFactory.newIdentifier(exprFactory.getAccumulatorVarName()), exprFactory.newList(arg0)); step = exprFactory.newGlobalCall( Operator.CONDITIONAL.getFunction(), arg1, step, - exprFactory.newIdentifier(ACCUMULATOR_VAR)); + exprFactory.newIdentifier(exprFactory.getAccumulatorVarName())); return Optional.of( exprFactory.fold( arg0.ident().name(), target, - ACCUMULATOR_VAR, + exprFactory.getAccumulatorVarName(), accuInit, condition, step, - exprFactory.newIdentifier(ACCUMULATOR_VAR))); + 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) { @@ -277,4 +325,14 @@ private static CelExpr reportArgumentError(CelMacroExprFactory exprFactory, CelE 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/CelUnparserVisitor.java b/parser/src/main/java/dev/cel/parser/CelUnparserVisitor.java index 6e8d2350a..61c8b10e9 100644 --- a/parser/src/main/java/dev/cel/parser/CelUnparserVisitor.java +++ b/parser/src/main/java/dev/cel/parser/CelUnparserVisitor.java @@ -13,20 +13,27 @@ // limitations under the License. package dev.cel.parser; -import com.google.protobuf.ByteString; +import static dev.cel.common.Operator.LOGICAL_AND; +import static dev.cel.common.Operator.LOGICAL_OR; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableSet; +import com.google.re2j.Pattern; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelSource; +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.ast.CelExpr.CelComprehension; -import dev.cel.common.ast.CelExpr.CelCreateList; -import dev.cel.common.ast.CelExpr.CelCreateMap; -import dev.cel.common.ast.CelExpr.CelCreateStruct; import dev.cel.common.ast.CelExpr.CelIdent; +import dev.cel.common.ast.CelExpr.CelList; +import dev.cel.common.ast.CelExpr.CelMap; import dev.cel.common.ast.CelExpr.CelSelect; +import dev.cel.common.ast.CelExpr.CelStruct; import dev.cel.common.ast.CelExpr.ExprKind.Kind; import dev.cel.common.ast.CelExprVisitor; +import dev.cel.common.values.CelByteString; import java.util.HashSet; import java.util.Optional; @@ -43,6 +50,10 @@ public class CelUnparserVisitor extends CelExprVisitor { protected static final String RIGHT_BRACE = "}"; protected static final String COLON = ":"; protected static final String QUESTION_MARK = "?"; + protected static final String BACKTICK = "`"; + private static final Pattern IDENTIFIER_SEGMENT_PATTERN = + Pattern.compile("[a-zA-Z_][a-zA-Z0-9_]*"); + private static final ImmutableSet RESTRICTED_FIELD_NAMES = ImmutableSet.of("in"); protected final CelAbstractSyntaxTree ast; protected final CelSource sourceInfo; @@ -60,6 +71,25 @@ public String unparse() { return stringBuilder.toString(); } + /** + * Unparses a specific {@link CelExpr} node within the AST. + * + *

This method exists to allow unparsing of an arbitrary node within the stored AST in this + * visitor. + */ + public String unparse(CelExpr expr) { + visit(expr); + return stringBuilder.toString(); + } + + private static String maybeQuoteField(String field) { + if (RESTRICTED_FIELD_NAMES.contains(field) + || !IDENTIFIER_SEGMENT_PATTERN.matcher(field).matches()) { + return BACKTICK + field + BACKTICK; + } + return field; + } + @Override public void visit(CelExpr expr) { if (sourceInfo.getMacroCalls().containsKey(expr.id())) { @@ -163,35 +193,35 @@ protected void visit(CelExpr expr, CelCall call) { } @Override - protected void visit(CelExpr expr, CelCreateList createList) { + protected void visit(CelExpr expr, CelList list) { stringBuilder.append(LEFT_BRACKET); - HashSet optionalIndices = new HashSet<>(createList.optionalIndices()); - for (int i = 0; i < createList.elements().size(); i++) { + HashSet optionalIndices = new HashSet<>(list.optionalIndices()); + for (int i = 0; i < list.elements().size(); i++) { if (i > 0) { stringBuilder.append(COMMA).append(SPACE); } if (optionalIndices.contains(i)) { stringBuilder.append(QUESTION_MARK); } - visit(createList.elements().get(i)); + visit(list.elements().get(i)); } stringBuilder.append(RIGHT_BRACKET); } @Override - protected void visit(CelExpr expr, CelCreateStruct createStruct) { - stringBuilder.append(createStruct.messageName()); + protected void visit(CelExpr expr, CelStruct struct) { + stringBuilder.append(struct.messageName()); stringBuilder.append(LEFT_BRACE); - for (int i = 0; i < createStruct.entries().size(); i++) { + for (int i = 0; i < struct.entries().size(); i++) { if (i > 0) { stringBuilder.append(COMMA).append(SPACE); } - CelCreateStruct.Entry e = createStruct.entries().get(i); + CelStruct.Entry e = struct.entries().get(i); if (e.optionalEntry()) { stringBuilder.append(QUESTION_MARK); } - stringBuilder.append(e.fieldKey()); + stringBuilder.append(maybeQuoteField(e.fieldKey())); stringBuilder.append(COLON).append(SPACE); visit(e.value()); } @@ -199,14 +229,14 @@ protected void visit(CelExpr expr, CelCreateStruct createStruct) { } @Override - protected void visit(CelExpr expr, CelCreateMap createMap) { + protected void visit(CelExpr expr, CelMap map) { stringBuilder.append(LEFT_BRACE); - for (int i = 0; i < createMap.entries().size(); i++) { + for (int i = 0; i < map.entries().size(); i++) { if (i > 0) { stringBuilder.append(COMMA).append(SPACE); } - CelCreateMap.Entry e = createMap.entries().get(i); + CelMap.Entry e = map.entries().get(i); if (e.optionalEntry()) { stringBuilder.append(QUESTION_MARK); } @@ -248,7 +278,7 @@ private void visitBinary(CelCall expr, String op) { // add parens if the current operator is lower precedence than the rhs expr // operator, or the same precedence and the operator is left recursive. boolean rhsParen = isComplexOperatorWithRespectTo(rhs, fun); - if (!rhsParen && Operator.isOperatorLeftRecursive(fun)) { + if (!rhsParen && isOperatorLeftRecursive(fun)) { rhsParen = isOperatorSamePrecedence(fun, rhs); } @@ -263,7 +293,7 @@ private void visitSelect(CelExpr operand, boolean testOnly, String op, String fi } boolean nested = !testOnly && isBinaryOrTernaryOperator(operand); visitMaybeNested(operand, nested); - stringBuilder.append(op).append(field); + stringBuilder.append(op).append(maybeQuoteField(field)); if (testOnly) { stringBuilder.append(RIGHT_PAREN); } @@ -305,7 +335,7 @@ private void visitIndex(CelCall expr, String op) { stringBuilder.append(RIGHT_BRACKET); } - private void visitMaybeNested(CelExpr expr, boolean nested) { + protected void visitMaybeNested(CelExpr expr, boolean nested) { if (nested) { stringBuilder.append(LEFT_PAREN); } @@ -315,7 +345,7 @@ private void visitMaybeNested(CelExpr expr, boolean nested) { } } - private boolean isBinaryOrTernaryOperator(CelExpr expr) { + protected boolean isBinaryOrTernaryOperator(CelExpr expr) { if (!isComplexOperator(expr)) { return false; } @@ -341,16 +371,29 @@ private boolean isComplexOperatorWithRespectTo(CelExpr expr, String op) { return false; } // Otherwise, return whether the given op has lower precedence than expr - return Operator.isOperatorLowerPrecedence(op, expr); + return isOperatorLowerPrecedence(op, expr); } // bytesToOctets converts byte sequences to a string using a three digit octal encoded value // per byte. - private String bytesToOctets(ByteString bytes) { + private String bytesToOctets(CelByteString bytes) { StringBuilder sb = new StringBuilder(); for (byte b : bytes.toByteArray()) { sb.append(String.format("\\%03o", b)); } return sb.toString(); } + + @VisibleForTesting + static boolean isOperatorLeftRecursive(String op) { + return !op.equals(LOGICAL_AND.getFunction()) && !op.equals(LOGICAL_OR.getFunction()); + } + + @VisibleForTesting + static boolean isOperatorLowerPrecedence(String op, CelExpr expr) { + if (!expr.exprKind().getKind().equals(CelExpr.ExprKind.Kind.CALL)) { + return false; + } + return Operator.lookupPrecedence(op) < Operator.lookupPrecedence(expr.call().function()); + } } diff --git a/parser/src/main/java/dev/cel/parser/Parser.java b/parser/src/main/java/dev/cel/parser/Parser.java index abc5eb21f..af860e936 100644 --- a/parser/src/main/java/dev/cel/parser/Parser.java +++ b/parser/src/main/java/dev/cel/parser/Parser.java @@ -33,10 +33,13 @@ 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.IdentOrGlobalCallContext; +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; @@ -52,6 +55,7 @@ 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; @@ -66,6 +70,7 @@ 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; @@ -125,6 +130,8 @@ final class Parser extends CELBaseVisitor { "var", "void", "while"); + private static final String ACCUMULATOR_NAME = "__result__"; + private static final String HIDDEN_ACCUMULATOR_NAME = "@result"; static CelValidationResult parse(CelParserImpl parser, CelSource source, CelOptions options) { if (source.getContent().size() > options.maxExpressionCodePointSize()) { @@ -142,7 +149,11 @@ static CelValidationResult parse(CelParserImpl parser, CelSource source, CelOpti CELParser antlrParser = new CELParser(new CommonTokenStream(antlrLexer)); CelSource.Builder sourceInfo = source.toBuilder(); sourceInfo.setDescription(source.getDescription()); - ExprFactory exprFactory = new ExprFactory(antlrParser, sourceInfo); + 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(); @@ -432,7 +443,7 @@ public CelExpr visitSelect(SelectContext context) { if (context.id == null) { return exprFactory.newExprBuilder(context).build(); } - String id = context.id.getText(); + String id = normalizeEscapedIdent(context.id); if (context.opt != null && context.opt.getText().equals("?")) { if (!options.enableOptionalSyntax()) { @@ -524,12 +535,12 @@ public CelExpr visitCreateMessage(CreateMessageContext context) { } CelExpr.Builder exprBuilder = exprFactory.newExprBuilder(context.op); - CelExpr.CelCreateStruct.Builder structExpr = visitStructFields(context.entries); - return exprBuilder.setCreateStruct(structExpr.setMessageName(messageName).build()).build(); + CelExpr.CelStruct.Builder structExpr = visitStructFields(context.entries); + return exprBuilder.setStruct(structExpr.setMessageName(messageName).build()).build(); } @Override - public CelExpr visitIdentOrGlobalCall(IdentOrGlobalCallContext context) { + public CelExpr visitIdent(IdentContext context) { checkNotNull(context); if (context.id == null) { return exprFactory.newExprBuilder(context).build(); @@ -541,11 +552,25 @@ public CelExpr visitIdentOrGlobalCall(IdentOrGlobalCallContext context) { if (context.leadingDot != null) { id = "." + id; } - if (context.op == null) { - return exprFactory - .newExprBuilder(context.id) - .setIdent(CelExpr.CelIdent.newBuilder().setName(id).build()) - .build(); + + 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); @@ -564,13 +589,13 @@ public CelExpr visitNested(NestedContext context) { public CelExpr visitCreateList(CreateListContext context) { checkNotNull(context); CelExpr.Builder exprBuilder = exprFactory.newExprBuilder(context.op); - CelExpr.CelCreateList createListExpr = visitListInitElements(context.listInit()); + CelExpr.CelList createListExpr = visitListInitElements(context.listInit()); - return exprBuilder.setCreateList(createListExpr).build(); + return exprBuilder.setList(createListExpr).build(); } - private CelExpr.CelCreateList visitListInitElements(ListInitContext context) { - CelExpr.CelCreateList.Builder listExpr = CelExpr.CelCreateList.newBuilder(); + private CelExpr.CelList visitListInitElements(ListInitContext context) { + CelExpr.CelList.Builder listExpr = CelExpr.CelList.newBuilder(); if (context == null) { return listExpr.build(); } @@ -595,9 +620,8 @@ private CelExpr.CelCreateList visitListInitElements(ListInitContext context) { public CelExpr visitCreateMap(CreateMapContext context) { checkNotNull(context); CelExpr.Builder exprBuilder = exprFactory.newExprBuilder(context.op); - CelExpr.CelCreateMap.Builder createMapExpr = visitMapEntries(context.entries); - // CelExpr.CelCreateStruct.Builder structExpr = visitMapEntries(context.entries); - return exprBuilder.setCreateMap(createMapExpr.build()).build(); + CelExpr.CelMap.Builder createMapExpr = visitMapEntries(context.entries); + return exprBuilder.setMap(createMapExpr.build()).build(); } private CelExpr buildMacroCallArgs(CelExpr expr) { @@ -614,6 +638,7 @@ private CelExpr buildMacroCallArgs(CelExpr expr) { // 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; @@ -661,18 +686,37 @@ private Optional visitMacro( CelExpr.newBuilder().setCall(callExpr.build()).build()); } + sourceInfo.removePositions(expr.id()); return expandedMacro; } - private CelExpr.CelCreateStruct.Builder visitStructFields(FieldInitializerListContext context) { + 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.CelCreateStruct.newBuilder(); + return CelExpr.CelStruct.newBuilder(); } int entryCount = min(context.cols.size(), context.fields.size(), context.values.size()); - CelExpr.CelCreateStruct.Builder structExpr = CelExpr.CelCreateStruct.newBuilder(); + CelExpr.CelStruct.Builder structExpr = CelExpr.CelStruct.newBuilder(); for (int index = 0; index < entryCount; index++) { OptFieldContext fieldContext = context.fields.get(index); boolean isOptionalEntry = false; @@ -685,13 +729,13 @@ private CelExpr.CelCreateStruct.Builder visitStructFields(FieldInitializerListCo } // The field may be empty due to a prior error. - if (fieldContext.IDENTIFIER() == null) { - return CelExpr.CelCreateStruct.newBuilder(); + if (fieldContext.escapeIdent() == null) { + return CelExpr.CelStruct.newBuilder(); } - String fieldName = fieldContext.IDENTIFIER().getText(); + String fieldName = normalizeEscapedIdent(fieldContext.escapeIdent()); - CelExpr.CelCreateStruct.Entry.Builder exprBuilder = - CelExpr.CelCreateStruct.Entry.newBuilder() + CelExpr.CelStruct.Entry.Builder exprBuilder = + CelExpr.CelStruct.Entry.newBuilder() .setId(exprFactory.newExprId(exprFactory.getPosition(context.cols.get(index)))); structExpr.addEntries( exprBuilder @@ -703,12 +747,12 @@ private CelExpr.CelCreateStruct.Builder visitStructFields(FieldInitializerListCo return structExpr; } - private CelExpr.CelCreateMap.Builder visitMapEntries(MapInitializerListContext context) { + private CelExpr.CelMap.Builder visitMapEntries(MapInitializerListContext context) { if (context == null || context.cols == null || context.keys == null || context.values == null) { - return CelExpr.CelCreateMap.newBuilder(); + return CelExpr.CelMap.newBuilder(); } int entryCount = min(context.cols.size(), context.keys.size(), context.values.size()); - CelExpr.CelCreateMap.Builder mapExpr = CelExpr.CelCreateMap.newBuilder(); + CelExpr.CelMap.Builder mapExpr = CelExpr.CelMap.newBuilder(); for (int index = 0; index < entryCount; index++) { OptExprContext keyContext = context.keys.get(index); boolean isOptionalEntry = false; @@ -719,8 +763,8 @@ private CelExpr.CelCreateMap.Builder visitMapEntries(MapInitializerListContext c isOptionalEntry = true; } } - CelExpr.CelCreateMap.Entry.Builder exprBuilder = - CelExpr.CelCreateMap.Entry.newBuilder() + CelExpr.CelMap.Entry.Builder exprBuilder = + CelExpr.CelMap.Entry.newBuilder() .setId(exprFactory.newExprId(exprFactory.getPosition(context.cols.get(index)))); mapExpr.addEntries( exprBuilder @@ -865,7 +909,7 @@ private CelExpr receiverCallOrMacro(MemberCallContext context, String id, CelExp return macroOrCall(context.args, context.open, id, Optional.of(member), true); } - private CelExpr globalCallOrMacro(IdentOrGlobalCallContext context, String id) { + private CelExpr globalCallOrMacro(GlobalCallContext context, String id) { return macroOrCall(context.args, context.op, id, Optional.empty(), false); } @@ -896,6 +940,7 @@ private CelExpr macroOrCall( 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; @@ -1031,12 +1076,17 @@ private static final class ExprFactory extends CelMacroExprFactory { 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) { + 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. @@ -1060,6 +1110,11 @@ public CelExpr reportError(CelIssue error) { return ERROR; } + @Override + public String getAccumulatorVarName() { + return accumulatorVarName; + } + // Internal methods used by the parser but not part of the public API. @FormatMethod @CanIgnoreReturnValue @@ -1110,6 +1165,11 @@ private long nextExprId(int 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. 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 5eee82579..d4365e34d 100644 --- a/parser/src/main/java/dev/cel/parser/gen/BUILD.bazel +++ b/parser/src/main/java/dev/cel/parser/gen/BUILD.bazel @@ -4,11 +4,10 @@ to avoid a path conflict with parser/src/main/java/dev/cel/parser/CelParser.java that causes build failures on filesystems with case-insensitive paths (e.g. macOS). """ +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/main/java/dev/cel/parser/gen/CEL.g4 b/parser/src/main/java/dev/cel/parser/gen/CEL.g4 index fbbd1b434..65f4b830d 100644 --- a/parser/src/main/java/dev/cel/parser/gen/CEL.g4 +++ b/parser/src/main/java/dev/cel/parser/gen/CEL.g4 @@ -11,6 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. + grammar CEL; // Grammar Rules @@ -44,26 +45,27 @@ calc ; unary - : member # MemberExpr - | (ops+='!')+ member # LogicalNot - | (ops+='-')+ member # Negate + : member # MemberExpr + | (ops+='!')+ member # LogicalNot + | (ops+='-')+ member # Negate ; member - : primary # PrimaryExpr - | member op='.' (opt='?')? id=IDENTIFIER # Select - | member op='.' id=IDENTIFIER open='(' args=exprList? ')' # MemberCall - | member op='[' (opt='?')? index=expr ']' # Index + : primary # PrimaryExpr + | member op='.' (opt='?')? id=escapeIdent # Select + | member op='.' id=IDENTIFIER open='(' args=exprList? ')' # MemberCall + | member op='[' (opt='?')? index=expr ']' # Index ; primary - : leadingDot='.'? id=IDENTIFIER (op='(' args=exprList? ')')? # IdentOrGlobalCall - | '(' e=expr ')' # Nested - | op='[' elems=listInit? ','? ']' # CreateList - | op='{' entries=mapInitializerList? ','? '}' # CreateMap + : leadingDot='.'? id=IDENTIFIER # Ident + | leadingDot='.'? id=IDENTIFIER (op='(' args=exprList? ')') # GlobalCall + | '(' e=expr ')' # Nested + | op='[' elems=listInit? ','? ']' # CreateList + | op='{' entries=mapInitializerList? ','? '}' # CreateMap | leadingDot='.'? ids+=IDENTIFIER (ops+='.' ids+=IDENTIFIER)* - op='{' entries=fieldInitializerList? ','? '}' # CreateMessage - | literal # ConstantLiteral + op='{' entries=fieldInitializerList? ','? '}' # CreateMessage + | literal # ConstantLiteral ; exprList @@ -79,13 +81,18 @@ fieldInitializerList ; optField - : (opt='?')? IDENTIFIER + : (opt='?')? escapeIdent ; mapInitializerList : keys+=optExpr cols+=':' values+=expr (',' keys+=optExpr cols+=':' values+=expr)* ; +escapeIdent + : id=IDENTIFIER # SimpleIdentifier + | id=ESC_IDENTIFIER # EscapedIdentifier + ; + optExpr : (opt='?')? e=expr ; @@ -197,3 +204,4 @@ STRING BYTES : ('b' | 'B') STRING; IDENTIFIER : (LETTER | '_') ( LETTER | DIGIT | '_')*; +ESC_IDENTIFIER : '`' (LETTER | DIGIT | '_' | '.' | '-' | '/' | ' ')+ '`'; diff --git a/parser/src/test/java/dev/cel/parser/BUILD.bazel b/parser/src/test/java/dev/cel/parser/BUILD.bazel index 270c412c5..1ade0181d 100644 --- a/parser/src/test/java/dev/cel/parser/BUILD.bazel +++ b/parser/src/test/java/dev/cel/parser/BUILD.bazel @@ -1,8 +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", @@ -12,20 +15,26 @@ java_library( deps = [ "//:auto_value", "//:java_truth", - "//common", + "//common:cel_ast", + "//common:cel_source", "//common:compiler_common", + "//common:operator", "//common:options", + "//common:proto_ast", + "//common:source_location", "//common/ast", "//common/internal", + "//common/values:cel_byte_string", "//extensions:optional_library", "//parser", "//parser:macro", - "//parser:operator", "//parser:parser_builder", + "//parser:parser_factory", "//parser:unparser", + "//parser:unparser_visitor", "//testing:adorner", "//testing:baseline_test_case", - "@cel_spec//proto/cel/expr:expr_java_proto", + "@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", diff --git a/parser/src/test/java/dev/cel/parser/CelMacroExprFactoryTest.java b/parser/src/test/java/dev/cel/parser/CelMacroExprFactoryTest.java index 0a66f26da..72abf81a4 100644 --- a/parser/src/test/java/dev/cel/parser/CelMacroExprFactoryTest.java +++ b/parser/src/test/java/dev/cel/parser/CelMacroExprFactoryTest.java @@ -17,15 +17,15 @@ import static com.google.common.truth.Truth.assertThat; import com.google.common.primitives.UnsignedLong; -import com.google.protobuf.ByteString; import dev.cel.common.CelIssue; import dev.cel.common.CelSourceLocation; import dev.cel.common.ast.CelConstant; import dev.cel.common.ast.CelExpr; -import dev.cel.common.ast.CelExpr.CelCreateMap; -import dev.cel.common.ast.CelExpr.CelCreateStruct.Entry; +import dev.cel.common.ast.CelExpr.CelMap; +import dev.cel.common.ast.CelExpr.CelStruct.Entry; import dev.cel.common.ast.CelExpr.ExprKind.Kind; import dev.cel.common.internal.Constants; +import dev.cel.common.values.CelByteString; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -41,6 +41,11 @@ private TestCelExprFactory() { exprId = 0L; } + @Override + public long copyExprId(long unused) { + return nextExprId(); + } + @Override public long nextExprId() { return ++exprId; @@ -56,6 +61,11 @@ public CelExpr reportError(CelIssue issue) { return CelExpr.newBuilder().setId(nextExprId()).setConstant(Constants.ERROR).build(); } + @Override + public String getAccumulatorVarName() { + return "__result__"; + } + @Override protected CelSourceLocation getSourceLocation(long exprId) { return CelSourceLocation.NONE; @@ -93,7 +103,7 @@ public void newBytesLiteral_returnsBytesConstant() { assertThat(expr.id()).isEqualTo(1L); assertThat(expr.exprKind().getKind()).isEqualTo(Kind.CONSTANT); assertThat(expr.constant().getKind()).isEqualTo(CelConstant.Kind.BYTES_VALUE); - assertThat(expr.constant().bytesValue()).isEqualTo(ByteString.copyFromUtf8("foo")); + assertThat(expr.constant().bytesValue()).isEqualTo(CelByteString.copyFromUtf8("foo")); } @Test @@ -150,21 +160,21 @@ public void newList_returnsList() { CelExpr element = exprFactory.newStringLiteral("foo"); CelExpr expr = exprFactory.newList(element); assertThat(expr.id()).isEqualTo(2L); - assertThat(expr.exprKind().getKind()).isEqualTo(Kind.CREATE_LIST); - assertThat(expr.createList().elements()).hasSize(1); - assertThat(expr.createList().elements()).containsExactly(element); + assertThat(expr.exprKind().getKind()).isEqualTo(Kind.LIST); + assertThat(expr.list().elements()).hasSize(1); + assertThat(expr.list().elements()).containsExactly(element); } @Test public void newMap_returnsMap() { TestCelExprFactory exprFactory = new TestCelExprFactory(); - CelCreateMap.Entry entry = + CelMap.Entry entry = exprFactory.newMapEntry( exprFactory.newStringLiteral("foo"), exprFactory.newStringLiteral("bar")); CelExpr expr = exprFactory.newMap(entry); assertThat(expr.id()).isEqualTo(4L); - assertThat(expr.exprKind().getKind()).isEqualTo(Kind.CREATE_MAP); - assertThat(expr.createMap().entries()).containsExactly(entry); + assertThat(expr.exprKind().getKind()).isEqualTo(Kind.MAP); + assertThat(expr.map().entries()).containsExactly(entry); } @Test @@ -172,7 +182,7 @@ public void newMapEntry_returnsMapEntry() { TestCelExprFactory exprFactory = new TestCelExprFactory(); CelExpr key = exprFactory.newStringLiteral("foo"); CelExpr value = exprFactory.newStringLiteral("bar"); - CelCreateMap.Entry entry = exprFactory.newMapEntry(key, value); + CelMap.Entry entry = exprFactory.newMapEntry(key, value); assertThat(entry.id()).isEqualTo(3L); assertThat(entry.value()).isEqualTo(value); } @@ -183,9 +193,9 @@ public void newMessage_returnsMessage() { Entry field = exprFactory.newMessageField("foo", exprFactory.newStringLiteral("bar")); CelExpr expr = exprFactory.newMessage("google.example.Baz", field); assertThat(expr.id()).isEqualTo(3L); - assertThat(expr.exprKind().getKind()).isEqualTo(Kind.CREATE_STRUCT); - assertThat(expr.createStruct().messageName()).isEqualTo("google.example.Baz"); - assertThat(expr.createStruct().entries()).containsExactly(field); + assertThat(expr.exprKind().getKind()).isEqualTo(Kind.STRUCT); + assertThat(expr.struct().messageName()).isEqualTo("google.example.Baz"); + assertThat(expr.struct().entries()).containsExactly(field); } @Test @@ -209,6 +219,7 @@ public void fold_returnsComprehension() { assertThat(expr.id()).isEqualTo(6L); assertThat(expr.exprKind().getKind()).isEqualTo(Kind.COMPREHENSION); assertThat(expr.comprehension().iterVar()).isEqualTo("i"); + assertThat(expr.comprehension().iterVar2()).isEmpty(); assertThat(expr.comprehension().iterRange()).isEqualTo(iterRange); assertThat(expr.comprehension().accuVar()).isEqualTo("a"); assertThat(expr.comprehension().accuInit()).isEqualTo(accuInit); @@ -218,347 +229,25 @@ public void fold_returnsComprehension() { } @Test - public void fold_overloadsAreEquivalent() { + public void fold_returnsTwoVariableComprehension() { TestCelExprFactory exprFactory = new TestCelExprFactory(); - CelExpr want = - exprFactory.fold( - "i", - exprFactory.newList(), - "a", - exprFactory.newIntLiteral(0L), - exprFactory.newBoolLiteral(true), - exprFactory.newIntLiteral(1L), - exprFactory.newIdentifier("foo")); - exprFactory.reset(); - assertThat( - exprFactory.fold( - "i", - exprFactory.newList().toBuilder(), - "a", - exprFactory.newIntLiteral(0L), - exprFactory.newBoolLiteral(true), - exprFactory.newIntLiteral(1L), - exprFactory.newIdentifier("foo"))) - .isEqualTo(want); - exprFactory.reset(); - assertThat( - exprFactory.fold( - "i", - exprFactory.newList(), - "a", - exprFactory.newIntLiteral(0L).toBuilder(), - exprFactory.newBoolLiteral(true), - exprFactory.newIntLiteral(1L), - exprFactory.newIdentifier("foo"))) - .isEqualTo(want); - exprFactory.reset(); - assertThat( - exprFactory.fold( - "i", - exprFactory.newList(), - "a", - exprFactory.newIntLiteral(0L), - exprFactory.newBoolLiteral(true).toBuilder(), - exprFactory.newIntLiteral(1L), - exprFactory.newIdentifier("foo"))) - .isEqualTo(want); - exprFactory.reset(); - assertThat( - exprFactory.fold( - "i", - exprFactory.newList(), - "a", - exprFactory.newIntLiteral(0L), - exprFactory.newBoolLiteral(true), - exprFactory.newIntLiteral(1L).toBuilder(), - exprFactory.newIdentifier("foo"))) - .isEqualTo(want); - exprFactory.reset(); - assertThat( - exprFactory.fold( - "i", - exprFactory.newList(), - "a", - exprFactory.newIntLiteral(0L), - exprFactory.newBoolLiteral(true), - exprFactory.newIntLiteral(1L), - exprFactory.newIdentifier("foo").toBuilder())) - .isEqualTo(want); - exprFactory.reset(); - assertThat( - exprFactory.fold( - "i", - exprFactory.newList().toBuilder(), - "a", - exprFactory.newIntLiteral(0L).toBuilder(), - exprFactory.newBoolLiteral(true), - exprFactory.newIntLiteral(1L), - exprFactory.newIdentifier("foo"))) - .isEqualTo(want); - exprFactory.reset(); - assertThat( - exprFactory.fold( - "i", - exprFactory.newList().toBuilder(), - "a", - exprFactory.newIntLiteral(0L), - exprFactory.newBoolLiteral(true).toBuilder(), - exprFactory.newIntLiteral(1L), - exprFactory.newIdentifier("foo"))) - .isEqualTo(want); - exprFactory.reset(); - assertThat( - exprFactory.fold( - "i", - exprFactory.newList().toBuilder(), - "a", - exprFactory.newIntLiteral(0L), - exprFactory.newBoolLiteral(true), - exprFactory.newIntLiteral(1L).toBuilder(), - exprFactory.newIdentifier("foo"))) - .isEqualTo(want); - exprFactory.reset(); - assertThat( - exprFactory.fold( - "i", - exprFactory.newList().toBuilder(), - "a", - exprFactory.newIntLiteral(0L), - exprFactory.newBoolLiteral(true), - exprFactory.newIntLiteral(1L), - exprFactory.newIdentifier("foo").toBuilder())) - .isEqualTo(want); - exprFactory.reset(); - assertThat( - exprFactory.fold( - "i", - exprFactory.newList().toBuilder(), - "a", - exprFactory.newIntLiteral(0L).toBuilder(), - exprFactory.newBoolLiteral(true).toBuilder(), - exprFactory.newIntLiteral(1L), - exprFactory.newIdentifier("foo"))) - .isEqualTo(want); - exprFactory.reset(); - assertThat( - exprFactory.fold( - "i", - exprFactory.newList().toBuilder(), - "a", - exprFactory.newIntLiteral(0L).toBuilder(), - exprFactory.newBoolLiteral(true), - exprFactory.newIntLiteral(1L).toBuilder(), - exprFactory.newIdentifier("foo"))) - .isEqualTo(want); - exprFactory.reset(); - assertThat( - exprFactory.fold( - "i", - exprFactory.newList().toBuilder(), - "a", - exprFactory.newIntLiteral(0L).toBuilder(), - exprFactory.newBoolLiteral(true), - exprFactory.newIntLiteral(1L), - exprFactory.newIdentifier("foo").toBuilder())) - .isEqualTo(want); - exprFactory.reset(); - assertThat( - exprFactory.fold( - "i", - exprFactory.newList().toBuilder(), - "a", - exprFactory.newIntLiteral(0L).toBuilder(), - exprFactory.newBoolLiteral(true).toBuilder(), - exprFactory.newIntLiteral(1L).toBuilder(), - exprFactory.newIdentifier("foo"))) - .isEqualTo(want); - exprFactory.reset(); - assertThat( - exprFactory.fold( - "i", - exprFactory.newList().toBuilder(), - "a", - exprFactory.newIntLiteral(0L).toBuilder(), - exprFactory.newBoolLiteral(true).toBuilder(), - exprFactory.newIntLiteral(1L).toBuilder(), - exprFactory.newIdentifier("foo").toBuilder())) - .isEqualTo(want); - exprFactory.reset(); - assertThat( - exprFactory.fold( - "i", - exprFactory.newList().toBuilder(), - "a", - exprFactory.newIntLiteral(0L).toBuilder(), - exprFactory.newBoolLiteral(true), - exprFactory.newIntLiteral(1L), - exprFactory.newIdentifier("foo"))) - .isEqualTo(want); - exprFactory.reset(); - assertThat( - exprFactory.fold( - "i", - exprFactory.newList().toBuilder(), - "a", - exprFactory.newIntLiteral(0L), - exprFactory.newBoolLiteral(true).toBuilder(), - exprFactory.newIntLiteral(1L), - exprFactory.newIdentifier("foo"))) - .isEqualTo(want); - exprFactory.reset(); - assertThat( - exprFactory.fold( - "i", - exprFactory.newList().toBuilder(), - "a", - exprFactory.newIntLiteral(0L), - exprFactory.newBoolLiteral(true), - exprFactory.newIntLiteral(1L).toBuilder(), - exprFactory.newIdentifier("foo"))) - .isEqualTo(want); - exprFactory.reset(); - assertThat( - exprFactory.fold( - "i", - exprFactory.newList().toBuilder(), - "a", - exprFactory.newIntLiteral(0L), - exprFactory.newBoolLiteral(true), - exprFactory.newIntLiteral(1L), - exprFactory.newIdentifier("foo").toBuilder())) - .isEqualTo(want); - exprFactory.reset(); - assertThat( - exprFactory.fold( - "i", - exprFactory.newList().toBuilder(), - "a", - exprFactory.newIntLiteral(0L).toBuilder(), - exprFactory.newBoolLiteral(true).toBuilder(), - exprFactory.newIntLiteral(1L), - exprFactory.newIdentifier("foo"))) - .isEqualTo(want); - exprFactory.reset(); - assertThat( - exprFactory.fold( - "i", - exprFactory.newList().toBuilder(), - "a", - exprFactory.newIntLiteral(0L).toBuilder(), - exprFactory.newBoolLiteral(true), - exprFactory.newIntLiteral(1L).toBuilder(), - exprFactory.newIdentifier("foo"))) - .isEqualTo(want); - exprFactory.reset(); - assertThat( - exprFactory.fold( - "i", - exprFactory.newList().toBuilder(), - "a", - exprFactory.newIntLiteral(0L).toBuilder(), - exprFactory.newBoolLiteral(true), - exprFactory.newIntLiteral(1L), - exprFactory.newIdentifier("foo").toBuilder())) - .isEqualTo(want); - exprFactory.reset(); - assertThat( - exprFactory.fold( - "i", - exprFactory.newList().toBuilder(), - "a", - exprFactory.newIntLiteral(0L), - exprFactory.newBoolLiteral(true).toBuilder(), - exprFactory.newIntLiteral(1L).toBuilder(), - exprFactory.newIdentifier("foo"))) - .isEqualTo(want); - exprFactory.reset(); - assertThat( - exprFactory.fold( - "i", - exprFactory.newList().toBuilder(), - "a", - exprFactory.newIntLiteral(0L), - exprFactory.newBoolLiteral(true).toBuilder(), - exprFactory.newIntLiteral(1L).toBuilder(), - exprFactory.newIdentifier("foo").toBuilder())) - .isEqualTo(want); - exprFactory.reset(); - assertThat( - exprFactory.fold( - "i", - exprFactory.newList(), - "a", - exprFactory.newIntLiteral(0L).toBuilder(), - exprFactory.newBoolLiteral(true).toBuilder(), - exprFactory.newIntLiteral(1L), - exprFactory.newIdentifier("foo"))) - .isEqualTo(want); - exprFactory.reset(); - assertThat( - exprFactory.fold( - "i", - exprFactory.newList(), - "a", - exprFactory.newIntLiteral(0L).toBuilder(), - exprFactory.newBoolLiteral(true), - exprFactory.newIntLiteral(1L).toBuilder(), - exprFactory.newIdentifier("foo"))) - .isEqualTo(want); - exprFactory.reset(); - assertThat( - exprFactory.fold( - "i", - exprFactory.newList(), - "a", - exprFactory.newIntLiteral(0L).toBuilder(), - exprFactory.newBoolLiteral(true), - exprFactory.newIntLiteral(1L), - exprFactory.newIdentifier("foo").toBuilder())) - .isEqualTo(want); - exprFactory.reset(); - assertThat( - exprFactory.fold( - "i", - exprFactory.newList(), - "a", - exprFactory.newIntLiteral(0L), - exprFactory.newBoolLiteral(true).toBuilder(), - exprFactory.newIntLiteral(1L).toBuilder(), - exprFactory.newIdentifier("foo"))) - .isEqualTo(want); - exprFactory.reset(); - assertThat( - exprFactory.fold( - "i", - exprFactory.newList(), - "a", - exprFactory.newIntLiteral(0L), - exprFactory.newBoolLiteral(true).toBuilder(), - exprFactory.newIntLiteral(1L), - exprFactory.newIdentifier("foo").toBuilder())) - .isEqualTo(want); - exprFactory.reset(); - assertThat( - exprFactory.fold( - "i", - exprFactory.newList(), - "a", - exprFactory.newIntLiteral(0L), - exprFactory.newBoolLiteral(true), - exprFactory.newIntLiteral(1L).toBuilder(), - exprFactory.newIdentifier("foo").toBuilder())) - .isEqualTo(want); - exprFactory.reset(); - assertThat( - exprFactory.fold( - "i", - exprFactory.newList().toBuilder(), - "a", - exprFactory.newIntLiteral(0L), - exprFactory.newBoolLiteral(true).toBuilder(), - exprFactory.newIntLiteral(1L), - exprFactory.newIdentifier("foo").toBuilder())) - .isEqualTo(want); + CelExpr iterRange = exprFactory.newList(); + CelExpr accuInit = exprFactory.newIntLiteral(0L); + CelExpr loopCondition = exprFactory.newBoolLiteral(true); + CelExpr loopStep = exprFactory.newIntLiteral(1L); + CelExpr result = exprFactory.newIdentifier("foo"); + CelExpr expr = + exprFactory.fold("i", "j", iterRange, "a", accuInit, loopCondition, loopStep, result); + assertThat(expr.id()).isEqualTo(6L); + assertThat(expr.exprKind().getKind()).isEqualTo(Kind.COMPREHENSION); + assertThat(expr.comprehension().iterVar()).isEqualTo("i"); + assertThat(expr.comprehension().iterVar2()).isEqualTo("j"); + assertThat(expr.comprehension().iterRange()).isEqualTo(iterRange); + assertThat(expr.comprehension().accuVar()).isEqualTo("a"); + assertThat(expr.comprehension().accuInit()).isEqualTo(accuInit); + assertThat(expr.comprehension().loopCondition()).isEqualTo(loopCondition); + assertThat(expr.comprehension().loopStep()).isEqualTo(loopStep); + assertThat(expr.comprehension().result()).isEqualTo(result); } @Test diff --git a/parser/src/test/java/dev/cel/parser/CelParserImplTest.java b/parser/src/test/java/dev/cel/parser/CelParserImplTest.java index f12fef292..1e7b44fab 100644 --- a/parser/src/test/java/dev/cel/parser/CelParserImplTest.java +++ b/parser/src/test/java/dev/cel/parser/CelParserImplTest.java @@ -17,6 +17,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; +import com.google.common.collect.ImmutableSet; import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; import com.google.testing.junit.testparameterinjector.TestParameters; @@ -260,10 +261,17 @@ public void parse_exprUnderMaxRecursionLimit_doesNotThrow( @TestParameters("{expression: 'A.all(a?b, c)'}") @TestParameters("{expression: 'A.exists(a?b, c)'}") @TestParameters("{expression: 'A.exists_one(a?b, c)'}") + @TestParameters("{expression: 'A.existsOne(a?b, c)'}") @TestParameters("{expression: 'A.filter(a?b, c)'}") public void parse_macroArgumentContainsSyntaxError_throws(String expression) { CelParser parser = - CelParserImpl.newBuilder().setStandardMacros(CelStandardMacro.STANDARD_MACROS).build(); + CelParserImpl.newBuilder() + .setStandardMacros( + ImmutableSet.builder() + .addAll(CelStandardMacro.STANDARD_MACROS) + .add(CelStandardMacro.EXISTS_ONE_NEW) + .build()) + .build(); CelValidationResult parseResult = parser.parse(expression); diff --git a/parser/src/test/java/dev/cel/parser/CelParserParameterizedTest.java b/parser/src/test/java/dev/cel/parser/CelParserParameterizedTest.java index 599a5478c..019cea520 100644 --- a/parser/src/test/java/dev/cel/parser/CelParserParameterizedTest.java +++ b/parser/src/test/java/dev/cel/parser/CelParserParameterizedTest.java @@ -26,12 +26,15 @@ import com.google.auto.value.AutoValue; import com.google.common.base.Ascii; import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableSet; import com.google.errorprone.annotations.Immutable; import com.google.protobuf.Descriptors.Descriptor; import com.google.protobuf.Descriptors.EnumDescriptor; import com.google.protobuf.Descriptors.FieldDescriptor; import com.google.protobuf.Descriptors.OneofDescriptor; +import com.google.protobuf.TextFormat; import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelOptions; import dev.cel.common.CelProtoAbstractSyntaxTree; import dev.cel.common.CelSource; @@ -55,7 +58,11 @@ public final class CelParserParameterizedTest extends BaselineTestCase { private static final CelParser PARSER = CelParserFactory.standardCelParserBuilder() - .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .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()), @@ -68,7 +75,21 @@ public final class CelParserParameterizedTest extends BaselineTestCase { .setId(1) .setConstant(CelConstant.ofValue(10L)) .build()))) - .setOptions(CelOptions.current().populateMacroCalls(true).build()) + .setOptions( + CelOptions.current() + .populateMacroCalls(true) + .enableHiddenAccumulatorVar(true) + .build()) + .build(); + + private static final CelParser PARSER_WITH_OLD_ACCU_VAR = + PARSER + .toParserBuilder() + .setOptions( + CelOptions.current() + .populateMacroCalls(true) + .enableHiddenAccumulatorVar(false) + .build()) .build(); @Test @@ -109,7 +130,7 @@ public void parser() { runTest(PARSER, "a"); runTest(PARSER, "a?b:c"); runTest(PARSER, "a || b"); - runTest(PARSER, "a || b || c || d || e || f "); + 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"); @@ -146,6 +167,7 @@ public void parser() { 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)"); @@ -190,6 +212,29 @@ public void parser() { .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}"); + } + + @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)"); } @Test @@ -200,9 +245,23 @@ public void parser_errors() { runTest(PARSER, "1 + $"); runTest(PARSER, "1.all(2, 3)"); runTest(PARSER, "1.exists(2, 3)"); + runTest(PARSER, "[].all(__result__, x)"); + runTest(PARSER, "[].exists(__result__, x)"); + runTest(PARSER, "[].exists_one(__result__, x)"); + runTest(PARSER, "[].map(__result__, x, x)"); + runTest(PARSER, "[].filter(__result__, x)"); + runTest(PARSER, "[].all(.x, x)"); + runTest(PARSER, "[].exists(.x, x)"); + runTest(PARSER, "[].exists_one(.x, x)"); + runTest(PARSER, "[].map(.x, x, x)"); + runTest(PARSER, "[].filter(.x, x)"); runTest(PARSER, "1 + +"); runTest(PARSER, "\"\\xFh\""); runTest(PARSER, "\"\\a\\b\\f\\n\\r\\t\\v\\'\\\"\\\\\\? Illegal escape \\>\""); + runTest(PARSER, "'\uD800'"); + runTest(PARSER, "'\uDFFF'"); + runTest(PARSER, "r\"\\\uD800\""); + runTest(PARSER, "as"); runTest(PARSER, "break"); runTest(PARSER, "const"); @@ -238,6 +297,7 @@ public void parser_errors() { runTest(PARSER, "TestAllTypes(){single_int32: 1, single_int64: 2}"); runTest(PARSER, "{"); runTest(PARSER, "t{>C}"); + runTest(PARSER, "has([(has(("); CelParser parserWithoutOptionalSupport = CelParserImpl.newBuilder() @@ -246,6 +306,28 @@ public void parser_errors() { 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(.`.`"); + } + + @Test + public void source_info() throws Exception { + runSourceInfoTest("[{}, {'field': true}].exists(i, has(i.field))"); } private void runTest(CelParser parser, String expression) { @@ -287,6 +369,15 @@ private void runTest(CelParser parser, String expression, boolean validateParseO 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)); + } + 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 diff --git a/parser/src/test/java/dev/cel/parser/CelStandardMacroTest.java b/parser/src/test/java/dev/cel/parser/CelStandardMacroTest.java index c06be2688..7ab96180c 100644 --- a/parser/src/test/java/dev/cel/parser/CelStandardMacroTest.java +++ b/parser/src/test/java/dev/cel/parser/CelStandardMacroTest.java @@ -17,6 +17,7 @@ import static com.google.common.truth.Truth.assertThat; import com.google.common.testing.EqualsTester; +import dev.cel.common.Operator; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -31,6 +32,8 @@ public void getFunction() { assertThat(CelStandardMacro.EXISTS.getFunction()).isEqualTo(Operator.EXISTS.getFunction()); assertThat(CelStandardMacro.EXISTS_ONE.getFunction()) .isEqualTo(Operator.EXISTS_ONE.getFunction()); + assertThat(CelStandardMacro.EXISTS_ONE_NEW.getFunction()) + .isEqualTo(Operator.EXISTS_ONE_NEW.getFunction()); assertThat(CelStandardMacro.FILTER.getFunction()).isEqualTo(Operator.FILTER.getFunction()); assertThat(CelStandardMacro.MAP.getFunction()).isEqualTo(Operator.MAP.getFunction()); assertThat(CelStandardMacro.MAP_FILTER.getFunction()).isEqualTo(Operator.MAP.getFunction()); @@ -89,6 +92,21 @@ public void testExistsOne() { .isEqualTo(CelStandardMacro.EXISTS_ONE.getDefinition().getKey().hashCode()); } + @Test + public void testExistsOneNew() { + assertThat(CelStandardMacro.EXISTS_ONE_NEW.getFunction()) + .isEqualTo(Operator.EXISTS_ONE_NEW.getFunction()); + assertThat(CelStandardMacro.EXISTS_ONE_NEW.getDefinition().getArgumentCount()).isEqualTo(2); + assertThat(CelStandardMacro.EXISTS_ONE_NEW.getDefinition().isReceiverStyle()).isTrue(); + assertThat(CelStandardMacro.EXISTS_ONE_NEW.getDefinition().getKey()) + .isEqualTo("existsOne:2:true"); + assertThat(CelStandardMacro.EXISTS_ONE_NEW.getDefinition().isVariadic()).isFalse(); + assertThat(CelStandardMacro.EXISTS_ONE_NEW.getDefinition().toString()) + .isEqualTo(CelStandardMacro.EXISTS_ONE_NEW.getDefinition().getKey()); + assertThat(CelStandardMacro.EXISTS_ONE_NEW.getDefinition().hashCode()) + .isEqualTo(CelStandardMacro.EXISTS_ONE_NEW.getDefinition().getKey().hashCode()); + } + @Test public void testMap2() { assertThat(CelStandardMacro.MAP.getFunction()).isEqualTo(Operator.MAP.getFunction()); diff --git a/parser/src/test/java/dev/cel/parser/CelUnparserImplTest.java b/parser/src/test/java/dev/cel/parser/CelUnparserImplTest.java index 86fb279c0..6789b5621 100644 --- a/parser/src/test/java/dev/cel/parser/CelUnparserImplTest.java +++ b/parser/src/test/java/dev/cel/parser/CelUnparserImplTest.java @@ -15,19 +15,24 @@ package dev.cel.parser; import static com.google.common.truth.Truth.assertThat; +import static dev.cel.parser.CelUnparserVisitor.isOperatorLeftRecursive; +import static dev.cel.parser.CelUnparserVisitor.isOperatorLowerPrecedence; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; import com.google.testing.junit.testparameterinjector.TestParameterValuesProvider; +import com.google.testing.junit.testparameterinjector.TestParameters; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelOptions; import dev.cel.common.CelProtoAbstractSyntaxTree; import dev.cel.common.CelSource; import dev.cel.common.ast.CelExpr; import dev.cel.common.ast.CelExpr.CelCall; -import dev.cel.common.ast.CelExpr.CelCreateMap; -import dev.cel.common.ast.CelExpr.CelCreateStruct; +import dev.cel.common.ast.CelExpr.CelMap; +import dev.cel.common.ast.CelExpr.CelStruct; import dev.cel.extensions.CelOptionalLibrary; import java.util.Arrays; import java.util.List; @@ -39,7 +44,11 @@ public final class CelUnparserImplTest { private final CelParser parser = CelParserImpl.newBuilder() - .setOptions(CelOptions.newBuilder().populateMacroCalls(true).build()) + .setOptions( + CelOptions.newBuilder() + .enableQuotedIdentifierSyntax(true) + .populateMacroCalls(true) + .build()) .addLibraries(CelOptionalLibrary.INSTANCE) .setStandardMacros(CelStandardMacro.STANDARD_MACROS) .build(); @@ -99,6 +108,15 @@ public List provideValues(Context context) { "a ? (b1 || b2) : (c1 && c2)", "(a ? b : c).method(d)", "a + b + c + d", + "foo.`a.b`", + "foo.`a/b`", + "foo.`a-b`", + "foo.`a b`", + "foo.`in`", + "Foo{`a.b`: foo}", + "Foo{`a/b`: foo}", + "Foo{`a-b`: foo}", + "Foo{`a b`: foo}", // Constants "true", @@ -140,6 +158,7 @@ public List provideValues(Context context) { // Macros "has(x[\"a\"].single_int32)", + "has(x.`foo-bar`.single_int32)", // This is a filter expression but is decompiled back to // map(x, filter_function, x) for which the evaluation is @@ -196,11 +215,11 @@ public List provideValues(Context context) { .build()) .build(), // bad args CelExpr.newBuilder() - .setCreateStruct( - CelCreateStruct.newBuilder() + .setStruct( + CelStruct.newBuilder() .setMessageName("Msg") .addEntries( - CelCreateStruct.Entry.newBuilder() + CelStruct.Entry.newBuilder() .setId(0) .setValue(CelExpr.newBuilder().build()) .setFieldKey("field") @@ -208,10 +227,10 @@ public List provideValues(Context context) { .build()) .build(), // bad struct CelExpr.newBuilder() - .setCreateMap( - CelCreateMap.newBuilder() + .setMap( + CelMap.newBuilder() .addEntries( - CelCreateMap.Entry.newBuilder() + CelMap.Entry.newBuilder() .setId(0) .setValue(CelExpr.newBuilder().build()) .setKey(CelExpr.newBuilder().build()) @@ -273,4 +292,84 @@ public void unparse_comprehensionWithoutMacroCallTracking_throwsException() thro "Comprehension unparsing requires macro calls to be populated. Ensure the option is" + " enabled."); } + + @Test + public void unparse_macroWithReceiverStyleArg() throws Exception { + CelParser parser = + CelParserImpl.newBuilder() + .setOptions(CelOptions.newBuilder().populateMacroCalls(true).build()) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .build(); + CelAbstractSyntaxTree ast = + parser.parse("[\"a\"].all(x, x.trim().lowerAscii().contains(\"b\"))").getAst(); + + assertThat(unparser.unparse(ast)) + .isEqualTo("[\"a\"].all(x, x.trim().lowerAscii().contains(\"b\"))"); + } + + @Test + @TestParameters({ + "{operator: '_[_]'}", + "{operator: '!_'}", + "{operator: '_==_'}", + "{operator: '_?_:_'}", + "{operator: '_!=_'}", + "{operator: '_<_'}", + "{operator: '_<=_'}", + "{operator: '_>_'}", + "{operator: '_>=_'}", + "{operator: '_+_'}", + "{operator: '_-_'}", + "{operator: '_*_'}", + "{operator: '_/_'}", + "{operator: '_%_'}", + "{operator: '-_'}", + "{operator: 'has'}", + "{operator: '_[?_]'}", + "{operator: '@not_strictly_false'}", + }) + public void operatorLeftRecursive(String operator) { + assertTrue(isOperatorLeftRecursive(operator)); + } + + @Test + @TestParameters({ + "{operator: '_&&_'}", + "{operator: '_||_'}", + }) + public void operatorNotLeftRecursive(String operator) { + assertFalse(isOperatorLeftRecursive(operator)); + } + + @Test + @TestParameters({ + "{operator1: '_[_]', operator2: '_&&_'}", + "{operator1: '_&&_', operator2: '_||_'}", + "{operator1: '_||_', operator2: '_?_:_'}", + "{operator1: '!_', operator2: '_*_'}", + "{operator1: '_==_', operator2: '_&&_'}", + "{operator1: '_!=_', operator2: '_?_:_'}", + }) + public void operatorLowerPrecedence(String operator1, String operator2) { + CelExpr expr = + CelExpr.newBuilder().setCall(CelCall.newBuilder().setFunction(operator2).build()).build(); + + assertTrue(isOperatorLowerPrecedence(operator1, expr)); + } + + @Test + @TestParameters({ + "{operator1: '_?_:_', operator2: '_&&_'}", + "{operator1: '_&&_', operator2: '_[_]'}", + "{operator1: '_||_', operator2: '!_'}", + "{operator1: '!_', operator2: '-_'}", + "{operator1: '_==_', operator2: '_!=_'}", + "{operator1: '_!=_', operator2: '_-_'}", + }) + public void operatorNotLowerPrecedence(String operator1, String operator2) { + CelExpr expr = + CelExpr.newBuilder().setCall(CelCall.newBuilder().setFunction(operator2).build()).build(); + + assertFalse(isOperatorLowerPrecedence(operator1, expr)); + } } diff --git a/parser/src/test/resources/parser.baseline b/parser/src/test/resources/parser.baseline index 79dd0ae65..37b8ef3cc 100644 --- a/parser/src/test/resources/parser.baseline +++ b/parser/src/test/resources/parser.baseline @@ -735,55 +735,112 @@ P: __comprehension__( // Target m^#1:Expr.Ident#, // Accumulator - __result__, + @result, // Init 0^#5:int64#, // LoopCondition - true^#7:bool#, + true^#6:bool#, // LoopStep _?_:_( f^#4:Expr.Ident#, _+_( - __result__^#8:Expr.Ident#, - 1^#6:int64# + @result^#7:Expr.Ident#, + 1^#8:int64# )^#9:Expr.Call#, - __result__^#10:Expr.Ident# + @result^#10:Expr.Ident# )^#11:Expr.Call#, // Result _==_( - __result__^#12:Expr.Ident#, - 1^#6:int64# - )^#13:Expr.Call#)^#14:Expr.Comprehension# + @result^#12:Expr.Ident#, + 1^#13:int64# + )^#14:Expr.Call#)^#15:Expr.Comprehension# L: __comprehension__( // Variable v, // Target m^#1[1,0]#, // Accumulator - __result__, + @result, // Init 0^#5[1,12]#, // LoopCondition - true^#7[1,12]#, + true^#6[1,12]#, // LoopStep _?_:_( f^#4[1,16]#, _+_( - __result__^#8[1,12]#, - 1^#6[1,12]# + @result^#7[1,12]#, + 1^#8[1,12]# )^#9[1,12]#, - __result__^#10[1,12]# + @result^#10[1,12]# )^#11[1,12]#, // Result _==_( - __result__^#12[1,12]#, - 1^#6[1,12]# - )^#13[1,12]#)^#14[1,12]# + @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__( @@ -792,40 +849,40 @@ P: __comprehension__( // Target m^#1:Expr.Ident#, // Accumulator - __result__, + @result, // Init []^#5:Expr.CreateList#, // LoopCondition true^#6:bool#, // LoopStep _+_( - __result__^#7:Expr.Ident#, + @result^#7:Expr.Ident#, [ f^#4:Expr.Ident# ]^#8:Expr.CreateList# )^#9:Expr.Call#, // Result - __result__^#10:Expr.Ident#)^#11:Expr.Comprehension# + @result^#10:Expr.Ident#)^#11:Expr.Comprehension# L: __comprehension__( // Variable v, // Target m^#1[1,0]#, // Accumulator - __result__, + @result, // Init []^#5[1,5]#, // LoopCondition true^#6[1,5]#, // LoopStep _+_( - __result__^#7[1,5]#, + @result^#7[1,5]#, [ f^#4[1,9]# ]^#8[1,5]# )^#9[1,5]#, // Result - __result__^#10[1,5]#)^#11[1,5]# + @result^#10[1,5]#)^#11[1,5]# M: m^#1:Expr.Ident#.map( v^#3:Expr.Ident#, f^#4:Expr.Ident# @@ -839,7 +896,7 @@ P: __comprehension__( // Target m^#1:Expr.Ident#, // Accumulator - __result__, + @result, // Init []^#6:Expr.CreateList#, // LoopCondition @@ -848,22 +905,22 @@ P: __comprehension__( _?_:_( p^#4:Expr.Ident#, _+_( - __result__^#8:Expr.Ident#, + @result^#8:Expr.Ident#, [ f^#5:Expr.Ident# ]^#9:Expr.CreateList# )^#10:Expr.Call#, - __result__^#11:Expr.Ident# + @result^#11:Expr.Ident# )^#12:Expr.Call#, // Result - __result__^#13:Expr.Ident#)^#14:Expr.Comprehension# + @result^#13:Expr.Ident#)^#14:Expr.Comprehension# L: __comprehension__( // Variable v, // Target m^#1[1,0]#, // Accumulator - __result__, + @result, // Init []^#6[1,5]#, // LoopCondition @@ -872,15 +929,15 @@ L: __comprehension__( _?_:_( p^#4[1,9]#, _+_( - __result__^#8[1,5]#, + @result^#8[1,5]#, [ f^#5[1,12]# ]^#9[1,5]# )^#10[1,5]#, - __result__^#11[1,5]# + @result^#11[1,5]# )^#12[1,5]#, // Result - __result__^#13[1,5]#)^#14[1,5]# + @result^#13[1,5]#)^#14[1,5]# M: m^#1:Expr.Ident#.map( v^#3:Expr.Ident#, p^#4:Expr.Ident#, @@ -895,7 +952,7 @@ P: __comprehension__( // Target m^#1:Expr.Ident#, // Accumulator - __result__, + @result, // Init []^#5:Expr.CreateList#, // LoopCondition @@ -904,22 +961,22 @@ P: __comprehension__( _?_:_( p^#4:Expr.Ident#, _+_( - __result__^#7:Expr.Ident#, + @result^#7:Expr.Ident#, [ v^#3:Expr.Ident# ]^#8:Expr.CreateList# )^#9:Expr.Call#, - __result__^#10:Expr.Ident# + @result^#10:Expr.Ident# )^#11:Expr.Call#, // Result - __result__^#12:Expr.Ident#)^#13:Expr.Comprehension# + @result^#12:Expr.Ident#)^#13:Expr.Comprehension# L: __comprehension__( // Variable v, // Target m^#1[1,0]#, // Accumulator - __result__, + @result, // Init []^#5[1,8]#, // LoopCondition @@ -928,15 +985,15 @@ L: __comprehension__( _?_:_( p^#4[1,12]#, _+_( - __result__^#7[1,8]#, + @result^#7[1,8]#, [ v^#3[1,9]# ]^#8[1,8]# )^#9[1,8]#, - __result__^#10[1,8]# + @result^#10[1,8]# )^#11[1,8]#, // Result - __result__^#12[1,8]#)^#13[1,8]# + @result^#12[1,8]#)^#13[1,8]# M: m^#1:Expr.Ident#.filter( v^#3:Expr.Ident#, p^#4:Expr.Ident# @@ -1205,7 +1262,7 @@ P: __comprehension__( // Target x^#1:Expr.Ident#, // Accumulator - __result__, + @result, // Init []^#19:Expr.CreateList#, // LoopCondition @@ -1218,7 +1275,7 @@ P: __comprehension__( // Target y^#4:Expr.Ident#, // Accumulator - __result__, + @result, // Init []^#10:Expr.CreateList#, // LoopCondition @@ -1230,32 +1287,32 @@ P: __comprehension__( 0^#9:int64# )^#8:Expr.Call#, _+_( - __result__^#12:Expr.Ident#, + @result^#12:Expr.Ident#, [ z^#6:Expr.Ident# ]^#13:Expr.CreateList# )^#14:Expr.Call#, - __result__^#15:Expr.Ident# + @result^#15:Expr.Ident# )^#16:Expr.Call#, // Result - __result__^#17:Expr.Ident#)^#18:Expr.Comprehension#, + @result^#17:Expr.Ident#)^#18:Expr.Comprehension#, _+_( - __result__^#21:Expr.Ident#, + @result^#21:Expr.Ident#, [ y^#3:Expr.Ident# ]^#22:Expr.CreateList# )^#23:Expr.Call#, - __result__^#24:Expr.Ident# + @result^#24:Expr.Ident# )^#25:Expr.Call#, // Result - __result__^#26:Expr.Ident#)^#27:Expr.Comprehension# + @result^#26:Expr.Ident#)^#27:Expr.Comprehension# L: __comprehension__( // Variable y, // Target x^#1[1,0]#, // Accumulator - __result__, + @result, // Init []^#19[1,8]#, // LoopCondition @@ -1268,7 +1325,7 @@ L: __comprehension__( // Target y^#4[1,12]#, // Accumulator - __result__, + @result, // Init []^#10[1,20]#, // LoopCondition @@ -1280,25 +1337,25 @@ L: __comprehension__( 0^#9[1,28]# )^#8[1,26]#, _+_( - __result__^#12[1,20]#, + @result^#12[1,20]#, [ z^#6[1,21]# ]^#13[1,20]# )^#14[1,20]#, - __result__^#15[1,20]# + @result^#15[1,20]# )^#16[1,20]#, // Result - __result__^#17[1,20]#)^#18[1,20]#, + @result^#17[1,20]#)^#18[1,20]#, _+_( - __result__^#21[1,8]#, + @result^#21[1,8]#, [ y^#3[1,9]# ]^#22[1,8]# )^#23[1,8]#, - __result__^#24[1,8]# + @result^#24[1,8]# )^#25[1,8]#, // Result - __result__^#26[1,8]#)^#27[1,8]# + @result^#26[1,8]#)^#27[1,8]# M: x^#1:Expr.Ident#.filter( y^#3:Expr.Ident#, ^#18:filter# @@ -1319,7 +1376,7 @@ P: __comprehension__( // Target a^#2:Expr.Ident#.b~test-only~^#4:Expr.Select#, // Accumulator - __result__, + @result, // Init []^#8:Expr.CreateList#, // LoopCondition @@ -1328,22 +1385,22 @@ P: __comprehension__( _?_:_( c^#7:Expr.Ident#, _+_( - __result__^#10:Expr.Ident#, + @result^#10:Expr.Ident#, [ c^#6:Expr.Ident# ]^#11:Expr.CreateList# )^#12:Expr.Call#, - __result__^#13:Expr.Ident# + @result^#13:Expr.Ident# )^#14:Expr.Call#, // Result - __result__^#15:Expr.Ident#)^#16:Expr.Comprehension# + @result^#15:Expr.Ident#)^#16:Expr.Comprehension# L: __comprehension__( // Variable c, // Target a^#2[1,4]#.b~test-only~^#4[1,3]#, // Accumulator - __result__, + @result, // Init []^#8[1,15]#, // LoopCondition @@ -1352,15 +1409,15 @@ L: __comprehension__( _?_:_( c^#7[1,19]#, _+_( - __result__^#10[1,15]#, + @result^#10[1,15]#, [ c^#6[1,16]# ]^#11[1,15]# )^#12[1,15]#, - __result__^#13[1,15]# + @result^#13[1,15]# )^#14[1,15]#, // Result - __result__^#15[1,15]#)^#16[1,15]# + @result^#15[1,15]#)^#16[1,15]# M: ^#4:has#.filter( c^#6:Expr.Ident#, c^#7:Expr.Ident# @@ -1377,7 +1434,7 @@ P: __comprehension__( // Target x^#1:Expr.Ident#, // Accumulator - __result__, + @result, // Init []^#35:Expr.CreateList#, // LoopCondition @@ -1391,62 +1448,62 @@ P: __comprehension__( // Target y^#4:Expr.Ident#, // Accumulator - __result__, + @result, // Init false^#11:bool#, // LoopCondition @not_strictly_false( !_( - __result__^#12:Expr.Ident# + @result^#12:Expr.Ident# )^#13:Expr.Call# )^#14:Expr.Call#, // LoopStep _||_( - __result__^#15:Expr.Ident#, + @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#, + @result^#17:Expr.Ident#)^#18:Expr.Comprehension#, __comprehension__( // Variable z, // Target y^#20:Expr.Ident#, // Accumulator - __result__, + @result, // Init false^#27:bool#, // LoopCondition @not_strictly_false( !_( - __result__^#28:Expr.Ident# + @result^#28:Expr.Ident# )^#29:Expr.Call# )^#30:Expr.Call#, // LoopStep _||_( - __result__^#31:Expr.Ident#, + @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# + @result^#33:Expr.Ident#)^#34:Expr.Comprehension# )^#19:Expr.Call#, _+_( - __result__^#37:Expr.Ident#, + @result^#37:Expr.Ident#, [ y^#3:Expr.Ident# ]^#38:Expr.CreateList# )^#39:Expr.Call#, - __result__^#40:Expr.Ident# + @result^#40:Expr.Ident# )^#41:Expr.Call#, // Result - __result__^#42:Expr.Ident#)^#43:Expr.Comprehension# + @result^#42:Expr.Ident#)^#43:Expr.Comprehension# L: __comprehension__( // Variable y, // Target x^#1[1,0]#, // Accumulator - __result__, + @result, // Init []^#35[1,8]#, // LoopCondition @@ -1460,55 +1517,55 @@ L: __comprehension__( // Target y^#4[1,12]#, // Accumulator - __result__, + @result, // Init false^#11[1,20]#, // LoopCondition @not_strictly_false( !_( - __result__^#12[1,20]# + @result^#12[1,20]# )^#13[1,20]# )^#14[1,20]#, // LoopStep _||_( - __result__^#15[1,20]#, + @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]#, + @result^#17[1,20]#)^#18[1,20]#, __comprehension__( // Variable z, // Target y^#20[1,37]#, // Accumulator - __result__, + @result, // Init false^#27[1,45]#, // LoopCondition @not_strictly_false( !_( - __result__^#28[1,45]# + @result^#28[1,45]# )^#29[1,45]# )^#30[1,45]#, // LoopStep _||_( - __result__^#31[1,45]#, + @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]# + @result^#33[1,45]#)^#34[1,45]# )^#19[1,34]#, _+_( - __result__^#37[1,8]#, + @result^#37[1,8]#, [ y^#3[1,9]# ]^#38[1,8]# )^#39[1,8]#, - __result__^#40[1,8]# + @result^#40[1,8]# )^#41[1,8]#, // Result - __result__^#42[1,8]#)^#43[1,8]# + @result^#42[1,8]#)^#43[1,8]# M: x^#1:Expr.Ident#.filter( y^#3:Expr.Ident#, _&&_( @@ -1543,7 +1600,7 @@ L: noop_macro( I: get_constant_macro() =====> P: 10^#1:int64# -L: 10^#1[1,18]# +L: 10^#1[NO_POS]# M: get_constant_macro()^#0:Expr.Call# I: a.?b[?0] && a[?c] @@ -1623,3 +1680,47 @@ I: while =====> P: while^#1:Expr.Ident# L: while^#1[1,0]# + +I: foo.`bar` +=====> +P: foo^#1:Expr.Ident#.bar^#2:Expr.Select# +L: foo^#1[1,0]#.bar^#2[1,3]# + +I: foo.`bar-baz` +=====> +P: foo^#1:Expr.Ident#.bar-baz^#2:Expr.Select# +L: foo^#1[1,0]#.bar-baz^#2[1,3]# + +I: foo.`bar baz` +=====> +P: foo^#1:Expr.Ident#.bar baz^#2:Expr.Select# +L: foo^#1[1,0]#.bar baz^#2[1,3]# + +I: foo.`bar.baz` +=====> +P: foo^#1:Expr.Ident#.bar.baz^#2:Expr.Select# +L: foo^#1[1,0]#.bar.baz^#2[1,3]# + +I: foo.`bar/baz` +=====> +P: foo^#1:Expr.Ident#.bar/baz^#2:Expr.Select# +L: foo^#1[1,0]#.bar/baz^#2[1,3]# + +I: foo.`bar_baz` +=====> +P: foo^#1:Expr.Ident#.bar_baz^#2:Expr.Select# +L: foo^#1[1,0]#.bar_baz^#2[1,3]# + +I: foo.`in` +=====> +P: foo^#1:Expr.Ident#.in^#2:Expr.Select# +L: foo^#1[1,0]#.in^#2[1,3]# + +I: Struct{`in`: false} +=====> +P: Struct{ + in:false^#3:bool#^#2:Expr.CreateStruct.Entry# +}^#1:Expr.CreateStruct# +L: Struct{ + in:false^#3[1,13]#^#2[1,11]# +}^#1[1,6]# \ No newline at end of file diff --git a/parser/src/test/resources/parser_errors.baseline b/parser/src/test/resources/parser_errors.baseline index 9ae4565ce..bb4ab3ed3 100644 --- a/parser/src/test/resources/parser_errors.baseline +++ b/parser/src/test/resources/parser_errors.baseline @@ -52,6 +52,66 @@ E: ERROR: :1:10: The argument must be a simple name | 1.exists(2, 3) | .........^ +I: [].all(__result__, x) +=====> +E: ERROR: :1:8: The iteration variable __result__ overwrites accumulator variable + | [].all(__result__, x) + | .......^ + +I: [].exists(__result__, x) +=====> +E: ERROR: :1:11: The iteration variable __result__ overwrites accumulator variable + | [].exists(__result__, x) + | ..........^ + +I: [].exists_one(__result__, x) +=====> +E: ERROR: :1:15: The iteration variable __result__ overwrites accumulator variable + | [].exists_one(__result__, x) + | ..............^ + +I: [].map(__result__, x, x) +=====> +E: ERROR: :1:8: The iteration variable __result__ overwrites accumulator variable + | [].map(__result__, x, x) + | .......^ + +I: [].filter(__result__, x) +=====> +E: ERROR: :1:11: The iteration variable __result__ overwrites accumulator variable + | [].filter(__result__, x) + | ..........^ + +I: [].all(.x, x) +=====> +E: ERROR: :1:9: The argument must be a simple name + | [].all(.x, x) + | ........^ + +I: [].exists(.x, x) +=====> +E: ERROR: :1:12: The argument must be a simple name + | [].exists(.x, x) + | ...........^ + +I: [].exists_one(.x, x) +=====> +E: ERROR: :1:16: The argument must be a simple name + | [].exists_one(.x, x) + | ...............^ + +I: [].map(.x, x, x) +=====> +E: ERROR: :1:9: The argument must be a simple name + | [].map(.x, x, x) + | ........^ + +I: [].filter(.x, x) +=====> +E: ERROR: :1:12: The argument must be a simple name + | [].filter(.x, x) + | ...........^ + I: 1 + + =====> E: ERROR: :1:5: mismatched input '+' expecting {'[', '{', '(', '.', '-', '!', 'true', 'false', 'null', NUM_FLOAT, NUM_INT, NUM_UINT, STRING, BYTES, IDENTIFIER} @@ -85,6 +145,24 @@ ERROR: :1:43: mismatched input '' expecting {'[', '{', '(', '.', '-' | "\a\b\f\n\r\t\v\'\"\\\? Illegal escape \>" | ..........................................^ +I: '?' +=====> +E: ERROR: :1:1: Invalid unicode code point + | '?' + | ^ + +I: '?' +=====> +E: ERROR: :1:1: Invalid unicode code point + | '?' + | ^ + +I: r"\?" +=====> +E: ERROR: :1:1: Invalid unicode code point + | r"\?" + | ^ + I: as =====> E: ERROR: :1:1: reserved identifier: as @@ -201,7 +279,7 @@ I: [1, 2, 3].map(var, var * var) E: ERROR: :1:15: reserved identifier: var | [1, 2, 3].map(var, var * var) | ..............^ -ERROR: :1:15: argument is not an identifier +ERROR: :1:15: The argument must be a simple name | [1, 2, 3].map(var, var * var) | ..............^ ERROR: :1:20: reserved identifier: var @@ -255,13 +333,22 @@ E: ERROR: :1:2: mismatched input '' expecting {'[', '{', '}', '(', ' I: t{>C} =====> -E: ERROR: :1:3: extraneous input '>' expecting {'}', ',', '?', IDENTIFIER} +E: ERROR: :1:3: extraneous input '>' expecting {'}', ',', '?', IDENTIFIER, ESC_IDENTIFIER} | t{>C} | ..^ ERROR: :1:5: mismatched input '}' expecting ':' | t{>C} | ....^ +I: has([(has(( +=====> +E: 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(( + | ...........^ + I: a.?b && a[?b] =====> E: ERROR: :1:2: unsupported syntax '.?' @@ -288,3 +375,51 @@ E: ERROR: :1:2: unsupported syntax '?' ERROR: :1:6: unsupported syntax '?' | [?a, ?b] | .....^ + +I: `bar` +=====> +E: ERROR: :1:1: mismatched input '`bar`' expecting {'[', '{', '(', '.', '-', '!', 'true', 'false', 'null', NUM_FLOAT, NUM_INT, NUM_UINT, STRING, BYTES, IDENTIFIER} + | `bar` + | ^ + +I: foo.`` +=====> +E: ERROR: :1:5: token recognition error at: '``' + | foo.`` + | ....^ +ERROR: :1:7: no viable alternative at input '.' + | foo.`` + | ......^ + +I: foo.`$bar` +=====> +E: ERROR: :1:5: token recognition error at: '`$' + | foo.`$bar` + | ....^ +ERROR: :1:10: token recognition error at: '`' + | foo.`$bar` + | .........^ + +I: foo.`bar` +=====> +E: ERROR: :1:5: unsupported syntax '`' + | foo.`bar` + | ....^ + +I: Struct{`bar`: false} +=====> +E: ERROR: :1:8: unsupported syntax '`' + | Struct{`bar`: false} + | .......^ + +I: has(.`.` +=====> +E: ERROR: :1:6: no viable alternative at input '.`.`' + | has(.`.` + | .....^ +ERROR: :1:6: unsupported syntax '`' + | has(.`.` + | .....^ +ERROR: :1:9: missing ')' at '' + | has(.`.` + | ........^ \ No newline at end of file diff --git a/parser/src/test/resources/parser_legacyAccuVar.baseline b/parser/src/test/resources/parser_legacyAccuVar.baseline new file mode 100644 index 000000000..5f9a48b31 --- /dev/null +++ b/parser/src/test/resources/parser_legacyAccuVar.baseline @@ -0,0 +1,280 @@ +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: 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.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: 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# \ No newline at end of file diff --git a/parser/src/test/resources/source_info.baseline b/parser/src/test/resources/source_info.baseline new file mode 100644 index 000000000..153a49822 --- /dev/null +++ b/parser/src/test/resources/source_info.baseline @@ -0,0 +1,143 @@ +I: [{}, {'field': true}].exists(i, has(i.field)) +=====> +S: location: "" +line_offsets: 46 +positions { + key: 1 + value: 0 +} +positions { + key: 2 + value: 1 +} +positions { + key: 3 + value: 5 +} +positions { + key: 4 + value: 13 +} +positions { + key: 5 + value: 6 +} +positions { + key: 6 + value: 15 +} +positions { + key: 8 + value: 29 +} +positions { + key: 10 + value: 36 +} +positions { + key: 11 + value: 37 +} +positions { + key: 12 + value: 35 +} +positions { + key: 13 + value: 28 +} +positions { + key: 14 + value: 28 +} +positions { + key: 15 + value: 28 +} +positions { + key: 16 + value: 28 +} +positions { + key: 17 + value: 28 +} +positions { + key: 18 + value: 28 +} +positions { + key: 19 + value: 28 +} +positions { + key: 20 + value: 28 +} +macro_calls { + key: 12 + value { + call_expr { + function: "has" + args { + id: 11 + select_expr { + operand { + id: 10 + ident_expr { + name: "i" + } + } + field: "field" + } + } + } + } +} +macro_calls { + key: 20 + value { + call_expr { + target { + id: 1 + list_expr { + elements { + id: 2 + struct_expr { + } + } + elements { + id: 3 + struct_expr { + entries { + id: 4 + map_key { + id: 5 + const_expr { + string_value: "field" + } + } + value { + id: 6 + const_expr { + bool_value: true + } + } + } + } + } + } + } + function: "exists" + args { + id: 8 + ident_expr { + name: "i" + } + } + args { + id: 12 + } + } + } +} diff --git a/policy/BUILD.bazel b/policy/BUILD.bazel new file mode 100644 index 000000000..bce68f001 --- /dev/null +++ b/policy/BUILD.bazel @@ -0,0 +1,67 @@ +load("@rules_java//java:defs.bzl", "java_library") + +package( + default_applicable_licenses = ["//:license"], + default_visibility = ["//visibility:public"], +) + +java_library( + name = "policy", + exports = ["//policy/src/main/java/dev/cel/policy"], +) + +java_library( + name = "compiled_rule", + exports = ["//policy/src/main/java/dev/cel/policy:compiled_rule"], +) + +java_library( + name = "source", + exports = ["//policy/src/main/java/dev/cel/policy:source"], +) + +java_library( + name = "validation_exception", + exports = ["//policy/src/main/java/dev/cel/policy:validation_exception"], +) + +java_library( + name = "parser", + exports = ["//policy/src/main/java/dev/cel/policy:parser"], +) + +java_library( + name = "policy_parser_context", + exports = ["//policy/src/main/java/dev/cel/policy:policy_parser_context"], +) + +java_library( + name = "parser_factory", + exports = ["//policy/src/main/java/dev/cel/policy:parser_factory"], +) + +java_library( + name = "compiler_factory", + exports = ["//policy/src/main/java/dev/cel/policy:compiler_factory"], +) + +java_library( + name = "parser_builder", + exports = ["//policy/src/main/java/dev/cel/policy:parser_builder"], +) + +java_library( + name = "compiler", + exports = ["//policy/src/main/java/dev/cel/policy:compiler"], +) + +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 new file mode 100644 index 000000000..e0d6af461 --- /dev/null +++ b/policy/src/main/java/dev/cel/policy/BUILD.bazel @@ -0,0 +1,266 @@ +load("@rules_java//java:defs.bzl", "java_library") + +package( + default_applicable_licenses = ["//:license"], + default_visibility = [ + "//policy:__pkg__", + "//publish:__pkg__", + ], +) + +java_library( + name = "policy", + srcs = [ + "CelPolicy.java", + ], + tags = [ + ], + deps = [ + ":required_fields_checker", + ":source", + "//:auto_value", + "//common/formats:value_string", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "source", + srcs = [ + "CelPolicySource.java", + ], + tags = [ + ], + deps = [ + "//:auto_value", + "//common:cel_source_helper", + "//common:source", + "//common:source_location", + "//common/internal", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "validation_exception", + srcs = [ + "CelPolicyValidationException.java", + ], + tags = [ + ], +) + +java_library( + name = "parser_factory", + srcs = ["CelPolicyParserFactory.java"], + tags = [ + ], + deps = [ + ":parser_builder", + ":yaml_parser", + "@maven//:org_yaml_snakeyaml", + ], +) + +java_library( + name = "yaml_parser", + srcs = [ + "CelPolicyYamlParser.java", + ], + deps = [ + ":parser", + ":parser_builder", + ":policy", + ":policy_parser_context", + ":source", + ":validation_exception", + "//common:compiler_common", + "//common/formats:parser_context", + "//common/formats:value_string", + "//common/formats:yaml_helper", + "//common/formats:yaml_parser_context_impl", + "//common/internal", + "@maven//:com_google_guava_guava", + "@maven//:org_yaml_snakeyaml", + ], +) + +java_library( + name = "parser", + srcs = [ + "CelPolicyParser.java", + ], + tags = [ + ], + deps = [ + ":policy", + ":policy_parser_context", + ":validation_exception", + ], +) + +java_library( + name = "parser_builder", + srcs = [ + "CelPolicyParserBuilder.java", + ], + tags = [ + ], + deps = [ + ":parser", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +java_library( + name = "compiler", + srcs = [ + "CelPolicyCompiler.java", + ], + tags = [ + ], + deps = [ + ":compiled_rule", + ":policy", + ":validation_exception", + "//common:cel_ast", + ], +) + +java_library( + name = "compiler_builder", + srcs = [ + "CelPolicyCompilerBuilder.java", + ], + tags = [ + ], + deps = [ + ":compiler", + "//optimizer:ast_optimizer", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +java_library( + name = "compiler_factory", + srcs = ["CelPolicyCompilerFactory.java"], + tags = [ + ], + deps = [ + ":compiler_builder", + ":compiler_impl", + "//bundle:cel", + "//checker:checker_builder", + "//compiler", + "//compiler:compiler_builder", + "//parser:parser_builder", + "//runtime", + ], +) + +java_library( + name = "policy_parser_context", + srcs = [ + "PolicyParserContext.java", + ], + tags = [ + ], + deps = [ + ":policy", + "//:auto_value", + "//common/formats:parser_context", + "//policy:source", + ], +) + +java_library( + name = "compiled_rule", + srcs = ["CelCompiledRule.java"], + deps = [ + "//:auto_value", + "//bundle:cel", + "//common:cel_ast", + "//common:compiler_common", + "//common/ast", + "//common/formats:value_string", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "compiler_impl", + srcs = [ + "CelPolicyCompilerImpl.java", + ], + visibility = ["//visibility:private"], + deps = [ + ":compiled_rule", + ":compiler", + ":compiler_builder", + ":policy", + ":rule_composer", + ":source", + ":validation_exception", + "//bundle:cel", + "//common:cel_ast", + "//common:cel_source", + "//common:compiler_common", + "//common:container", + "//common:source_location", + "//common/ast", + "//common/formats:value_string", + "//common/types", + "//common/types:type_providers", + "//optimizer", + "//optimizer:ast_optimizer", + "//optimizer:optimization_exception", + "//optimizer:optimizer_builder", + "//optimizer/optimizers:common_subexpression_elimination", + "//optimizer/optimizers:constant_folding", + "//validator", + "//validator:ast_validator", + "//validator:validator_builder", + "//validator/validators:ast_depth_limit_validator", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "required_fields_checker", + srcs = [ + "RequiredFieldsChecker.java", + ], + visibility = ["//visibility:private"], + deps = [ + "//:auto_value", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "rule_composer", + srcs = ["RuleComposer.java"], + deps = [ + ":compiled_rule", + "//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:cel_types", + "//common/types:type_providers", + "//extensions:optional_library", + "//optimizer:ast_optimizer", + "//optimizer:mutable_ast", + "@maven//:com_google_guava_guava", + ], +) diff --git a/policy/src/main/java/dev/cel/policy/CelCompiledRule.java b/policy/src/main/java/dev/cel/policy/CelCompiledRule.java new file mode 100644 index 000000000..af40bd74f --- /dev/null +++ b/policy/src/main/java/dev/cel/policy/CelCompiledRule.java @@ -0,0 +1,163 @@ +// 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. + +package dev.cel.policy; + +import com.google.auto.value.AutoOneOf; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import dev.cel.bundle.Cel; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelVarDecl; +import dev.cel.common.ast.CelConstant; +import dev.cel.common.ast.CelExpr; +import dev.cel.common.formats.ValueString; +import java.util.Optional; + +/** + * Abstract representation of a compiled rule. This contains set of compiled variables and match + * statements which defines an expression graph for a policy. + */ +@AutoValue +public abstract class CelCompiledRule { + + /** Source metadata identifier associated with the compiled rule. */ + public abstract long sourceId(); + + public abstract Optional ruleId(); + + public abstract ImmutableList variables(); + + public abstract ImmutableList matches(); + + public abstract Cel cel(); + + /** + * 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() { + boolean isOptionalOutput = false; + for (CelCompiledMatch match : matches()) { + if (match.result().kind().equals(CelCompiledMatch.Result.Kind.RULE) + && match.result().rule().hasOptionalOutput()) { + // 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; + } + } + + return isOptionalOutput; + } + + /** + * A compiled policy variable (ex: variables.foo). Note that this is not the same thing as the + * variables declared in the config. + */ + @AutoValue + public abstract static class CelCompiledVariable { + public abstract String name(); + + /** Compiled variable in AST. */ + public abstract CelAbstractSyntaxTree ast(); + + /** The variable declaration used to compile this variable in {@link #ast}. */ + public abstract CelVarDecl celVarDecl(); + + static CelCompiledVariable create( + String name, CelAbstractSyntaxTree ast, CelVarDecl celVarDecl) { + return new AutoValue_CelCompiledRule_CelCompiledVariable(name, ast, celVarDecl); + } + } + + /** A compiled Match. */ + @AutoValue + public abstract static class CelCompiledMatch { + /** Source metadata identifier associated with the compiled match. */ + public abstract long sourceId(); + + public abstract CelAbstractSyntaxTree condition(); + + public abstract Result result(); + + public boolean isConditionTriviallyTrue() { + CelExpr celExpr = condition().getExpr(); + return celExpr.constantOrDefault().getKind().equals(CelConstant.Kind.BOOLEAN_VALUE) + && celExpr.constant().booleanValue(); + } + + /** Encapsulates the result of this match when condition is met. (either an output or a rule) */ + @AutoOneOf(CelCompiledMatch.Result.Kind.class) + public abstract static class Result { + public abstract OutputValue output(); + + public abstract CelCompiledRule rule(); + + public abstract Kind kind(); + + static Result ofOutput(long id, CelAbstractSyntaxTree ast) { + return AutoOneOf_CelCompiledRule_CelCompiledMatch_Result.output( + OutputValue.create(id, ast)); + } + + static Result ofRule(CelCompiledRule value) { + return AutoOneOf_CelCompiledRule_CelCompiledMatch_Result.rule(value); + } + + /** Kind for {@link Result}. */ + public enum Kind { + OUTPUT, + RULE + } + } + + /** + * Encapsulates the output value of the match with its original ID that was used to compile + * with. + */ + @AutoValue + public abstract static class OutputValue { + + /** Source metadata identifier associated with the output. */ + public abstract long sourceId(); + + public abstract CelAbstractSyntaxTree ast(); + + public static OutputValue create(long id, CelAbstractSyntaxTree ast) { + return new AutoValue_CelCompiledRule_CelCompiledMatch_OutputValue(id, ast); + } + } + + static CelCompiledMatch create( + long sourceId, CelAbstractSyntaxTree condition, CelCompiledMatch.Result result) { + return new AutoValue_CelCompiledRule_CelCompiledMatch(sourceId, condition, result); + } + } + + static CelCompiledRule create( + long sourceId, + Optional ruleId, + ImmutableList variables, + ImmutableList matches, + Cel cel) { + return new AutoValue_CelCompiledRule(sourceId, ruleId, variables, matches, cel); + } +} diff --git a/policy/src/main/java/dev/cel/policy/CelPolicy.java b/policy/src/main/java/dev/cel/policy/CelPolicy.java new file mode 100644 index 000000000..19f6631d0 --- /dev/null +++ b/policy/src/main/java/dev/cel/policy/CelPolicy.java @@ -0,0 +1,331 @@ +// 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. + +package dev.cel.policy; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.auto.value.AutoOneOf; +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.common.formats.ValueString; +import java.util.ArrayList; +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; + +/** + * Abstract representation of a policy. It declares a name, rule, and evaluation semantic for a + * given expression graph. + */ +@AutoValue +public abstract class CelPolicy { + + public abstract ValueString name(); + + public abstract Optional description(); + + public abstract Optional displayName(); + + public abstract Rule rule(); + + public abstract CelPolicySource policySource(); + + public abstract ImmutableMap metadata(); + + public abstract ImmutableList imports(); + + /** Creates a new builder to construct a {@link CelPolicy} instance. */ + public static Builder newBuilder() { + return new AutoValue_CelPolicy.Builder() + .setName(ValueString.of(0, "")) + .setRule(Rule.newBuilder(0).build()) + .setMetadata(ImmutableMap.of()); + } + + public abstract Builder toBuilder(); + + /** Builder for {@link CelPolicy}. */ + @AutoValue.Builder + public abstract static class Builder { + public abstract CelPolicySource policySource(); + + public abstract Builder setName(ValueString name); + + public abstract Builder setDescription(ValueString description); + + public abstract Builder setDisplayName(ValueString displayName); + + public abstract Builder setRule(Rule rule); + + public abstract Builder setPolicySource(CelPolicySource policySource); + + private final HashMap metadata = new HashMap<>(); + + public abstract Builder setMetadata(ImmutableMap value); + + private final ArrayList importList = new ArrayList<>(); + + abstract Builder setImports(ImmutableList value); + + public List imports() { + return Collections.unmodifiableList(importList); + } + + public Map metadata() { + return Collections.unmodifiableMap(metadata); + } + + @CanIgnoreReturnValue + public Builder addImport(Import value) { + importList.add(value); + return this; + } + + @CanIgnoreReturnValue + public Builder addImports(Collection values) { + importList.addAll(values); + return this; + } + + @CanIgnoreReturnValue + public Builder putMetadata(String key, Object value) { + metadata.put(key, value); + return this; + } + + @CanIgnoreReturnValue + public Builder putMetadata(Map map) { + metadata.putAll(map); + return this; + } + + abstract CelPolicy autoBuild(); + + public CelPolicy build() { + setImports(ImmutableList.copyOf(importList)); + setMetadata(ImmutableMap.copyOf(metadata)); + return autoBuild(); + } + } + + /** + * Rule declares a rule identifier, description, along with a set of variables and match + * statements. + */ + @AutoValue + public abstract static class Rule { + public abstract long id(); + + public abstract Optional ruleId(); + + public abstract Optional description(); + + public abstract ImmutableSet variables(); + + public abstract ImmutableSet matches(); + + /** Builder for {@link Rule}. */ + public static Builder newBuilder(long id) { + return new AutoValue_CelPolicy_Rule.Builder() + .setId(id) + .setVariables(ImmutableSet.of()) + .setMatches(ImmutableSet.of()); + } + + /** Creates a new builder to construct a {@link Rule} instance. */ + @AutoValue.Builder + public abstract static class Builder { + + public abstract Rule.Builder setRuleId(ValueString id); + + public abstract Rule.Builder setDescription(ValueString description); + + abstract ImmutableSet variables(); + + abstract ImmutableSet.Builder variablesBuilder(); + + abstract ImmutableSet matches(); + + abstract ImmutableSet.Builder matchesBuilder(); + + abstract Builder setId(long value); + + @CanIgnoreReturnValue + public Builder addVariables(Variable... variables) { + return addVariables(Arrays.asList(variables)); + } + + @CanIgnoreReturnValue + public Builder addVariables(Iterable variables) { + this.variablesBuilder().addAll(checkNotNull(variables)); + return this; + } + + @CanIgnoreReturnValue + public Builder addMatches(Match... matches) { + return addMatches(Arrays.asList(matches)); + } + + @CanIgnoreReturnValue + public Builder addMatches(Iterable matches) { + this.matchesBuilder().addAll(checkNotNull(matches)); + return this; + } + + abstract Rule.Builder setVariables(ImmutableSet variables); + + abstract Rule.Builder setMatches(ImmutableSet matches); + + public abstract Rule build(); + } + } + + /** + * Match declares a condition (defaults to true) as well as an output or a rule. Either the output + * or the rule field may be set, but not both. + */ + @AutoValue + public abstract static class Match { + + public abstract ValueString condition(); + + public abstract Result result(); + + public abstract long id(); + + /** Explanation returns the explanation expression, or empty expression if output is not set. */ + public abstract Optional explanation(); + + /** Encapsulates the result of this match when condition is met. (either an output or a rule) */ + @AutoOneOf(Match.Result.Kind.class) + public abstract static class Result { + public abstract ValueString output(); + + public abstract Rule rule(); + + public abstract Kind kind(); + + public static Result ofOutput(ValueString value) { + return AutoOneOf_CelPolicy_Match_Result.output(value); + } + + public static Result ofRule(Rule value) { + return AutoOneOf_CelPolicy_Match_Result.rule(value); + } + + /** Kind for {@link Result}. */ + public enum Kind { + OUTPUT, + RULE + } + } + + /** Builder for {@link Match}. */ + @AutoValue.Builder + public abstract static class Builder implements RequiredFieldsChecker { + public abstract Builder setId(long value); + + public abstract Builder setCondition(ValueString condition); + + public abstract Builder setResult(Result result); + + public abstract Builder setExplanation(ValueString explanation); + + abstract Optional id(); + + public abstract Optional result(); + + abstract Optional explanation(); + + @Override + public ImmutableList requiredFields() { + return ImmutableList.of(RequiredField.of("output or a rule", this::result)); + } + + public abstract Match build(); + } + + /** Creates a new builder to construct a {@link Match} instance. */ + public static Builder newBuilder(long id) { + return new AutoValue_CelPolicy_Match.Builder().setId(id); + } + } + + /** Variable is a named expression which may be referenced in subsequent expressions. */ + @AutoValue + public abstract static class Variable { + + public abstract ValueString name(); + + public abstract ValueString expression(); + + public abstract Optional description(); + + public abstract Optional displayName(); + + /** Builder for {@link Variable}. */ + @AutoValue.Builder + public abstract static class Builder implements RequiredFieldsChecker { + + abstract Optional name(); + + abstract Optional expression(); + + abstract Optional description(); + + abstract Optional displayName(); + + public abstract Builder setName(ValueString name); + + public abstract Builder setExpression(ValueString expression); + + public abstract Builder setDescription(ValueString description); + + public abstract Builder setDisplayName(ValueString displayName); + + @Override + public ImmutableList requiredFields() { + return ImmutableList.of( + RequiredField.of("name", this::name), RequiredField.of("expression", this::expression)); + } + + public abstract Variable build(); + } + + /** Creates a new builder to construct a {@link Variable} instance. */ + public static Builder newBuilder() { + return new AutoValue_CelPolicy_Variable.Builder(); + } + } + + /** Import represents an imported type name which is aliased within CEL expressions. */ + @AutoValue + public abstract static class Import { + public abstract long id(); + + public abstract ValueString name(); + + public static Import create(long id, ValueString name) { + return new AutoValue_CelPolicy_Import(id, name); + } + } +} diff --git a/policy/src/main/java/dev/cel/policy/CelPolicyCompiler.java b/policy/src/main/java/dev/cel/policy/CelPolicyCompiler.java new file mode 100644 index 000000000..e0af6d85b --- /dev/null +++ b/policy/src/main/java/dev/cel/policy/CelPolicyCompiler.java @@ -0,0 +1,45 @@ +// 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. + +package dev.cel.policy; + +import dev.cel.common.CelAbstractSyntaxTree; + +/** Public interface for compiling CEL policies. */ +public interface CelPolicyCompiler { + + /** + * Combines the {@link #compileRule} and {@link #compose} into a single call. + * + *

This generates a single CEL AST from a collection of policy expressions associated with a + * CEL environment. + */ + default CelAbstractSyntaxTree compile(CelPolicy policy) throws CelPolicyValidationException { + return compose(policy, compileRule(policy)); + } + + /** + * Produces a {@link CelCompiledRule} from the policy which contains a set of compiled variables + * and match statements. Compiled rule defines an expression graph, which can be composed into a + * single expression via {@link #compose} call. + */ + CelCompiledRule compileRule(CelPolicy policy) throws CelPolicyValidationException; + + /** + * Composes {@link CelCompiledRule}, representing an expression graph, into a single expression + * value. + */ + CelAbstractSyntaxTree compose(CelPolicy policy, CelCompiledRule compiledRule) + throws CelPolicyValidationException; +} diff --git a/policy/src/main/java/dev/cel/policy/CelPolicyCompilerBuilder.java b/policy/src/main/java/dev/cel/policy/CelPolicyCompilerBuilder.java new file mode 100644 index 000000000..4089477a1 --- /dev/null +++ b/policy/src/main/java/dev/cel/policy/CelPolicyCompilerBuilder.java @@ -0,0 +1,49 @@ +// 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. + +package dev.cel.policy; + +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 { + + /** Sets the prefix for the policy variables. Default is `variables.`. */ + @CanIgnoreReturnValue + CelPolicyCompilerBuilder setVariablesPrefix(String prefix); + + /** + * Limit the number of iteration while composing rules into a single AST. An exception is thrown + * if the iteration count exceeds the set value. + */ + @CanIgnoreReturnValue + CelPolicyCompilerBuilder setIterationLimit(int iterationLimit); + + /** + * Enforces the composed AST to stay below the configured depth limit. An exception is thrown if + * the depth exceeds the configured limit. Setting a negative value disables this check. + */ + @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/CelPolicyCompilerFactory.java b/policy/src/main/java/dev/cel/policy/CelPolicyCompilerFactory.java new file mode 100644 index 000000000..8641e5bb7 --- /dev/null +++ b/policy/src/main/java/dev/cel/policy/CelPolicyCompilerFactory.java @@ -0,0 +1,46 @@ +// 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. + +package dev.cel.policy; + +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 policy compilers. */ +public final class CelPolicyCompilerFactory { + + /** Create a builder for constructing a {@link CelPolicyCompiler} instance. */ + public static CelPolicyCompilerBuilder newPolicyCompiler(Cel cel) { + return CelPolicyCompilerImpl.newBuilder(cel); + } + + /** Create a builder for constructing a {@link CelPolicyCompiler} instance. */ + public static CelPolicyCompilerBuilder newPolicyCompiler( + CelCompiler celCompiler, CelRuntime celRuntime) { + return newPolicyCompiler(CelFactory.combine(celCompiler, celRuntime)); + } + + /** Create a builder for constructing a {@link CelPolicyCompiler} instance. */ + public static CelPolicyCompilerBuilder newPolicyCompiler( + CelParser celParser, CelChecker celChecker, CelRuntime celRuntime) { + return newPolicyCompiler(CelCompilerFactory.combine(celParser, celChecker), celRuntime); + } + + private CelPolicyCompilerFactory() {} +} diff --git a/policy/src/main/java/dev/cel/policy/CelPolicyCompilerImpl.java b/policy/src/main/java/dev/cel/policy/CelPolicyCompilerImpl.java new file mode 100644 index 000000000..37a79f98a --- /dev/null +++ b/policy/src/main/java/dev/cel/policy/CelPolicyCompilerImpl.java @@ -0,0 +1,408 @@ +// 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. + +package dev.cel.policy; + +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.CanIgnoreReturnValue; +import dev.cel.bundle.Cel; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelContainer; +import dev.cel.common.CelIssue; +import dev.cel.common.CelSource; +import dev.cel.common.CelSourceLocation; +import dev.cel.common.CelValidationException; +import dev.cel.common.CelValidationResult; +import dev.cel.common.CelVarDecl; +import dev.cel.common.ast.CelConstant; +import dev.cel.common.ast.CelExpr; +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; +import dev.cel.optimizer.optimizers.ConstantFoldingOptimizer; +import dev.cel.optimizer.optimizers.SubexpressionOptimizer; +import dev.cel.optimizer.optimizers.SubexpressionOptimizer.SubexpressionOptimizerOptions; +import dev.cel.policy.CelCompiledRule.CelCompiledMatch; +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.Import; +import dev.cel.policy.CelPolicy.Match; +import dev.cel.policy.CelPolicy.Variable; +import dev.cel.policy.RuleComposer.RuleCompositionException; +import dev.cel.validator.CelAstValidator; +import dev.cel.validator.CelValidator; +import dev.cel.validator.CelValidatorFactory; +import dev.cel.validator.validators.AstDepthLimitValidator; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +/** Package-private implementation for policy compiler. */ +final class CelPolicyCompilerImpl implements CelPolicyCompiler { + private static final String DEFAULT_VARIABLE_PREFIX = "variables."; + private static final int DEFAULT_ITERATION_LIMIT = 1000; + private final Cel cel; + private final String variablesPrefix; + private final int iterationLimit; + private final ImmutableList optimizers; + private final Optional astDepthValidator; + + @Override + public CelCompiledRule compileRule(CelPolicy policy) throws CelPolicyValidationException { + CompilerContext compilerContext = new CompilerContext(policy.policySource()); + + Cel extendedCel = this.cel; + + if (!policy.imports().isEmpty()) { + CelContainer.Builder containerBuilder = + extendedCel.toCheckerBuilder().container().toBuilder(); + + for (Import imp : policy.imports()) { + try { + containerBuilder.addAbbreviations(imp.name().value()); + } catch (IllegalArgumentException e) { + compilerContext.addIssue( + imp.id(), + CelIssue.formatError( + 1, 0, String.format("Error configuring import: %s", e.getMessage()))); + } + } + + extendedCel = extendedCel.toCelBuilder().setContainer(containerBuilder.build()).build(); + } + + CelCompiledRule compiledRule = compileRuleImpl(policy.rule(), extendedCel, compilerContext); + if (compilerContext.hasError()) { + throw new CelPolicyValidationException(compilerContext.getIssueString()); + } + + return compiledRule; + } + + @Override + public CelAbstractSyntaxTree compose(CelPolicy policy, CelCompiledRule compiledRule) + throws CelPolicyValidationException { + Cel cel = compiledRule.cel(); + CelOptimizer composingOptimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(cel) + .addAstOptimizers( + RuleComposer.newInstance(compiledRule, variablesPrefix, iterationLimit)) + .build(); + + CelAbstractSyntaxTree ast; + + try { + // This is a minimal expression used as a basis of stitching together all the rules into a + // single graph. + ast = cel.compile("true").getAst(); + ast = composingOptimizer.optimize(ast); + } catch (CelValidationException | CelOptimizationException e) { + if (e.getCause() instanceof RuleCompositionException) { + RuleCompositionException re = (RuleCompositionException) e.getCause(); + CompilerContext compilerContext = new CompilerContext(policy.policySource()); + // The exact CEL error message produced from composition failure isn't too useful for users. + // Ex: ERROR: :1:1: found no matching overload for '_?_:_' applied to '(bool, map(int, int), + // bool)' (candidates: (bool, %A0, %A0)) + // Transform the error messages in a user-friendly way while retaining the original + // CelValidationException as its originating cause. + + ImmutableList transformedIssues = + re.compileException.getErrors().stream() + .map(x -> CelIssue.formatError(x.getSourceLocation(), re.failureReason)) + .collect(toImmutableList()); + for (long id : re.errorIds) { + compilerContext.addIssue(id, transformedIssues); + } + + throw new CelPolicyValidationException(compilerContext.getIssueString(), re.getCause()); + } + + // Something has gone seriously wrong. + throw new CelPolicyValidationException("Unexpected error while composing rules.", e); + } + + CelOptimizer astOptimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(cel).addAstOptimizers(optimizers).build(); + try { + // Optimize the composed graph using const fold and CSE + ast = astOptimizer.optimize(ast); + } catch (CelOptimizationException e) { + throw new CelPolicyValidationException( + "Failed to optimize the composed policy. Reason: " + e.getMessage(), e); + } + + assertAstDepthIsSafe(ast, cel); + + return ast; + } + + private void assertAstDepthIsSafe(CelAbstractSyntaxTree ast, Cel cel) + throws CelPolicyValidationException { + if (!astDepthValidator.isPresent()) { + return; + } + CelValidator celValidator = + CelValidatorFactory.standardCelValidatorBuilder(cel) + .addAstValidators(astDepthValidator.get()) + .build(); + CelValidationResult result = celValidator.validate(ast); + if (result.hasError()) { + throw new CelPolicyValidationException(result.getErrorString()); + } + } + + private CelCompiledRule compileRuleImpl( + CelPolicy.Rule rule, Cel ruleCel, CompilerContext compilerContext) { + // 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. + Cel localCel = ruleCel; + ImmutableList.Builder variableBuilder = ImmutableList.builder(); + for (Variable variable : rule.variables()) { + ValueString expression = variable.expression(); + CelAbstractSyntaxTree varAst; + CelType outputType = SimpleType.DYN; + try { + varAst = localCel.compile(expression.value()).getAst(); + outputType = varAst.getResultType(); + } catch (CelValidationException e) { + compilerContext.addIssue(expression.id(), e.getErrors()); + // A sentinel AST representing an error is created to allow compiler checks to continue + varAst = newErrorAst(); + } + String variableName = variable.name().value(); + CelVarDecl newVariable = + CelVarDecl.newVarDeclaration(variablesPrefix + variableName, outputType); + localCel = localCel.toCelBuilder().addVarDeclarations(newVariable).build(); + variableBuilder.add(CelCompiledVariable.create(variableName, varAst, newVariable)); + } + + ImmutableList.Builder matchBuilder = ImmutableList.builder(); + for (Match match : rule.matches()) { + CelAbstractSyntaxTree conditionAst; + try { + conditionAst = localCel.compile(match.condition().value()).getAst(); + if (!conditionAst.getResultType().equals(SimpleType.BOOL)) { + compilerContext.addIssue( + match.condition().id(), + CelIssue.formatError(1, 0, "condition must produce a boolean output.")); + } + } catch (CelValidationException e) { + compilerContext.addIssue(match.condition().id(), e.getErrors()); + continue; + } + + Result matchResult; + switch (match.result().kind()) { + case OUTPUT: + CelAbstractSyntaxTree outputAst; + ValueString output = match.result().output(); + try { + outputAst = localCel.compile(output.value()).getAst(); + } catch (CelValidationException e) { + compilerContext.addIssue(output.id(), e.getErrors()); + continue; + } + + matchResult = Result.ofOutput(output.id(), outputAst); + break; + case RULE: + CelCompiledRule nestedRule = + compileRuleImpl(match.result().rule(), localCel, compilerContext); + matchResult = Result.ofRule(nestedRule); + break; + default: + throw new IllegalArgumentException("Unexpected kind: " + match.result().kind()); + } + + matchBuilder.add(CelCompiledMatch.create(match.id(), conditionAst, matchResult)); + } + + CelCompiledRule compiledRule = + CelCompiledRule.create( + rule.id(), rule.ruleId(), variableBuilder.build(), matchBuilder.build(), ruleCel); + + // Validate that all branches in the policy are reachable + checkUnreachableCode(compiledRule, compilerContext); + + return compiledRule; + } + + private void checkUnreachableCode(CelCompiledRule compiledRule, CompilerContext compilerContext) { + 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 the match is a single output or a nested rule that always returns a value, it is + // exhaustive. If the condition is trivially true, then all subsequent branches are + // unreachable. + boolean isExhaustive = + isTriviallyTrue + && (compiledMatch.result().kind().equals(Kind.OUTPUT) + || !compiledMatch.result().rule().hasOptionalOutput()); + + if (isExhaustive && i != matchCount - 1) { + if (compiledMatch.result().kind().equals(Kind.OUTPUT)) { + compilerContext.addIssue( + compiledMatch.sourceId(), + CelIssue.formatError(1, 0, "Match creates unreachable outputs")); + } else { + compilerContext.addIssue( + compiledMatch.result().rule().sourceId(), + CelIssue.formatError(1, 0, "Rule creates unreachable outputs")); + } + } + } + } + + private static CelAbstractSyntaxTree newErrorAst() { + return CelAbstractSyntaxTree.newParsedAst( + CelExpr.ofConstant(0, CelConstant.ofValue("*error*")), CelSource.newBuilder().build()); + } + + private static final class CompilerContext { + private final ArrayList issues; + private final CelPolicySource celPolicySource; + + private void addIssue(long id, CelIssue... issues) { + addIssue(id, Arrays.asList(issues)); + } + + private void addIssue(long id, List issues) { + for (CelIssue issue : issues) { + CelSourceLocation absoluteLocation = computeAbsoluteLocation(id, issue); + this.issues.add(CelIssue.formatError(absoluteLocation, issue.getMessage())); + } + } + + private CelSourceLocation computeAbsoluteLocation(long id, CelIssue issue) { + int policySourceOffset = + Optional.ofNullable(celPolicySource.getPositionsMap().get(id)).orElse(-1); + if (policySourceOffset == -1) { + return CelSourceLocation.NONE; + } + CelSourceLocation policySourceLocation = + celPolicySource.getOffsetLocation(policySourceOffset).orElse(null); + if (policySourceLocation == null) { + return CelSourceLocation.NONE; + } + + int absoluteLine = issue.getSourceLocation().getLine() + policySourceLocation.getLine() - 1; + int absoluteColumn = issue.getSourceLocation().getColumn() + policySourceLocation.getColumn(); + int absoluteOffset = celPolicySource.getContent().lineOffsets().get(absoluteLine - 2); + + return celPolicySource + .getOffsetLocation(absoluteOffset + absoluteColumn) + .orElse(CelSourceLocation.NONE); + } + + private boolean hasError() { + return !issues.isEmpty(); + } + + private String getIssueString() { + return CelIssue.toDisplayString(issues, celPolicySource); + } + + private CompilerContext(CelPolicySource celPolicySource) { + this.issues = new ArrayList<>(); + this.celPolicySource = celPolicySource; + } + } + + 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) { + this.cel = cel; + this.astDepthLimitValidator = Optional.of(AstDepthLimitValidator.DEFAULT); + } + + @Override + @CanIgnoreReturnValue + public Builder setVariablesPrefix(String prefix) { + this.variablesPrefix = checkNotNull(prefix); + return this; + } + + @Override + @CanIgnoreReturnValue + public Builder setIterationLimit(int iterationLimit) { + this.iterationLimit = iterationLimit; + return this; + } + + @Override + @CanIgnoreReturnValue + public Builder setAstDepthLimit(int astDepthLimit) { + if (astDepthLimit < 0) { + astDepthLimitValidator = Optional.empty(); + } else { + astDepthLimitValidator = Optional.of(AstDepthLimitValidator.newInstance(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, this.optimizers, astDepthLimitValidator); + } + } + + static Builder newBuilder(Cel cel) { + return new Builder(cel) + .setVariablesPrefix(DEFAULT_VARIABLE_PREFIX) + .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/CelPolicyParser.java b/policy/src/main/java/dev/cel/policy/CelPolicyParser.java new file mode 100644 index 000000000..4a20188d5 --- /dev/null +++ b/policy/src/main/java/dev/cel/policy/CelPolicyParser.java @@ -0,0 +1,94 @@ +// 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. + +package dev.cel.policy; + +/** CelPolicyParser is the interface for parsing policies into a canonical Policy representation. */ +public interface CelPolicyParser { + + /** Parses the input {@code policySource} and returns a {@link CelPolicy}. */ + CelPolicy parse(String policySource) throws CelPolicyValidationException; + + /** + * Parses the input {@code policySource} and returns a {@link CelPolicy}. + * + *

The {@code description} may be used to help tailor error messages for the location where the + * {@code policySource} originates, e.g. a file name or form UI element. + */ + CelPolicy parse(String policySource, String description) throws CelPolicyValidationException; + + /** + * TagVisitor declares a set of interfaces for handling custom tags which would otherwise be + * unsupported within the policy, rule, match, or variable objects. + * + * @param Type of the node (ex: YAML). + */ + interface TagVisitor { + + /** + * visitPolicyTag accepts a parser context, field id, tag name, yaml node, and parent Policy to + * allow for continued parsing within a custom tag. + */ + default void visitPolicyTag( + PolicyParserContext ctx, + long id, + String tagName, + T node, + CelPolicy.Builder policyBuilder) { + ctx.reportError(id, String.format("Unsupported policy tag: %s", tagName)); + } + + /** + * visitRuleTag accepts a parser context, field id, tag name, yaml node, as well as the parent + * policy and current rule to allow for continued parsing within custom tags. + */ + default void visitRuleTag( + PolicyParserContext ctx, + long id, + String tagName, + T node, + CelPolicy.Builder policyBuilder, + CelPolicy.Rule.Builder ruleBuilder) { + ctx.reportError(id, String.format("Unsupported rule tag: %s", tagName)); + } + + /** + * visitMatchTag accepts a parser context, field id, tag name, yaml node, as well as the parent + * policy and current match to allow for continued parsing within custom tags. + */ + default void visitMatchTag( + PolicyParserContext ctx, + long id, + String tagName, + T node, + CelPolicy.Builder policyBuilder, + CelPolicy.Match.Builder matchBuilder) { + ctx.reportError(id, String.format("Unsupported match tag: %s", tagName)); + } + + /** + * visitVariableTag accepts a parser context, field id, tag name, yaml node, as well as the + * parent policy and current variable to allow for continued parsing within custom tags. + */ + default void visitVariableTag( + PolicyParserContext ctx, + long id, + String tagName, + T node, + CelPolicy.Builder policyBuilder, + CelPolicy.Variable.Builder variableBuilder) { + ctx.reportError(id, String.format("Unsupported variable tag: %s", tagName)); + } + } +} diff --git a/policy/src/main/java/dev/cel/policy/CelPolicyParserBuilder.java b/policy/src/main/java/dev/cel/policy/CelPolicyParserBuilder.java new file mode 100644 index 000000000..266264780 --- /dev/null +++ b/policy/src/main/java/dev/cel/policy/CelPolicyParserBuilder.java @@ -0,0 +1,59 @@ +// 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. + +package dev.cel.policy; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import com.google.errorprone.annotations.CheckReturnValue; +import dev.cel.policy.CelPolicyParser.TagVisitor; + +/** + * Interface for building an instance of {@link CelPolicyParser}. + * + * @param Type of the node (Ex: YAML). + */ +public interface CelPolicyParserBuilder { + + /** Adds a custom tag visitor to allow for handling of custom tags. */ + @CanIgnoreReturnValue + CelPolicyParserBuilder addTagVisitor(TagVisitor tagVisitor); + + /** + * Configures the parser to allow for key-value pairs to declare a variable name and expression. + * + *

For example: + * + *

{@code
+   * variables:
+   * - foo: bar
+   * - baz: qux
+   * }
+ * + *

This is in contrast to the default behavior, which requires the following syntax: + * + *

{@code
+   * variables:
+   * - name: foo
+   *   expression: bar
+   * - name: baz
+   *   expression: qux
+   * }
+ */ + @CanIgnoreReturnValue + CelPolicyParserBuilder enableSimpleVariables(boolean enable); + + /** Builds a new instance of {@link CelPolicyParser}. */ + @CheckReturnValue + CelPolicyParser build(); +} diff --git a/policy/src/main/java/dev/cel/policy/CelPolicyParserFactory.java b/policy/src/main/java/dev/cel/policy/CelPolicyParserFactory.java new file mode 100644 index 000000000..1c7ba225b --- /dev/null +++ b/policy/src/main/java/dev/cel/policy/CelPolicyParserFactory.java @@ -0,0 +1,31 @@ +// 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. + +package dev.cel.policy; + +import org.yaml.snakeyaml.nodes.Node; + +/** Factory class for producing policy parser and policy config parsers. */ +public final class CelPolicyParserFactory { + + /** + * Configure a builder to construct a {@link CelPolicyParser} instance that takes in a YAML + * document. + */ + public static CelPolicyParserBuilder newYamlParserBuilder() { + return CelPolicyYamlParser.newBuilder(); + } + + private CelPolicyParserFactory() {} +} diff --git a/policy/src/main/java/dev/cel/policy/CelPolicySource.java b/policy/src/main/java/dev/cel/policy/CelPolicySource.java new file mode 100644 index 000000000..2bf488bae --- /dev/null +++ b/policy/src/main/java/dev/cel/policy/CelPolicySource.java @@ -0,0 +1,78 @@ +// 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. + +package dev.cel.policy; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableMap; +import com.google.errorprone.annotations.CheckReturnValue; +import dev.cel.common.CelSourceHelper; +import dev.cel.common.CelSourceLocation; +import dev.cel.common.Source; +import dev.cel.common.internal.CelCodePointArray; +import java.util.Map; +import java.util.Optional; + +/** CelPolicySource represents the source content of a policy and its related metadata. */ +@AutoValue +public abstract class CelPolicySource implements Source { + + @Override + public abstract CelCodePointArray getContent(); + + @Override + public abstract String getDescription(); + + @Override + public abstract ImmutableMap getPositionsMap(); + + @Override + public Optional getSnippet(int line) { + return CelSourceHelper.getSnippet(getContent(), line); + } + + /** + * Get the line and column in the source expression text for the given code point {@code offset}. + */ + public Optional getOffsetLocation(int offset) { + return CelSourceHelper.getOffsetLocation(getContent(), offset); + } + + /** Builder for {@link CelPolicySource}. */ + @AutoValue.Builder + public abstract static class Builder { + + public abstract Builder setContent(CelCodePointArray content); + + public abstract Builder setDescription(String description); + + public abstract Builder setPositionsMap(Map value); + + @CheckReturnValue + public abstract CelPolicySource build(); + } + + public abstract Builder toBuilder(); + + public static Builder newBuilder(String content) { + return newBuilder(CelCodePointArray.fromString(content)); + } + + public static Builder newBuilder(CelCodePointArray celCodePointArray) { + return new AutoValue_CelPolicySource.Builder() + .setDescription("") + .setContent(celCodePointArray) + .setPositionsMap(ImmutableMap.of()); + } +} diff --git a/policy/src/main/java/dev/cel/policy/CelPolicyValidationException.java b/policy/src/main/java/dev/cel/policy/CelPolicyValidationException.java new file mode 100644 index 000000000..4d52fcc1d --- /dev/null +++ b/policy/src/main/java/dev/cel/policy/CelPolicyValidationException.java @@ -0,0 +1,30 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.policy; + +/** + * CelPolicyValidationException encapsulates all issues that arise when parsing or compiling a + * policy. + */ +public final class CelPolicyValidationException extends Exception { + + public CelPolicyValidationException(String message) { + super(message); + } + + public CelPolicyValidationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/policy/src/main/java/dev/cel/policy/CelPolicyYamlParser.java b/policy/src/main/java/dev/cel/policy/CelPolicyYamlParser.java new file mode 100644 index 000000000..18b406af0 --- /dev/null +++ b/policy/src/main/java/dev/cel/policy/CelPolicyYamlParser.java @@ -0,0 +1,498 @@ +// 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. + +package dev.cel.policy; + +import static com.google.common.base.Preconditions.checkNotNull; +import static dev.cel.common.formats.YamlHelper.ERROR; +import static dev.cel.common.formats.YamlHelper.assertRequiredFields; +import static dev.cel.common.formats.YamlHelper.assertYamlType; + +import com.google.common.collect.ImmutableSet; +import dev.cel.common.CelIssue; +import dev.cel.common.formats.ParserContext; +import dev.cel.common.formats.ValueString; +import dev.cel.common.formats.YamlHelper; +import dev.cel.common.formats.YamlHelper.YamlNodeType; +import dev.cel.common.formats.YamlParserContextImpl; +import dev.cel.common.internal.CelCodePointArray; +import dev.cel.policy.CelPolicy.Import; +import dev.cel.policy.CelPolicy.Match; +import dev.cel.policy.CelPolicy.Match.Result; +import dev.cel.policy.CelPolicy.Variable; +import java.util.List; +import java.util.Map; +import org.yaml.snakeyaml.nodes.MappingNode; +import org.yaml.snakeyaml.nodes.Node; +import org.yaml.snakeyaml.nodes.NodeTuple; +import org.yaml.snakeyaml.nodes.ScalarNode; +import org.yaml.snakeyaml.nodes.SequenceNode; + +final class CelPolicyYamlParser implements CelPolicyParser { + + // Sentinel values for parsing errors + private static final ValueString ERROR_VALUE = ValueString.newBuilder().setValue(ERROR).build(); + private static final Match ERROR_MATCH = + 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 final TagVisitor tagVisitor; + private final boolean enableSimpleVariables; + + @Override + public CelPolicy parse(String policySource) throws CelPolicyValidationException { + return parse(policySource, ""); + } + + @Override + public CelPolicy parse(String policySource, String description) + throws CelPolicyValidationException { + ParserImpl parser = + new ParserImpl(tagVisitor, enableSimpleVariables, policySource, description); + return parser.parseYaml(); + } + + private static class ParserImpl implements PolicyParserContext { + + private final TagVisitor tagVisitor; + private final boolean enableSimpleVariables; + private final CelPolicySource policySource; + private final ParserContext ctx; + + private CelPolicy parseYaml() throws CelPolicyValidationException { + Node node; + String policySourceString = policySource.getContent().toString(); + try { + Node yamlNode = + YamlHelper.parseYamlSource(policySourceString) + .orElseThrow( + () -> + new CelPolicyValidationException( + String.format( + "YAML document empty or malformed: %s", policySourceString))); + node = yamlNode; + } catch (RuntimeException e) { + throw new CelPolicyValidationException("YAML document is malformed: " + e.getMessage(), e); + } + + CelPolicy celPolicy = parsePolicy(this, node); + + if (!ctx.getIssues().isEmpty()) { + throw new CelPolicyValidationException( + CelIssue.toDisplayString(ctx.getIssues(), celPolicy.policySource())); + } + + return celPolicy; + } + + @Override + public NewPolicyMetadata newPolicy(Node node) { + long id = ctx.collectMetadata(node); + return NewPolicyMetadata.create(policySource, id); + } + + @Override + public CelPolicy parsePolicy(PolicyParserContext ctx, Node node) { + NewPolicyMetadata newPolicyMetadata = newPolicy(node); + CelPolicy.Builder policyBuilder = newPolicyMetadata.policyBuilder(); + if (!assertYamlType(ctx, newPolicyMetadata.id(), node, YamlNodeType.MAP)) { + return policyBuilder.build(); + } + + MappingNode rootNode = (MappingNode) node; + for (NodeTuple nodeTuple : rootNode.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 fieldName = ((ScalarNode) keyNode).getValue(); + switch (fieldName) { + case "imports": + parseImports(policyBuilder, ctx, valueNode); + break; + case "name": + policyBuilder.setName(ctx.newYamlString(valueNode)); + break; + case "description": + policyBuilder.setDescription(ctx.newYamlString(valueNode)); + break; + case "display_name": + policyBuilder.setDisplayName(ctx.newYamlString(valueNode)); + break; + case "rule": + policyBuilder.setRule(parseRule(ctx, policyBuilder, valueNode)); + break; + default: + tagVisitor.visitPolicyTag(ctx, keyId, fieldName, valueNode, policyBuilder); + break; + } + } + + return policyBuilder + .setPolicySource(policySource.toBuilder().setPositionsMap(ctx.getIdToOffsetMap()).build()) + .build(); + } + + private void parseImports( + CelPolicy.Builder policyBuilder, PolicyParserContext ctx, Node node) { + long id = ctx.collectMetadata(node); + if (!assertYamlType(ctx, id, node, YamlNodeType.LIST)) { + return; + } + + SequenceNode importListNode = (SequenceNode) node; + for (Node importNode : importListNode.getValue()) { + parseImport(policyBuilder, ctx, importNode); + } + } + + private void parseImport( + CelPolicy.Builder policyBuilder, PolicyParserContext ctx, Node node) { + long importId = ctx.collectMetadata(node); + if (!assertYamlType(ctx, importId, 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(); + if (!fieldName.equals("name")) { + ctx.reportError( + keyId, String.format("Invalid import key: %s, expected 'name'", fieldName)); + continue; + } + + Node value = nodeTuple.getValueNode(); + long valueId = ctx.collectMetadata(value); + if (!assertYamlType(ctx, valueId, value, YamlNodeType.STRING, YamlNodeType.TEXT)) { + continue; + } + + policyBuilder.addImport(Import.create(valueId, ctx.newYamlString(value))); + } + } + + @Override + public CelPolicy.Rule parseRule( + PolicyParserContext ctx, CelPolicy.Builder policyBuilder, Node node) { + long valueId = ctx.collectMetadata(node); + CelPolicy.Rule.Builder ruleBuilder = CelPolicy.Rule.newBuilder(valueId); + if (!assertYamlType(ctx, valueId, node, YamlNodeType.MAP)) { + return ruleBuilder.build(); + } + + for (NodeTuple nodeTuple : ((MappingNode) node).getValue()) { + Node key = nodeTuple.getKeyNode(); + long tagId = ctx.collectMetadata(key); + if (!assertYamlType(ctx, tagId, key, YamlNodeType.STRING, YamlNodeType.TEXT)) { + continue; + } + String fieldName = ((ScalarNode) key).getValue(); + Node value = nodeTuple.getValueNode(); + switch (fieldName) { + case "id": + ruleBuilder.setRuleId(ctx.newYamlString(value)); + break; + case "description": + ruleBuilder.setDescription(ctx.newYamlString(value)); + break; + case "variables": + ruleBuilder.addVariables(parseVariables(ctx, policyBuilder, value)); + break; + case "match": + ruleBuilder.addMatches(parseMatches(ctx, policyBuilder, value)); + break; + default: + tagVisitor.visitRuleTag(ctx, tagId, fieldName, value, policyBuilder, ruleBuilder); + break; + } + } + return ruleBuilder.build(); + } + + private ImmutableSet parseMatches( + PolicyParserContext ctx, CelPolicy.Builder policyBuilder, Node node) { + long valueId = ctx.collectMetadata(node); + ImmutableSet.Builder matchesBuilder = ImmutableSet.builder(); + if (!assertYamlType(ctx, valueId, node, YamlNodeType.LIST)) { + return matchesBuilder.build(); + } + + SequenceNode matchListNode = (SequenceNode) node; + for (Node elementNode : matchListNode.getValue()) { + matchesBuilder.add(parseMatch(ctx, policyBuilder, elementNode)); + } + + return matchesBuilder.build(); + } + + @Override + public CelPolicy.Match parseMatch( + PolicyParserContext ctx, CelPolicy.Builder policyBuilder, Node node) { + long nodeId = ctx.collectMetadata(node); + if (!assertYamlType(ctx, nodeId, node, YamlNodeType.MAP)) { + return ERROR_MATCH; + } + MappingNode matchNode = (MappingNode) node; + CelPolicy.Match.Builder matchBuilder = + CelPolicy.Match.newBuilder(nodeId).setCondition(ValueString.of(ctx.nextId(), "true")); + for (NodeTuple nodeTuple : matchNode.getValue()) { + Node key = nodeTuple.getKeyNode(); + long tagId = ctx.collectMetadata(key); + if (!assertYamlType(ctx, tagId, key, YamlNodeType.STRING, YamlNodeType.TEXT)) { + continue; + } + String fieldName = ((ScalarNode) key).getValue(); + Node value = nodeTuple.getValueNode(); + switch (fieldName) { + case "condition": + matchBuilder.setCondition(ctx.newSourceString(value)); + break; + case "output": + matchBuilder + .result() + .filter(result -> result.kind().equals(Match.Result.Kind.RULE)) + .ifPresent( + result -> ctx.reportError(tagId, "Only the rule or the output may be set")); + matchBuilder.setResult(Match.Result.ofOutput(ctx.newSourceString(value))); + break; + case "explanation": + matchBuilder + .result() + .filter(result -> result.kind().equals(Match.Result.Kind.RULE)) + .ifPresent( + result -> + ctx.reportError( + tagId, + "Explanation can only be set on output match cases, not nested rules")); + matchBuilder.setExplanation(ctx.newYamlString(value)); + break; + case "rule": + matchBuilder + .result() + .filter(result -> result.kind().equals(Match.Result.Kind.OUTPUT)) + .ifPresent( + result -> ctx.reportError(tagId, "Only the rule or the output may be set")); + matchBuilder + .explanation() + .ifPresent( + result -> + ctx.reportError( + result.id(), + "Explanation can only be set on output match cases, not nested rules")); + matchBuilder.setResult(Match.Result.ofRule(parseRule(ctx, policyBuilder, value))); + break; + default: + tagVisitor.visitMatchTag(ctx, tagId, fieldName, value, policyBuilder, matchBuilder); + break; + } + } + + if (!assertRequiredFields(ctx, nodeId, matchBuilder.getMissingRequiredFieldNames())) { + return ERROR_MATCH; + } + + return matchBuilder.build(); + } + + private ImmutableSet parseVariables( + PolicyParserContext ctx, CelPolicy.Builder policyBuilder, Node node) { + long valueId = ctx.collectMetadata(node); + ImmutableSet.Builder variableBuilder = ImmutableSet.builder(); + if (!assertYamlType(ctx, valueId, node, YamlNodeType.LIST)) { + return variableBuilder.build(); + } + + SequenceNode variableListNode = (SequenceNode) node; + for (Node elementNode : variableListNode.getValue()) { + variableBuilder.add(parseVariable(ctx, policyBuilder, elementNode)); + } + + return variableBuilder.build(); + } + + @Override + public CelPolicy.Variable parseVariable( + PolicyParserContext ctx, CelPolicy.Builder policyBuilder, Node node) { + long id = ctx.collectMetadata(node); + if (!assertYamlType(ctx, id, node, YamlNodeType.MAP)) { + return ERROR_VARIABLE; + } + + MappingNode variableMap = (MappingNode) node; + Variable.Builder builder = Variable.newBuilder(); + + if (enableSimpleVariables) { + return parseVariableInline(ctx, id, variableMap, builder); + } + return parseVariableObject(ctx, policyBuilder, id, variableMap, builder); + } + + private Variable parseVariableInline( + PolicyParserContext ctx, long id, MappingNode variableMap, Variable.Builder builder) { + int iterations = 0; + for (NodeTuple nodeTuple : variableMap.getValue()) { + Node keyNode = nodeTuple.getKeyNode(); + long keyId = ctx.collectMetadata(keyNode); + builder + .setName(ctx.newYamlString(keyNode)) + .setExpression(ctx.newSourceString(nodeTuple.getValueNode())); + iterations++; + + if (iterations > 1) { + ctx.reportError(keyId, "Only one variable may be defined inline"); + } + } + + if (!assertRequiredFields(ctx, id, builder.getMissingRequiredFieldNames())) { + return ERROR_VARIABLE; + } + + return builder.build(); + } + + private Variable parseVariableObject( + PolicyParserContext ctx, + CelPolicy.Builder policyBuilder, + long id, + MappingNode variableMap, + Variable.Builder builder) { + for (NodeTuple nodeTuple : variableMap.getValue()) { + Node keyNode = nodeTuple.getKeyNode(); + long keyId = ctx.collectMetadata(keyNode); + Node valueNode = nodeTuple.getValueNode(); + String keyName = ((ScalarNode) keyNode).getValue(); + switch (keyName) { + case "name": + builder.setName(ctx.newYamlString(valueNode)); + break; + case "expression": + builder.setExpression(ctx.newSourceString(valueNode)); + break; + case "description": + builder.setDescription(ctx.newYamlString(valueNode)); + break; + case "display_name": + builder.setDisplayName(ctx.newYamlString(valueNode)); + break; + default: + tagVisitor.visitVariableTag(ctx, keyId, keyName, valueNode, policyBuilder, builder); + break; + } + } + + if (!assertRequiredFields(ctx, id, builder.getMissingRequiredFieldNames())) { + return ERROR_VARIABLE; + } + + return builder.build(); + } + + private ParserImpl( + TagVisitor tagVisitor, + boolean enableSimpleVariables, + String source, + String description) { + this.tagVisitor = tagVisitor; + this.enableSimpleVariables = enableSimpleVariables; + this.policySource = + CelPolicySource.newBuilder(CelCodePointArray.fromString(source)) + .setDescription(description) + .build(); + this.ctx = YamlParserContextImpl.newInstance(policySource); + } + + @Override + public long nextId() { + return ctx.nextId(); + } + + @Override + public long collectMetadata(Node node) { + return ctx.collectMetadata(node); + } + + @Override + public void reportError(long id, String message) { + ctx.reportError(id, message); + } + + @Override + public List getIssues() { + return ctx.getIssues(); + } + + @Override + public Map getIdToOffsetMap() { + return ctx.getIdToOffsetMap(); + } + + @Override + public ValueString newYamlString(Node node) { + return ctx.newYamlString(node); + } + + @Override + public ValueString newSourceString(Node node) { + return ctx.newSourceString(node); + } + } + + static final class Builder implements CelPolicyParserBuilder { + + private TagVisitor tagVisitor; + private boolean enableSimpleVariables; + + private Builder() { + this.tagVisitor = new TagVisitor() {}; + this.enableSimpleVariables = false; + } + + @Override + public CelPolicyParserBuilder addTagVisitor(TagVisitor tagVisitor) { + this.tagVisitor = tagVisitor; + return this; + } + + @Override + public CelPolicyParserBuilder enableSimpleVariables(boolean enable) { + this.enableSimpleVariables = enable; + return this; + } + + @Override + public CelPolicyParser build() { + return new CelPolicyYamlParser(tagVisitor, enableSimpleVariables); + } + } + + static CelPolicyParserBuilder newBuilder() { + return new Builder().enableSimpleVariables(false); + } + + private CelPolicyYamlParser(TagVisitor tagVisitor, boolean enableSimpleVariables) { + this.tagVisitor = checkNotNull(tagVisitor); + this.enableSimpleVariables = enableSimpleVariables; + } +} diff --git a/policy/src/main/java/dev/cel/policy/PolicyParserContext.java b/policy/src/main/java/dev/cel/policy/PolicyParserContext.java new file mode 100644 index 000000000..204bf591f --- /dev/null +++ b/policy/src/main/java/dev/cel/policy/PolicyParserContext.java @@ -0,0 +1,54 @@ +// 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.policy; + +import com.google.auto.value.AutoValue; +import dev.cel.common.formats.ParserContext; +import dev.cel.policy.CelPolicy.Match; +import dev.cel.policy.CelPolicy.Rule; +import dev.cel.policy.CelPolicy.Variable; + +/** + * PolicyParserContext declares a set of interfaces for creating and managing metadata specifically + * for {@link CelPolicy}. + */ +public interface PolicyParserContext extends ParserContext { + + /** + * Wrapper for a new instance of {@link CelPolicy.Builder} and the associated node ID. The + * CelPolicy builder also has a policy source set by the parser. + */ + @AutoValue + abstract class NewPolicyMetadata { + public abstract CelPolicy.Builder policyBuilder(); + + public abstract long id(); + + static NewPolicyMetadata create(CelPolicySource source, long id) { + return new AutoValue_PolicyParserContext_NewPolicyMetadata( + CelPolicy.newBuilder().setPolicySource(source), id); + } + } + + NewPolicyMetadata newPolicy(T node); + + CelPolicy parsePolicy(PolicyParserContext ctx, T node); + + Rule parseRule(PolicyParserContext ctx, CelPolicy.Builder policyBuilder, T node); + + Match parseMatch(PolicyParserContext ctx, CelPolicy.Builder policyBuilder, T node); + + Variable parseVariable(PolicyParserContext ctx, CelPolicy.Builder policyBuilder, T node); +} diff --git a/policy/src/main/java/dev/cel/policy/RequiredFieldsChecker.java b/policy/src/main/java/dev/cel/policy/RequiredFieldsChecker.java new file mode 100644 index 000000000..e46b2822a --- /dev/null +++ b/policy/src/main/java/dev/cel/policy/RequiredFieldsChecker.java @@ -0,0 +1,49 @@ +// 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. + +package dev.cel.policy; + +import static com.google.common.collect.ImmutableList.toImmutableList; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import java.util.Optional; +import java.util.function.Supplier; + +/** + * Interface to be implemented on a builder that can be used to verify all required fields being + * set. + */ +interface RequiredFieldsChecker { + + ImmutableList requiredFields(); + + default ImmutableList getMissingRequiredFieldNames() { + return requiredFields().stream() + .filter(entry -> !entry.fieldValue().get().isPresent()) + .map(RequiredField::displayName) + .collect(toImmutableList()); + } + + @AutoValue + abstract class RequiredField { + abstract String displayName(); + + abstract Supplier> fieldValue(); + + static RequiredField of(String displayName, Supplier> fieldValue) { + return new AutoValue_RequiredFieldsChecker_RequiredField(displayName, fieldValue); + } + } +} diff --git a/policy/src/main/java/dev/cel/policy/RuleComposer.java b/policy/src/main/java/dev/cel/policy/RuleComposer.java new file mode 100644 index 000000000..73d31a4ee --- /dev/null +++ b/policy/src/main/java/dev/cel/policy/RuleComposer.java @@ -0,0 +1,382 @@ +// 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. + +package dev.cel.policy; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.ImmutableList.toImmutableList; +import static java.util.stream.Collectors.toCollection; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import dev.cel.bundle.Cel; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelMutableAst; +import dev.cel.common.CelMutableSource; +import dev.cel.common.CelValidationException; +import dev.cel.common.Operator; +import dev.cel.common.ast.CelConstant; +import dev.cel.common.ast.CelExpr.ExprKind.Kind; +import dev.cel.common.ast.CelMutableExpr; +import dev.cel.common.formats.ValueString; +import dev.cel.common.navigation.CelNavigableMutableAst; +import dev.cel.common.navigation.CelNavigableMutableExpr; +import dev.cel.common.types.CelType; +import dev.cel.common.types.CelTypes; +import dev.cel.extensions.CelOptionalLibrary.Function; +import dev.cel.optimizer.AstMutator; +import dev.cel.optimizer.CelAstOptimizer; +import dev.cel.policy.CelCompiledRule.CelCompiledMatch; +import dev.cel.policy.CelCompiledRule.CelCompiledMatch.OutputValue; +import dev.cel.policy.CelCompiledRule.CelCompiledMatch.Result; +import dev.cel.policy.CelCompiledRule.CelCompiledVariable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** Package-private class for composing various rules into a single expression using optimizer. */ +final class RuleComposer implements CelAstOptimizer { + private final CelCompiledRule compiledRule; + private final String variablePrefix; + private final AstMutator astMutator; + + @Override + public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel) { + Step result = optimizeRule(cel, compiledRule); + return OptimizationResult.create(result.expr.toParsedAst()); + } + + private Step optimizeRule(Cel cel, CelCompiledRule compiledRule) { + cel = + cel.toCelBuilder() + .addVarDeclarations( + compiledRule.variables().stream() + .map(CelCompiledVariable::celVarDecl) + .collect(toImmutableList())) + .build(); + + Step output = null; + // If the rule has an optional output, the last result in the ternary should return + // `optional.none`. This output is implicit and created here to reflect the desired + // last possible output of this type of rule. + if (compiledRule.hasOptionalOutput()) { + output = + Step.newUnconditionalOptionalStep( + newTrueLiteral(), astMutator.newGlobalCall(Function.OPTIONAL_NONE.getFunction())); + } + + long lastOutputId = 0; + // The expected output type of the rule, used to verify that all branches agree on the type. + CelType lastOutputType = null; + for (CelCompiledMatch match : Lists.reverse(compiledRule.matches())) { + CelAbstractSyntaxTree conditionAst = match.condition(); + boolean isTriviallyTrue = match.isConditionTriviallyTrue(); + CelMutableAst condAst = CelMutableAst.fromCelAst(conditionAst); + + long currentSourceId = lastOutputId; + + switch (match.result().kind()) { + case OUTPUT: + // If the match has an output, then it is considered a non-optional output since + // it is explicitly stated. If the rule itself is optional, then the base case value + // of output being optional.none() will convert the non-optional value to an optional + // one. + OutputValue matchOutput = match.result().output(); + Step step = + Step.newNonOptionalStep( + !isTriviallyTrue, condAst, CelMutableAst.fromCelAst(matchOutput.ast())); + currentSourceId = matchOutput.sourceId(); + + output = combine(astMutator, step, output); + + String outputFailureMessage = + String.format( + "incompatible output types: block has output type %s, but previous outputs have" + + " type %s", + lastOutputType == null ? "" : CelTypes.format(lastOutputType), + CelTypes.format(matchOutput.ast().getResultType())); + lastOutputType = + assertComposedAstIsValid( + cel, output.expr, outputFailureMessage, currentSourceId, lastOutputId) + .getResultType(); + + break; + case RULE: + // If the match has a nested rule, then compute the rule and whether it has + // an optional return value. + CelCompiledRule matchNestedRule = match.result().rule(); + Step nestedRule = optimizeRule(cel, matchNestedRule); + Step ruleStep = + new Step( + matchNestedRule.hasOptionalOutput(), !isTriviallyTrue, condAst, nestedRule.expr); + currentSourceId = getFirstOutputSourceId(matchNestedRule); + + output = combine(astMutator, ruleStep, output); + + lastOutputType = + assertComposedAstIsValid( + cel, + output.expr, + String.format( + "failed composing the subrule '%s' due to incompatible output types.", + matchNestedRule.ruleId().map(ValueString::value).orElse("")), + currentSourceId, + lastOutputId) + .getResultType(); + break; + } + + 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); + } + + static RuleComposer newInstance( + CelCompiledRule compiledRule, String variablePrefix, int iterationLimit) { + return new RuleComposer(compiledRule, variablePrefix, iterationLimit); + } + + // Assembles two output expressions into a single output step. + private Step combine(AstMutator astMutator, Step currentStep, Step accumulatedStep) { + if (accumulatedStep == null) { + return currentStep; + } + CelMutableAst trueCondition = newTrueLiteral(); + + if (currentStep.isOptional) { + return combineWhenCurrentIsOptional(currentStep, accumulatedStep, astMutator, trueCondition); + } else { + return combineWhenCurrentIsNonOptional( + currentStep, accumulatedStep, astMutator, trueCondition); + } + } + + private Step combineWhenCurrentIsOptional( + Step currentStep, Step accumulatedStep, AstMutator astMutator, CelMutableAst trueCondition) { + // optional.combine(optional) // optional + // (optional && conditional).combine(non-optional) // optional + // (optional && unconditional).combine(non-optional) // non-optional + if (accumulatedStep.isOptional) { + if (currentStep.isConditional) { + return Step.newUnconditionalOptionalStep( + trueCondition, + astMutator.newGlobalCall( + Operator.CONDITIONAL.getFunction(), + currentStep.cond, + currentStep.expr, + accumulatedStep.expr)); + } else { + if (!isOptionalNone(accumulatedStep.expr)) { + // If either the nested rule or current condition output are optional then + // use optional.or() to specify the combination of the first and second results + // Note, the argument order is reversed due to the traversal of matches in + // reverse order. + return Step.newUnconditionalOptionalStep( + trueCondition, + astMutator.newMemberCall(currentStep.expr, "or", accumulatedStep.expr)); + } + return currentStep; + } + } else { // accumulatedStep is non-optional + if (currentStep.isConditional) { + return Step.newUnconditionalOptionalStep( + trueCondition, + astMutator.newGlobalCall( + Operator.CONDITIONAL.getFunction(), + currentStep.cond, + currentStep.expr, + astMutator.newGlobalCall( + Function.OPTIONAL_OF.getFunction(), accumulatedStep.expr))); + } else { + return Step.newUnconditionalNonOptionalStep( + trueCondition, + astMutator.newMemberCall(currentStep.expr, "orValue", accumulatedStep.expr)); + } + } + } + + private Step combineWhenCurrentIsNonOptional( + Step currentStep, Step accumulatedStep, AstMutator astMutator, CelMutableAst trueCondition) { + // non-optional.combine(non-optional) // non-optional + // (non-optional && conditional).combine(optional) // optional + // (non-optional && unconditional).combine(optional) // non-optional + // + // The last combination case is unusual, but effectively it means that the non-optional value + // prunes away + // the potential optional output. + if (accumulatedStep.isOptional) { + if (currentStep.isConditional) { + return Step.newUnconditionalOptionalStep( + trueCondition, + astMutator.newGlobalCall( + Operator.CONDITIONAL.getFunction(), + currentStep.cond, + astMutator.newGlobalCall(Function.OPTIONAL_OF.getFunction(), currentStep.expr), + accumulatedStep.expr)); + } else { + // If the condition is trivially true, none of the matches in the rule causes the result + // to become optional, and the rule is not the last match, then this will introduce + // unreachable outputs or rules (pruning away 'accumulatedStep'). + return currentStep; + } + } else { // accumulatedStep is non-optional + return Step.newUnconditionalNonOptionalStep( + trueCondition, + astMutator.newGlobalCall( + Operator.CONDITIONAL.getFunction(), + currentStep.cond, + currentStep.expr, + accumulatedStep.expr)); + } + } + + 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( + CelMutableAst ast, List compiledVariables) { + CelMutableAst mutatedAst = ast; + for (CelCompiledVariable compiledVariable : Lists.reverse(compiledVariables)) { + String variableName = variablePrefix + compiledVariable.name(); + ImmutableList exprsToReplace = + CelNavigableMutableAst.fromAst(mutatedAst) + .getRoot() + .allNodes() + .filter( + node -> + node.expr().getKind().equals(Kind.IDENT) + && node.expr().ident().name().equals(variableName)) + .collect(toImmutableList()); + + for (CelNavigableMutableExpr expr : exprsToReplace) { + CelMutableAst variableAst = CelMutableAst.fromCelAst(compiledVariable.ast()); + mutatedAst = astMutator.replaceSubtree(mutatedAst, variableAst, expr.id()); + } + } + + return mutatedAst; + } + + private CelAbstractSyntaxTree assertComposedAstIsValid( + Cel cel, CelMutableAst composedAst, String failureMessage, Long... ids) { + return assertComposedAstIsValid(cel, composedAst, failureMessage, Arrays.asList(ids)); + } + + private CelAbstractSyntaxTree assertComposedAstIsValid( + Cel cel, CelMutableAst composedAst, String failureMessage, List ids) { + try { + return cel.check(composedAst.toParsedAst()).getAst(); + } catch (CelValidationException e) { + ids = ids.stream().filter(id -> id > 0).collect(toCollection(ArrayList::new)); + throw new RuleCompositionException(failureMessage, e, ids); + } + } + + private static long getFirstOutputSourceId(CelCompiledRule rule) { + for (CelCompiledMatch match : rule.matches()) { + if (match.result().kind() == Result.Kind.OUTPUT) { + return match.result().output().sourceId(); + } else if (match.result().kind() == Result.Kind.RULE) { + return getFirstOutputSourceId(match.result().rule()); + } + } + + // Fallback to the nested rule ID if the policy is invalid and contains no output + return rule.sourceId(); + } + + // Step represents an intermediate stage of rule and match expression composition. + // + // The CelCompiledRule and CelCompiledMatch types are meant to represent standalone tuples of + // 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 { + final String failureReason; + final List errorIds; + final CelValidationException compileException; + + private RuleCompositionException( + String failureReason, CelValidationException e, List errorIds) { + super(e); + this.failureReason = failureReason; + this.errorIds = errorIds; + 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 new file mode 100644 index 000000000..5157e0c74 --- /dev/null +++ b/policy/src/test/java/dev/cel/policy/BUILD.bazel @@ -0,0 +1,65 @@ +load("@rules_java//java:defs.bzl", "java_library") +load("//:testing.bzl", "junit4_test_suites") + +package( + default_applicable_licenses = ["//:license"], +) + +java_library( + name = "tests", + testonly = True, + srcs = glob( + ["*.java"], + ), + data = [ + "@cel_policy//conformance:testdata", + ], + resources = [ + "//testing:policy_test_resources", + ], + deps = [ + "//:java_truth", + "//bundle:cel", + "//bundle:environment", + "//bundle:environment_yaml_parser", + "//common:cel_ast", + "//common:options", + "//common/formats:value_string", + "//common/internal", + "//common/resources/testdata/proto3:standalone_global_enum_java_proto", + "//common/types", + "//compiler", + "//extensions:optional_library", + "//parser:macro", + "//parser:parser_factory", + "//parser:unparser", + "//policy", + "//policy:compiled_rule", + "//policy:compiler_factory", + "//policy:parser", + "//policy:parser_factory", + "//policy:rule_composer", + "//policy:source", + "//policy:validation_exception", + "//policy/testing:k8s_test_tag_handler", + "//runtime", + "//runtime:function_binding", + "//testing:cel_runtime_flavor", + "//testing/protos:single_file_java_proto", + "@bazel_tools//tools/java/runfiles", + "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", + "@maven//:com_google_guava_guava", + "@maven//:com_google_testparameterinjector_test_parameter_injector", + "@maven//:junit_junit", + "@maven//:org_yaml_snakeyaml", + ], +) + +junit4_test_suites( + name = "test_suites", + sizes = [ + "small", + ], + src_dir = "src/test/java", + deps = [":tests"], +) diff --git a/policy/src/test/java/dev/cel/policy/CelPolicyCompilerFactoryTest.java b/policy/src/test/java/dev/cel/policy/CelPolicyCompilerFactoryTest.java new file mode 100644 index 000000000..1f323256b --- /dev/null +++ b/policy/src/test/java/dev/cel/policy/CelPolicyCompilerFactoryTest.java @@ -0,0 +1,47 @@ +// 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. + +package dev.cel.policy; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.compiler.CelCompilerFactory; +import dev.cel.parser.CelParserFactory; +import dev.cel.runtime.CelRuntimeFactory; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public final class CelPolicyCompilerFactoryTest { + + @Test + public void newPolicyCompiler_compilerRuntimeCombined() { + assertThat( + CelPolicyCompilerFactory.newPolicyCompiler( + CelCompilerFactory.standardCelCompilerBuilder().build(), + CelRuntimeFactory.standardCelRuntimeBuilder().build())) + .isNotNull(); + } + + @Test + public void newPolicyCompiler_parserCheckerRuntimeCombined() { + assertThat( + CelPolicyCompilerFactory.newPolicyCompiler( + CelParserFactory.standardCelParserBuilder().build(), + CelCompilerFactory.standardCelCheckerBuilder().build(), + CelRuntimeFactory.standardCelRuntimeBuilder().build())) + .isNotNull(); + } +} diff --git a/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java b/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java new file mode 100644 index 000000000..73950069f --- /dev/null +++ b/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java @@ -0,0 +1,588 @@ +// 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. + +package dev.cel.policy; + +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.common.CelAbstractSyntaxTree; +import dev.cel.common.CelOptions; +import dev.cel.common.formats.ValueString; +import dev.cel.common.types.OptionalType; +import dev.cel.common.types.SimpleType; +import dev.cel.expr.conformance.proto3.TestAllTypes; +import dev.cel.extensions.CelOptionalLibrary; +import dev.cel.parser.CelStandardMacro; +import dev.cel.parser.CelUnparserFactory; +import dev.cel.policy.PolicyTestHelper.PolicyTestSuite; +import dev.cel.policy.PolicyTestHelper.PolicyTestSuite.PolicyTestSection; +import dev.cel.policy.PolicyTestHelper.PolicyTestSuite.PolicyTestSection.PolicyTestCase; +import dev.cel.policy.PolicyTestHelper.PolicyTestSuite.PolicyTestSection.PolicyTestCase.PolicyTestInput; +import dev.cel.policy.PolicyTestHelper.TestYamlPolicy; +import dev.cel.policy.testing.K8sTagHandler; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.CelLateFunctionBindings; +import dev.cel.testing.CelRuntimeFlavor; +import dev.cel.testing.testdata.SingleFile; +import dev.cel.testing.testdata.proto3.StandaloneGlobalEnum; +import java.io.IOException; +import java.net.URL; +import java.util.Map; +import java.util.Optional; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public final class CelPolicyCompilerImplTest { + + private static final CelPolicyParser POLICY_PARSER = + CelPolicyParserFactory.newYamlParserBuilder().addTagVisitor(new K8sTagHandler()).build(); + private static final CelEnvironmentYamlParser ENVIRONMENT_PARSER = + CelEnvironmentYamlParser.newInstance(); + private static final CelOptions CEL_OPTIONS = + CelOptions.current() + .populateMacroCalls(true) + .enableHeterogeneousNumericComparisons(true) + .build(); + + @TestParameter private CelRuntimeFlavor runtimeFlavor; + + @Test + public void compileYamlPolicy_success(@TestParameter TestYamlPolicy yamlPolicy) throws Exception { + // Read config and produce an environment to compile policies + String configSource = yamlPolicy.readConfigYamlContent(); + CelEnvironment celEnvironment = ENVIRONMENT_PARSER.parse(configSource); + Cel cel = celEnvironment.extend(newCel(), CEL_OPTIONS); + // Read the policy source + String policySource = yamlPolicy.readPolicyYamlContent(); + CelPolicy policy = POLICY_PARSER.parse(policySource); + + CelAbstractSyntaxTree ast = + CelPolicyCompilerFactory.newPolicyCompiler(cel).build().compile(policy); + + assertThat(CelUnparserFactory.newUnparser().unparse(ast)).isEqualTo(yamlPolicy.getUnparsed()); + } + + @Test + public void compileYamlPolicy_withImportsOnNestedRules() throws Exception { + String policySource = + "imports:\n" + + " - name: cel.expr.conformance.proto3.TestAllTypes\n" + + " - name: dev.cel.testing.testdata.SingleFile\n" + + "rule:\n" + + " match:\n" + + " - rule:\n" + + " id: 'nested rule with imports'\n" + + " match:\n" + + " - condition: 'TestAllTypes{}.single_string == SingleFile{}.name'\n" + + " output: 'true'\n"; + Cel cel = newCel(); + CelPolicy policy = POLICY_PARSER.parse(policySource); + + CelAbstractSyntaxTree ast = + CelPolicyCompilerFactory.newPolicyCompiler(cel).build().compile(policy); + + assertThat(ast.getResultType()).isEqualTo(OptionalType.create(SimpleType.BOOL)); + } + + @Test + public void compileYamlPolicy_nestedRuleOptionalFallbackDivergence() throws Exception { + Cel cel = newCel().toCelBuilder().addVar("input_val", SimpleType.INT).build(); + String policySource = + "name: grandparent_policy\n" + + "rule:\n" + + " match:\n" + + " - condition: 'input_val == 2'\n" // conditional grandparent match + + " rule:\n" + + " id: parent_rule\n" + + " match:\n" + + " - condition: 'input_val == 1'\n" // conditional parent match + + " rule:\n" + + " id: nested_rule\n" + + " match:\n" + + " - condition: 'input_val == 3'\n" + + " output: 'true'\n" + + " - output: 'true'\n" // fallback (optional) + + " - output: 'true'\n"; + CelPolicy policy = POLICY_PARSER.parse(policySource); + CelAbstractSyntaxTree ast = + CelPolicyCompilerFactory.newPolicyCompiler(cel).build().compile(policy); + + assertThat(ast.getResultType()).isEqualTo(OptionalType.create(SimpleType.BOOL)); + } + + @Test + public void compileYamlPolicy_containsCompilationError_throws( + @TestParameter TestErrorYamlPolicy testCase) throws Exception { + // Read config and produce an environment to compile policies + 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()); + + CelPolicyValidationException e = + assertThrows( + CelPolicyValidationException.class, + () -> CelPolicyCompilerFactory.newPolicyCompiler(cel).build().compile(policy)); + + assertThat(e).hasMessageThat().isEqualTo(testCase.readExpectedErrorsBaseline()); + } + + @Test + public void compileYamlPolicy_multilineContainsError_throws( + @TestParameter MultilineErrorTest testCase) throws Exception { + String policyContent = testCase.yaml; + CelPolicy policy = POLICY_PARSER.parse(policyContent); + + CelPolicyValidationException e = + assertThrows( + CelPolicyValidationException.class, + () -> CelPolicyCompilerFactory.newPolicyCompiler(newCel()).build().compile(policy)); + + assertThat(e).hasMessageThat().isEqualTo(testCase.expected); + } + + @Test + public void compileYamlPolicy_exceedsDefaultAstDepthLimit_throws() throws Exception { + Cel cel = newCel().toCelBuilder().addVar("msg", SimpleType.DYN).build(); + String longExpr = "msg.b.c.d.e.f"; + String policyContent = + String.format( + "name: deeply_nested_ast\n" + "rule:\n" + " match:\n" + " - output: %s", longExpr); + CelPolicy policy = POLICY_PARSER.parse(policyContent); + + CelPolicyValidationException e = + assertThrows( + CelPolicyValidationException.class, + () -> + CelPolicyCompilerFactory.newPolicyCompiler(cel) + .setAstDepthLimit(5) + .build() + .compile(policy)); + + assertThat(e) + .hasMessageThat() + .isEqualTo("ERROR: :-1:0: AST's depth exceeds the configured limit: 5."); + } + + @Test + public void compileYamlPolicy_constantFoldingFailure_throwsDuringComposition() throws Exception { + String policyContent = + "name: ast_with_div_by_zero\n" // + + "rule:\n" // + + " match:\n" // + + " - output: 1 / 0"; + CelPolicy policy = POLICY_PARSER.parse(policyContent); + + CelPolicyValidationException e = + assertThrows( + CelPolicyValidationException.class, + () -> CelPolicyCompilerFactory.newPolicyCompiler(newCel()).build().compile(policy)); + assertThat(e) + .hasMessageThat() + .isEqualTo( + "Failed to optimize the composed policy. Reason: Constant folding failure. Failed to" + + " evaluate subtree due to: evaluation error: / by zero"); + } + + @Test + public void compileYamlPolicy_astDepthLimitCheckDisabled_doesNotThrow() throws Exception { + String longExpr = + "0+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+35+36+37+38+39+40+41+42+43+44+45+46+47+48+49+50"; + String policyContent = + String.format( + "name: deeply_nested_ast\n" + "rule:\n" + " match:\n" + " - output: %s", longExpr); + CelPolicy policy = POLICY_PARSER.parse(policyContent); + + CelAbstractSyntaxTree ast = + CelPolicyCompilerFactory.newPolicyCompiler(newCel()) + .setAstDepthLimit(-1) + .build() + .compile(policy); + assertThat(ast).isNotNull(); + } + + @Test + @SuppressWarnings("unchecked") + public void evaluateYamlPolicy_withCanonicalTestData( + @TestParameter(valuesProvider = EvaluablePolicyTestDataProvider.class) + EvaluablePolicyTestData testData) + throws Exception { + // Setup + // Read config and produce an environment to compile policies + String configSource = testData.yamlPolicy.readConfigYamlContent(); + CelEnvironment celEnvironment = ENVIRONMENT_PARSER.parse(configSource); + Cel cel = celEnvironment.extend(newCel(), CEL_OPTIONS); + // Read the policy source + String policySource = testData.yamlPolicy.readPolicyYamlContent(); + CelPolicy policy = POLICY_PARSER.parse(policySource); + 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 + // Compile then evaluate the policy + CelAbstractSyntaxTree compiledPolicyAst = + CelPolicyCompilerFactory.newPolicyCompiler(cel).build().compile(policy); + ImmutableMap.Builder inputBuilder = ImmutableMap.builder(); + for (Map.Entry entry : testData.testCase.getInput().entrySet()) { + String exprInput = entry.getValue().getExpr(); + if (isNullOrEmpty(exprInput)) { + inputBuilder.put(entry.getKey(), entry.getValue().getValue()); + } else { + CelAbstractSyntaxTree exprInputAst = cel.compile(exprInput).getAst(); + inputBuilder.put(entry.getKey(), cel.createProgram(exprInputAst).eval()); + } + } + Object evalResult = cel.createProgram(compiledPolicyAst).eval(inputBuilder.buildOrThrow()); + + // Assert + // Note that policies may either produce an optional or a non-optional result, + // if all the rules included nested ones can always produce a default result when none of the + // condition matches + if (testData.yamlPolicy.producesOptionalResult()) { + Optional policyOutput = (Optional) evalResult; + if (policyOutput.isPresent()) { + assertThat(policyOutput).hasValue(expectedOutput); + } else { + assertThat(policyOutput).isEmpty(); + } + } else { + assertThat(evalResult).isEqualTo(expectedOutput); + } + } + + @Test + @SuppressWarnings("unchecked") + public void evaluateYamlPolicy_nestedRuleProducesOptionalOutput() throws Exception { + Cel cel = newCel(); + String policySource = + "name: nested_rule_with_optional_result\n" + + "rule:\n" + + " match:\n" + + " - rule:\n" + + " match:\n" + + " - condition: 'true'\n" + + " output: 'optional.of(true)'\n"; + CelPolicy policy = POLICY_PARSER.parse(policySource); + CelAbstractSyntaxTree compiledPolicyAst = + CelPolicyCompilerFactory.newPolicyCompiler(cel).build().compile(policy); + Optional evalResult = (Optional) cel.createProgram(compiledPolicyAst).eval(); + + // Result is Optional containing true + assertThat(evalResult).hasValue(true); + } + + @Test + public void evaluateYamlPolicy_lateBoundFunction() throws Exception { + String configSource = + "name: late_bound_function_config\n" + + "functions:\n" + + " - name: 'lateBoundFunc'\n" + + " overloads:\n" + + " - id: 'lateBoundFunc_string'\n" + + " args:\n" + + " - type_name: 'string'\n" + + " return:\n" + + " type_name: 'string'\n"; + CelEnvironment celEnvironment = ENVIRONMENT_PARSER.parse(configSource); + 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" + + " match:\n" + + " - output: |\n" + + " lateBoundFunc('foo')\n"; + CelPolicy policy = POLICY_PARSER.parse(policySource); + CelAbstractSyntaxTree compiledPolicyAst = + CelPolicyCompilerFactory.newPolicyCompiler(cel).build().compile(policy); + String exampleValue = "bar"; + CelLateFunctionBindings lateFunctionBindings = + CelLateFunctionBindings.from( + CelFunctionBinding.from( + "lateBoundFunc_string", String.class, arg -> arg + exampleValue)); + + String evalResult = + (String) + cel.createProgram(compiledPolicyAst) + .eval((unused) -> Optional.empty(), lateFunctionBindings); + assertThat(evalResult).isEqualTo("foo" + exampleValue); + } + + @Test + public void evaluateYamlPolicy_withSimpleVariable() throws Exception { + Cel cel = newCel(); + String policySource = + "name: shorthand_variables_policy\n" + + "rule:\n" + + " variables:\n" + + " - first: 'true'\n" + + " - second: 'false'\n" + + " match:\n" + + " - output: 'variables.first && variables.second'"; + CelPolicyParser parser = + CelPolicyParserFactory.newYamlParserBuilder().enableSimpleVariables(true).build(); + CelPolicy policy = parser.parse(policySource); + + 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); + 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; + + private EvaluablePolicyTestData(TestYamlPolicy yamlPolicy, PolicyTestCase testCase) { + this.yamlPolicy = yamlPolicy; + this.testCase = testCase; + } + } + + private static final class EvaluablePolicyTestDataProvider extends TestParameterValuesProvider { + + @Override + protected ImmutableList provideValues(Context context) throws Exception { + ImmutableList.Builder builder = ImmutableList.builder(); + for (TestYamlPolicy yamlPolicy : TestYamlPolicy.values()) { + PolicyTestSuite testSuite = yamlPolicy.readTestYamlContent(); + for (PolicyTestSection testSection : testSuite.getSection()) { + for (PolicyTestCase testCase : testSection.getTests()) { + String testName = + String.format( + "%s %s %s", + yamlPolicy.getPolicyName(), testSection.getName(), testCase.getName()); + builder.add( + value(new EvaluablePolicyTestData(yamlPolicy, testCase)).withName(testName)); + } + } + } + + return builder.build(); + } + } + + private Cel newCel() { + return runtimeFlavor + .builder() + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addCompilerLibraries(CelOptionalLibrary.INSTANCE) + .addRuntimeLibraries(CelOptionalLibrary.INSTANCE) + .addFileTypes(StandaloneGlobalEnum.getDescriptor().getFile()) + .addMessageTypes(TestAllTypes.getDescriptor(), SingleFile.getDescriptor()) + .setOptions(CEL_OPTIONS) + .addFunctionBindings( + CelFunctionBinding.fromOverloads( + "locationCode", + CelFunctionBinding.from( + "locationCode_string", + String.class, + (ip) -> { + switch (ip) { + case "10.0.0.1": + return "us"; + case "10.0.0.2": + return "de"; + default: + return "ir"; + } + }))) + .build(); + } + + private enum MultilineErrorTest { + SINGLE_FOLDED( + "name: \"errors\"\n" + + "rule:\n" + + " match:\n" + + " - output: >\n" + + " 'test'.format(variables.missing])", + "ERROR: :5:40: extraneous input ']' expecting ')'\n" + + " | 'test'.format(variables.missing])\n" + + " | .......................................^"), + DOUBLE_FOLDED( + "name: \"errors\"\n" + + "rule:\n" + + " match:\n" + + " - output: >\n" + + " 'test'.format(\n" + + " variables.missing])", + "ERROR: :6:26: extraneous input ']' expecting ')'\n" + + " | variables.missing])\n" + + " | .........................^"), + TRIPLE_FOLDED( + "name: \"errors\"\n" + + "rule:\n" + + " match:\n" + + " - output: >\n" + + " 'test'.\n" + + " format(\n" + + " variables.missing])", + "ERROR: :7:26: extraneous input ']' expecting ')'\n" + + " | variables.missing])\n" + + " | .........................^"), + SINGLE_LITERAL( + "name: \"errors\"\n" + + "rule:\n" + + " match:\n" + + " - output: |\n" + + " 'test'.format(variables.missing])", + "ERROR: :5:40: extraneous input ']' expecting ')'\n" + + " | 'test'.format(variables.missing])\n" + + " | .......................................^"), + DOUBLE_LITERAL( + "name: \"errors\"\n" + + "rule:\n" + + " match:\n" + + " - output: |\n" + + " 'test'.format(\n" + + " variables.missing])", + "ERROR: :6:26: extraneous input ']' expecting ')'\n" + + " | variables.missing])\n" + + " | .........................^"), + TRIPLE_LITERAL( + "name: \"errors\"\n" + + "rule:\n" + + " match:\n" + + " - output: |\n" + + " 'test'.\n" + + " format(\n" + + " variables.missing])", + "ERROR: :7:26: extraneous input ']' expecting ')'\n" + + " | variables.missing])\n" + + " | .........................^"), + ; + + private final String yaml; + private final String expected; + + MultilineErrorTest(String yaml, String expected) { + this.yaml = yaml; + this.expected = expected; + } + } + + private enum TestErrorYamlPolicy { + COMPOSE_ERRORS_CONFLICTING_OUTPUT("compose_conflicting_output"), + COMPOSE_ERRORS_CONFLICTING_SUBRULE("compose_conflicting_subrule"), + ERRORS_UNREACHABLE("unreachable"), + DUPLICATE_VARIABLE("duplicate_variable"), + IMPORT("import"), + INCOMPATIBLE_OUTPUTS("incompatible_outputs"), + SYNTAX("syntax"), + UNDECLARED_REFERENCE("undeclared_reference"); + + private final String name; + private final String policyFilePath; + + private String getPolicyFilePath() { + return policyFilePath; + } + + private String readPolicyYamlContent() throws IOException { + return readFromYaml( + String.format( + "cel_policy/conformance/testdata/compile_errors/%s/policy.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 { + URL url = Resources.getResource(String.format("policy/%s/expected_errors.baseline", name)); + return Resources.toString(url, UTF_8).trim(); + } + + TestErrorYamlPolicy(String name) { + this.name = name; + this.policyFilePath = String.format("%s/policy.yaml", name); + } + } +} diff --git a/policy/src/test/java/dev/cel/policy/CelPolicySourceTest.java b/policy/src/test/java/dev/cel/policy/CelPolicySourceTest.java new file mode 100644 index 000000000..9aa73ee6c --- /dev/null +++ b/policy/src/test/java/dev/cel/policy/CelPolicySourceTest.java @@ -0,0 +1,54 @@ +// 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. + +package dev.cel.policy; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.common.internal.CelCodePointArray; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public final class CelPolicySourceTest { + + @Test + public void constructPolicySource_success() { + CelPolicySource policySource = + CelPolicySource.newBuilder(CelCodePointArray.fromString("hello world")) + .setDescription("") + .build(); + + assertThat(policySource.getContent().toString()).isEqualTo("hello world"); + assertThat(policySource.getDescription()).isEqualTo(""); + } + + @Test + public void getSnippet_success() { + CelPolicySource policySource = + CelPolicySource.newBuilder(CelCodePointArray.fromString("hello\nworld")).build(); + + assertThat(policySource.getSnippet(1)).hasValue("hello"); + assertThat(policySource.getSnippet(2)).hasValue("world"); + } + + @Test + public void getSnippet_returnsEmpty() { + CelPolicySource policySource = + CelPolicySource.newBuilder(CelCodePointArray.fromString("hello\nworld")).build(); + + assertThat(policySource.getSnippet(3)).isEmpty(); + } +} diff --git a/policy/src/test/java/dev/cel/policy/CelPolicyYamlParserTest.java b/policy/src/test/java/dev/cel/policy/CelPolicyYamlParserTest.java new file mode 100644 index 000000000..2a2c47a98 --- /dev/null +++ b/policy/src/test/java/dev/cel/policy/CelPolicyYamlParserTest.java @@ -0,0 +1,413 @@ +// 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. + +package dev.cel.policy; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.Iterables; +import com.google.testing.junit.testparameterinjector.TestParameter; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.common.formats.ValueString; +import dev.cel.policy.CelPolicy.Import; +import dev.cel.policy.PolicyTestHelper.TestYamlPolicy; +import dev.cel.policy.testing.K8sTagHandler; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public final class CelPolicyYamlParserTest { + + private static final CelPolicyParser POLICY_PARSER = + CelPolicyParserFactory.newYamlParserBuilder().addTagVisitor(new K8sTagHandler()).build(); + + @Test + public void parseYamlPolicy_success(@TestParameter TestYamlPolicy yamlPolicy) throws Exception { + String policySource = yamlPolicy.readPolicyYamlContent(); + String description = yamlPolicy.getPolicyName(); + + CelPolicy policy = POLICY_PARSER.parse(policySource, description); + + assertThat(policy.name().value()).isEqualTo(yamlPolicy.getPolicyName()); + assertThat(policy.policySource().getContent().toString()).isEqualTo(policySource); + assertThat(policy.policySource().getDescription()).isEqualTo(description); + } + + @Test + public void parser_setEmpty() throws Exception { + assertThrows(CelPolicyValidationException.class, () -> POLICY_PARSER.parse("", "")); + } + + @Test + public void parseYamlPolicy_withDescription_atPolicyLevel() throws Exception { + String policySource = + "name: 'policy_with_description'\n" + + "description: 'this is a description of the policy'\n" + + "rule:\n" + + " variables:\n" + + " - name: 'variable_with_description'\n" + + " description: 'this is a description of the variable'\n" + + " expression: 'true'"; + + CelPolicy policy = POLICY_PARSER.parse(policySource); + + assertThat(policy.description()) + .hasValue(ValueString.of(5, "this is a description of the policy")); + } + + @Test + public void parseYamlPolicy_withDisplayName_atPolicyLevel() throws Exception { + String policySource = + "name: 'policy_with_description'\n" + + "display_name: 'display name of the policy'\n" + + "rule:\n" + + " variables:\n" + + " - name: 'variable_with_description'\n" + + " description: 'this is a description of the variable'\n" + + " expression: 'true'"; + + CelPolicy policy = POLICY_PARSER.parse(policySource); + + assertThat(policy.displayName()).hasValue(ValueString.of(5, "display name of the policy")); + } + + @Test + public void parseYamlPolicy_withDescription() throws Exception { + String policySource = + "rule:\n" + + " variables:\n" + + " - name: 'variable_with_description'\n" + + " description: 'this is a description of the variable'\n" + + " expression: 'true'"; + + CelPolicy policy = POLICY_PARSER.parse(policySource); + + assertThat(policy.rule().variables()).hasSize(1); + assertThat(Iterables.getOnlyElement(policy.rule().variables()).description()) + .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 = + "rule:\n" + + " variables:\n" + + " - name: 'variable_with_description'\n" + + " display_name: 'Display Name'\n" + + " expression: 'true'"; + + CelPolicy policy = POLICY_PARSER.parse(policySource); + + assertThat(policy.rule().variables()).hasSize(1); + assertThat(Iterables.getOnlyElement(policy.rule().variables()).displayName()) + .hasValue(ValueString.of(10, "Display Name")); + } + + @Test + public void parseYamlPolicy_withExplanation() throws Exception { + String policySource = + "rule:\n" + + " match:\n" + + " - output: 'true'\n" + + " explanation: \"'custom explanation'\""; + + CelPolicy policy = POLICY_PARSER.parse(policySource); + + assertThat(policy.rule().matches()).hasSize(1); + assertThat(Iterables.getOnlyElement(policy.rule().matches()).explanation()) + .hasValue(ValueString.of(11, "'custom explanation'")); + } + + @Test + public void parseYamlPolicy_withImports() throws Exception { + String policySource = + "name: 'policy_with_imports'\n" + + "imports:\n" + + "- name: foo\n" + + "- name: >\n" + + " bar"; + + CelPolicy policy = POLICY_PARSER.parse(policySource); + + assertThat(policy.imports()) + .containsExactly( + Import.create(8L, ValueString.of(9L, "foo")), + Import.create(12L, ValueString.of(13L, "bar"))) + .inOrder(); + } + + @Test + public void parseYamlPolicy_withSimpleVariable_multipleInlinedVariables() { + String policySource = + "name: shorthand_variables_policy\n" + + "rule:\n" + + " variables:\n" + + " - first: 'true'\n" + + " second: 'false'\n" + + " match:\n" + + " - condition: 'variables.my_var'\n" + + " output: 'true'\n"; + CelPolicyParser parser = + CelPolicyParserFactory.newYamlParserBuilder().enableSimpleVariables(true).build(); + + CelPolicyValidationException e = + assertThrows(CelPolicyValidationException.class, () -> parser.parse(policySource)); + assertThat(e) + .hasMessageThat() + .contains( + "ERROR: :5:7: Only one variable may be defined inline\n" + + " | second: 'false'\n" + + " | ......^"); + } + + @Test + public void parseYamlPolicy_errors(@TestParameter PolicyParseErrorTestCase testCase) { + CelPolicyValidationException e = + assertThrows( + CelPolicyValidationException.class, () -> POLICY_PARSER.parse(testCase.yamlPolicy)); + assertThat(e).hasMessageThat().isEqualTo(testCase.expectedErrorMessage); + } + + private enum PolicyParseErrorTestCase { + MALFORMED_YAML_DOCUMENT( + "a:\na", + "YAML document is malformed: while scanning a simple key\n" + + " in 'reader', line 2, column 1:\n" + + " a\n" + + " ^\n" + + "could not find expected ':'\n" + + " in 'reader', line 2, column 2:\n" + + " a\n" + + " ^\n"), + ILLEGAL_YAML_TYPE_POLICY_KEY( + "1: test", + "ERROR: :1:1: Got yaml node type tag:yaml.org,2002:int, wanted type(s)" + + " [tag:yaml.org,2002:str !txt]\n" + + " | 1: test\n" + + " | ^"), + ILLEGAL_YAML_TYPE_ON_NAME_VALUE( + "name: \n" + " illegal: yaml-type", + "ERROR: :2:3: Got yaml node type tag:yaml.org,2002:map, wanted type(s)" + + " [tag:yaml.org,2002:str !txt]\n" + + " | illegal: yaml-type\n" + + " | ..^"), + ILLEGAL_YAML_TYPE_ON_RULE_VALUE( + "rule: illegal", + "ERROR: :1:7: Got yaml node type tag:yaml.org,2002:str, wanted type(s)" + + " [tag:yaml.org,2002:map]\n" + + " | rule: illegal\n" + + " | ......^"), + ILLEGAL_YAML_TYPE_ON_RULE_MAP_KEY( + "rule: \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_MATCHES_VALUE( + "rule:\n" + " match: illegal\n", + "ERROR: :2:10: Got yaml node type tag:yaml.org,2002:str, wanted type(s)" + + " [tag:yaml.org,2002:seq]\n" + + " | match: illegal\n" + + " | .........^"), + ILLEGAL_YAML_TYPE_ON_MATCHES_LIST( + "rule:\n" + " match:\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_MATCH_MAP_KEY( + "rule:\n" + " match:\n" + " - 1 : foo\n" + " output: 'hi'", + "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" + + " | ......^"), + ILLEGAL_YAML_TYPE_ON_VARIABLE_VALUE( + "rule:\n" + " variables: illegal\n", + "ERROR: :2:14: Got yaml node type tag:yaml.org,2002:str, wanted type(s)" + + " [tag:yaml.org,2002:seq]\n" + + " | variables: illegal\n" + + " | .............^"), + ILLEGAL_YAML_TYPE_ON_VARIABLE_MAP_KEY( + "rule:\n" + " variables:\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" + + " | ......^"), + MULTIPLE_YAML_DOCS( + "name: foo\n" + "---\n" + "name: bar", + "YAML document is malformed: expected a single document in the stream\n" + + " in 'reader', line 1, column 1:\n" + + " name: foo\n" + + " ^\n" + + "but found another document\n" + + " in 'reader', line 2, column 1:\n" + + " ---\n" + + " ^\n"), + UNSUPPORTED_RULE_TAG( + "rule:\n" + " custom: yaml-type", + "ERROR: :2:3: Unsupported rule tag: custom\n" + + " | custom: yaml-type\n" + + " | ..^"), + UNSUPPORTED_POLICY_TAG( + "inputs:\n" + " - name: a\n" + " - name: b", + "ERROR: :1:1: Unsupported policy tag: inputs\n" + " | inputs:\n" + " | ^"), + UNSUPPORTED_VARIABLE_TAG( + "rule:\n" // + + " variables:\n" // + + " - name: 'hello'\n" // + + " expression: 'true'\n" // + + " alt_name: 'bool_true'", + "ERROR: :5:7: Unsupported variable tag: alt_name\n" + + " | alt_name: 'bool_true'\n" + + " | ......^"), + MISSING_VARIABLE_NAME( + "rule:\n" // + + " variables:\n" // + + " - expression: 'true'", // + "ERROR: :3:7: Missing required attribute(s): name\n" + + " | - expression: 'true'\n" + + " | ......^"), + MISSING_VARIABLE_EXPRESSION( + "rule:\n" // + + " variables:\n" // + + " - name: 'hello'", // + "ERROR: :3:7: Missing required attribute(s): expression\n" + + " | - name: 'hello'\n" + + " | ......^"), + UNSUPPORTED_MATCH_TAG( + "rule:\n" + + " match:\n" + + " - name: 'true'\n" + + " output: 'hi'\n" + + " alt_name: 'bool_true'", + "ERROR: :3:7: Unsupported match tag: name\n" + + " | - name: 'true'\n" + + " | ......^\n" + + "ERROR: :5:7: Unsupported match tag: alt_name\n" + + " | alt_name: 'bool_true'\n" + + " | ......^"), + MATCH_MISSING_OUTPUT_AND_RULE( + "rule:\n" // + + " match:\n" // + + " - condition: 'true'", + "ERROR: :3:7: Missing required attribute(s): output or a rule\n" + + " | - condition: 'true'\n" + + " | ......^"), + MATCH_OUTPUT_SET_THEN_RULE( + "rule:\n" + + " match:\n" + + " - condition: \"true\"\n" + + " output: \"world\"\n" + + " rule:\n" + + " match:\n" + + " - output: \"hello\"", + "ERROR: :5:7: Only the rule or the output may be set\n" + + " | rule:\n" + + " | ......^"), + MATCH_RULE_SET_THEN_OUTPUT( + "rule:\n" + + " match:\n" + + " - condition: \"true\"\n" + + " rule:\n" + + " match:\n" + + " - output: \"hello\"\n" + + " output: \"world\"", + "ERROR: :7:7: Only the rule or the output may be set\n" + + " | output: \"world\"\n" + + " | ......^"), + MATCH_NESTED_RULE_SET_THEN_EXPLANATION( + "rule:\n" + + " match:\n" + + " - condition: \"true\"\n" + + " rule:\n" + + " match:\n" + + " - output: \"hello\"\n" + + " explanation: \"foo\"", + "ERROR: :7:7: Explanation can only be set on output match cases, not nested rules\n" + + " | explanation: \"foo\"\n" + + " | ......^"), + MATCH_EXPLANATION_SET_THEN_NESTED_RULE( + "rule:\n" + + " match:\n" + + " - condition: \"true\"\n" + + " explanation: \"foo\"\n" + + " rule:\n" + + " match:\n" + + " - output: \"hello\"\n", + "ERROR: :4:21: Explanation can only be set on output match cases, not nested rules\n" + + " | explanation: \"foo\"\n" + + " | ....................^"), + INVALID_ROOT_NODE_TYPE( + "- rule:\n" + " id: a", + "ERROR: :1:1: Got yaml node type tag:yaml.org,2002:seq, wanted type(s)" + + " [tag:yaml.org,2002:map]\n" + + " | - rule:\n" + + " | ^"), + ILLEGAL_RULE_DESCRIPTION_TYPE( + "rule:\n" + " description: 1", + "ERROR: :2:16: Got yaml node type tag:yaml.org,2002:int, wanted type(s)" + + " [tag:yaml.org,2002:str !txt]\n" + + " | description: 1\n" + + " | ...............^"), + ILLEGAL_YAML_TYPE_IMPORT_EXPECTED_LIST( + "imports: foo", + "ERROR: :1:10: Got yaml node type tag:yaml.org,2002:str, wanted type(s)" + + " [tag:yaml.org,2002:seq]\n" + + " | imports: foo\n" + + " | .........^"), + ILLEGAL_YAML_TYPE_IMPORT_ELEMENT_EXPECTED_MAP( + "imports:\n" // + + "- foo", + "ERROR: :2:3: Got yaml node type tag:yaml.org,2002:str, wanted type(s)" + + " [tag:yaml.org,2002:map]\n" + + " | - foo\n" + + " | ..^"), + ILLEGAL_YAML_TYPE_IMPORT_ELEMENT_MAP_INVALID_KEY( + "imports:\n" // + + "- 1: 2", + "ERROR: :2:3: Got yaml node type tag:yaml.org,2002:int, wanted type(s)" + + " [tag:yaml.org,2002:str !txt]\n" + + " | - 1: 2\n" + + " | ..^"), + ILLEGAL_YAML_TYPE_IMPORT_ELEMENT_MAP_INVALID_VALUE_NAME( + "imports:\n" // + + "- foo: bar", + "ERROR: :2:3: Invalid import key: foo, expected 'name'\n" + + " | - foo: bar\n" + + " | ..^"); + + private final String yamlPolicy; + private final String expectedErrorMessage; + + PolicyParseErrorTestCase(String yamlPolicy, String expectedErrorMessage) { + this.yamlPolicy = yamlPolicy; + this.expectedErrorMessage = expectedErrorMessage; + } + } +} diff --git a/policy/src/test/java/dev/cel/policy/PolicyTestHelper.java b/policy/src/test/java/dev/cel/policy/PolicyTestHelper.java new file mode 100644 index 000000000..3fe2e3322 --- /dev/null +++ b/policy/src/test/java/dev/cel/policy/PolicyTestHelper.java @@ -0,0 +1,312 @@ +// 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. + +package dev.cel.policy; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.annotations.VisibleForTesting; +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; +import java.util.List; +import java.util.Map; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.Constructor; + +/** Package-private class to assist with policy testing. */ +@AutoBazelRepository +final class PolicyTestHelper { + + private static final Runfiles runfiles = createRunfiles(); + + enum TestYamlPolicy { + NESTED_RULE( + "nested_rule", + 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()).orValue(@index1 ? {\"banned\":" + + " false} : @index2))"), + NESTED_RULE2( + "nested_rule2", + false, + "cel.@block([resource.origin, !(@index0 in [\"us\", \"uk\", \"es\"])]," + + " resource.?user.orValue(\"\").startsWith(\"bad\") ? ((@index0 in {\"us\": false," + + " \"ru\": false, \"ir\": false} && @index1) ? {\"banned\": \"restricted_region\"} :" + + " {\"banned\": \"bad_actor\"}) : (@index1 ? {\"banned\": \"unconfigured_region\"} :" + + " {}))"), + NESTED_RULE3( + "nested_rule3", + true, + "cel.@block([resource.origin, !(@index0 in [\"us\", \"uk\", \"es\"])]," + + " resource.?user.orValue(\"\").startsWith(\"bad\") ? optional.of((@index0 in {\"us\":" + + " 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.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, + "cel.@block([request.auth.claims, has(@index0.nationality), resource.labels.location in" + + " spec.restricted_destinations], (@index1 && @index0.nationality == spec.origin &&" + + " (locationCode(destination.ip) in spec.restricted_destinations || @index2)) ? true :" + + " ((!@index1 && locationCode(origin.ip) == spec.origin &&" + + " (locationCode(destination.ip) in spec.restricted_destinations || @index2)) ? true :" + + " false))"), + K8S( + "k8s", + true, + "cel.@block([resource.labels.?environment.orValue(\"prod\")]," + + " !(resource.labels.?break_glass.orValue(\"false\") == \"true\" ||" + + " resource.containers.all(@it:0:0, @it:0:0.startsWith(@index0 + \".\"))) ?" + + " optional.of(\"only \" + @index0 + \" containers are allowed in namespace \" +" + + " resource.namespace) : optional.none())"), + PB( + "pb", + true, + "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 ||" + + " 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, + "cel.@block([now.getHours()], (@index0 >= 20) ? ((@index0 < 21) ? optional.of(\"goodbye," + + " me!\") : ((@index0 < 22) ? optional.of(\"goodbye, me!!\") : ((@index0 < 24) ?" + + " optional.of(\"goodbye, me!!!\") : optional.none()))) : optional.of(\"hello," + + " me\"))"); + + private final String name; + private final boolean producesOptionalResult; + private final String unparsed; + + TestYamlPolicy(String name, boolean producesOptionalResult, String unparsed) { + this.name = name; + this.producesOptionalResult = producesOptionalResult; + this.unparsed = unparsed; + } + + String getPolicyName() { + return name; + } + + boolean producesOptionalResult() { + return this.producesOptionalResult; + } + + String getUnparsed() { + return unparsed; + } + + String readPolicyYamlContent() throws IOException { + return readFromYaml( + String.format( + "cel_policy/conformance/testdata/%s/policy.yaml", name)); + } + + String readConfigYamlContent() throws IOException { + 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( + "cel_policy/conformance/testdata/%s/tests.yaml", name)); + + return yaml.load(testContent); + } + } + + static String readFromYaml(String yamlPath) throws IOException { + return readFile(yamlPath); + } + + /** + * TestSuite describes a set of tests divided by section. + * + *

Visibility must be public for YAML deserialization to work. This is effectively + * package-private since the outer class is. + */ + @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; + } + + public void setSection(List section) { + this.section = section; + } + + public String getDescription() { + return description; + } + + public List getSection() { + return section; + } + + @VisibleForTesting + public static final class PolicyTestSection { + private String name; + private List tests; + + public void setName(String name) { + this.name = name; + } + + public void setTests(List tests) { + this.tests = tests; + } + + public String getName() { + return name; + } + + public List getTests() { + return tests; + } + + @VisibleForTesting + public static final class PolicyTestCase { + private String name; + private Map input; + private Object output; + + public void setName(String name) { + this.name = name; + } + + public void setInput(Map input) { + this.input = input; + } + + public void setOutput(Object output) { + this.output = output; + } + + public String getName() { + return name; + } + + public Map getInput() { + return input; + } + + public Object getOutput() { + return output; + } + + @VisibleForTesting + public static final class PolicyTestInput { + private Object value; + private String expr; + + public Object getValue() { + return value; + } + + public void setValue(Object value) { + this.value = value; + } + + public String getExpr() { + return expr; + } + + public void setExpr(String expr) { + this.expr = expr; + } + } + } + } + } + + private static String readFile(String rlocationPath) throws IOException { + String resolvedPath = runfiles.rlocation(Ascii.toLowerCase(rlocationPath)); + if (resolvedPath == null) { + throw new IOException("Unmapped runfile path: " + rlocationPath); + } + File file = new File(resolvedPath); + if (!file.exists()) { + throw new IOException( + String.format( + "Runfile not found on disk at '%s' (unresolved path: '%s')", + resolvedPath, rlocationPath)); + } + return Files.asCharSource(file, UTF_8).read(); + } + + static boolean hasRunfile(String rlocationPath) { + String resolvedPath = runfiles.rlocation(Ascii.toLowerCase(rlocationPath)); + return resolvedPath != null && new File(resolvedPath).exists(); + } + + private static Runfiles createRunfiles() { + try { + return Runfiles.preload().withSourceRepository(AutoBazelRepository_PolicyTestHelper.NAME); + } catch (IOException e) { + throw new RuntimeException("Failed to initialize Runfiles", e); + } + } + + private PolicyTestHelper() {} +} diff --git a/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/BUILD.bazel b/protobuf/BUILD.bazel new file mode 100644 index 000000000..29b4b4f14 --- /dev/null +++ b/protobuf/BUILD.bazel @@ -0,0 +1,40 @@ +load("@rules_java//java:defs.bzl", "java_library") + +package( + default_applicable_licenses = ["//:license"], + default_visibility = ["//:internal"], +) + +java_library( + name = "cel_lite_descriptor", + # used_by_android + visibility = ["//:android_allow_list"], + exports = ["//protobuf/src/main/java/dev/cel/protobuf:cel_lite_descriptor"], +) + +java_library( + name = "proto_descriptor_collector", + testonly = 1, + visibility = ["//:internal"], + exports = ["//protobuf/src/main/java/dev/cel/protobuf:proto_descriptor_collector"], +) + +java_library( + name = "debug_printer", + testonly = 1, + visibility = ["//:internal"], + exports = ["//protobuf/src/main/java/dev/cel/protobuf:debug_printer"], +) + +java_library( + name = "lite_descriptor_codegen_metadata", + testonly = 1, + visibility = ["//:internal"], + exports = ["//protobuf/src/main/java/dev/cel/protobuf:lite_descriptor_codegen_metadata"], +) + +alias( + name = "cel_lite_descriptor_generator", + actual = "//protobuf/src/main/java/dev/cel/protobuf:cel_lite_descriptor_generator", + visibility = ["//:internal"], +) diff --git a/protobuf/src/main/java/dev/cel/protobuf/BUILD.bazel b/protobuf/src/main/java/dev/cel/protobuf/BUILD.bazel new file mode 100644 index 000000000..b2dac98e7 --- /dev/null +++ b/protobuf/src/main/java/dev/cel/protobuf/BUILD.bazel @@ -0,0 +1,102 @@ +load("@rules_java//java:defs.bzl", "java_binary", "java_library") + +package( + default_applicable_licenses = ["//:license"], + default_visibility = [ + "//protobuf:__pkg__", + "//publish:__pkg__", + ], +) + +java_binary( + name = "cel_lite_descriptor_generator", + srcs = ["CelLiteDescriptorGenerator.java"], + main_class = "dev.cel.protobuf.CelLiteDescriptorGenerator", + runtime_deps = [ + # Prevent Classloader from picking protolite. We need full version to access descriptors to codegen CelLiteDescriptor. + "@maven//:com_google_protobuf_protobuf_java", + ], + deps = [ + ":debug_printer", + ":java_file_generator", + ":proto_descriptor_collector", + "//common:cel_descriptor_util", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + "@maven//:info_picocli_picocli", + ], +) + +java_library( + name = "cel_lite_descriptor", + srcs = ["CelLiteDescriptor.java"], + # used_by_android + tags = [ + ], + deps = [ + "//common/annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_protobuf_protobuf_javalite", + ], +) + +java_library( + name = "proto_descriptor_collector", + srcs = ["ProtoDescriptorCollector.java"], + tags = [ + ], + deps = [ + ":cel_lite_descriptor", + ":debug_printer", + ":lite_descriptor_codegen_metadata", + "//common/internal:well_known_proto", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + ], +) + +filegroup( + name = "cel_lite_descriptor_template_file", + srcs = ["templates/cel_lite_descriptor_template.txt"], + visibility = ["//visibility:private"], +) + +java_library( + name = "java_file_generator", + srcs = ["JavaFileGenerator.java"], + resources = [ + ":cel_lite_descriptor_template_file", + ], + visibility = ["//visibility:private"], + deps = [ + ":lite_descriptor_codegen_metadata", + "//:auto_value", + "@maven//:com_google_guava_guava", + "@maven//:org_freemarker_freemarker", + ], +) + +java_library( + name = "debug_printer", + srcs = ["DebugPrinter.java"], + tags = [ + ], + deps = [ + "@maven//:info_picocli_picocli", + ], +) + +java_library( + name = "lite_descriptor_codegen_metadata", + srcs = ["LiteDescriptorCodegenMetadata.java"], + tags = [ + ], + deps = [ + ":cel_lite_descriptor", + "//:auto_value", + "//common/annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven//:org_jspecify_jspecify", + ], +) diff --git a/protobuf/src/main/java/dev/cel/protobuf/CelLiteDescriptor.java b/protobuf/src/main/java/dev/cel/protobuf/CelLiteDescriptor.java new file mode 100644 index 000000000..c066bb18e --- /dev/null +++ b/protobuf/src/main/java/dev/cel/protobuf/CelLiteDescriptor.java @@ -0,0 +1,304 @@ +// 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.protobuf; + +import static java.lang.Math.ceil; + +import com.google.errorprone.annotations.Immutable; +import com.google.protobuf.MessageLite; +import dev.cel.common.annotations.Internal; +import java.util.Collections; +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.function.Supplier; + +/** + * Base class for code generated CEL lite descriptors to extend from. + * + *

CEL Library Internals. Do Not Use. + */ +@Internal +@Immutable +public abstract class CelLiteDescriptor { + @SuppressWarnings("Immutable") // Copied to unmodifiable map + private final Map protoFqnToDescriptors; + + private final String version; + + public Map getProtoTypeNamesToDescriptors() { + return protoFqnToDescriptors; + } + + /** Retrieves the CEL-Java version this descriptor was generated with */ + public String getVersion() { + return version; + } + + /** + * Contains a collection of classes which describe protobuf messagelite types. + * + *

CEL Library Internals. Do Not Use. + */ + @Internal + @Immutable + public static final class MessageLiteDescriptor { + private final String fullyQualifiedProtoTypeName; + + @SuppressWarnings("Immutable") // Copied to an unmodifiable list + private final List fieldLiteDescriptors; + + @SuppressWarnings("Immutable") // Copied to an unmodifiable map + private final Map fieldNameToFieldDescriptors; + + @SuppressWarnings("Immutable") // Copied to an unmodifiable map + private final Map fieldNumberToFieldDescriptors; + + @SuppressWarnings("Immutable") // Does not alter the descriptor content + private final Supplier messageBuilderSupplier; + + public String getProtoTypeName() { + return fullyQualifiedProtoTypeName; + } + + public List getFieldDescriptors() { + return fieldLiteDescriptors; + } + + public Optional findByFieldNumber(int fieldNumber) { + return Optional.ofNullable(fieldNumberToFieldDescriptors.get(fieldNumber)); + } + + public FieldLiteDescriptor getByFieldNameOrThrow(String fieldName) { + return Objects.requireNonNull(fieldNameToFieldDescriptors.get(fieldName)); + } + + public FieldLiteDescriptor getByFieldNumberOrThrow(int fieldNumber) { + return findByFieldNumber(fieldNumber) + .orElseThrow( + () -> new NoSuchElementException("Could not find field number: " + fieldNumber)); + } + + /** Gets the builder for the message. Returns null for maps. */ + public MessageLite.Builder newMessageBuilder() { + return messageBuilderSupplier.get(); + } + + /** + * CEL Library Internals. Do not use. + * + *

Public visibility due to codegen. + */ + @Internal + public MessageLiteDescriptor( + String fullyQualifiedProtoTypeName, + List fieldLiteDescriptors, + Supplier messageBuilderSupplier) { + this.fullyQualifiedProtoTypeName = Objects.requireNonNull(fullyQualifiedProtoTypeName); + // This is a cheap operation. View over the existing map with mutators disabled. + this.fieldLiteDescriptors = + Collections.unmodifiableList(Objects.requireNonNull(fieldLiteDescriptors)); + this.messageBuilderSupplier = Objects.requireNonNull(messageBuilderSupplier); + + Map fieldNameMap = + new HashMap<>(getMapInitialCapacity(fieldLiteDescriptors.size())); + Map fieldNumberMap = + new HashMap<>(getMapInitialCapacity(fieldLiteDescriptors.size())); + for (FieldLiteDescriptor fd : fieldLiteDescriptors) { + fieldNameMap.put(fd.fieldName, fd); + fieldNumberMap.put(fd.fieldNumber, fd); + } + this.fieldNameToFieldDescriptors = Collections.unmodifiableMap(fieldNameMap); + this.fieldNumberToFieldDescriptors = Collections.unmodifiableMap(fieldNumberMap); + } + } + + /** + * Describes a field of a protobuf messagelite type. + * + *

CEL Library Internals. Do Not Use. + */ + @Internal + @Immutable + public static final class FieldLiteDescriptor { + private final int fieldNumber; + private final String fieldName; + private final JavaType javaType; + private final String fieldProtoTypeName; + private final Type protoFieldType; + private final EncodingType encodingType; + private final boolean isPacked; + + /** + * Enumeration of encoding type. This describes how CEL should deserialize the encoded message + * bytes using protobuf's wire format. This is analogous to the following from field + * descriptors: + * + *

    + *
  • LIST: Repeated Field + *
  • MAP: Map Field + *
  • SINGULAR: Neither of above (scalars, messages) + *
+ */ + public enum EncodingType { + SINGULAR, + LIST, + MAP + } + + /** + * Enumeration of the java type. + * + *

This is exactly the same as com.google.protobuf.Descriptors#JavaType + */ + public enum JavaType { + INT, + LONG, + FLOAT, + DOUBLE, + BOOLEAN, + STRING, + BYTE_STRING, + ENUM, + MESSAGE + } + + /** + * Enumeration of the protobuf type. + * + *

This is exactly the same as com.google.protobuf.Descriptors#Type + */ + public enum Type { + DOUBLE, + FLOAT, + INT64, + UINT64, + INT32, + FIXED64, + FIXED32, + BOOL, + STRING, + GROUP, + MESSAGE, + BYTES, + UINT32, + ENUM, + SFIXED32, + SFIXED64, + SINT32, + SINT64 + } + + public String getFieldName() { + return fieldName; + } + + /** + * Gets the field's java type. + * + *

This is exactly the same as com.google.protobuf.Descriptors#JavaType + */ + public JavaType getJavaType() { + return javaType; + } + + public EncodingType getEncodingType() { + return encodingType; + } + + /** + * Gets the field's protobuf type. + * + *

This is exactly the same as com.google.protobuf.Descriptors#Type + */ + public Type getProtoFieldType() { + return protoFieldType; + } + + /** Checks whether the repeated field is packed. */ + public boolean getIsPacked() { + return isPacked; + } + + /** + * Gets the fully qualified protobuf type name for the field, including its package name (ex: + * cel.expr.conformance.proto3.TestAllTypes.SingleStringWrapper). Returns an empty string for + * primitives. + */ + public String getFieldProtoTypeName() { + return fieldProtoTypeName; + } + + /** + * Must be public, used for codegen only. Do not use. + * + * @param fieldNumber Field index + * @param fieldName Name of the field + * @param javaType Canonical Java type name (ex: Long, Double, Float, Message... see + * com.google.protobuf.Descriptors#JavaType) + * @param encodingType Describes whether the field is a singular (primitives or messages), list + * or a map with respect to CEL. + * @param protoFieldType Protobuf Field Type (ex: INT32, SINT32, GROUP, MESSAGE... see + * com.google.protobuf.Descriptors#Type) + * @param fieldProtoTypeName Fully qualified protobuf type name for the field. Empty if the + * field is a primitive. + */ + @Internal + public FieldLiteDescriptor( + int fieldNumber, + String fieldName, + JavaType javaType, + EncodingType encodingType, // LIST, MAP, SINGULAR + Type protoFieldType, // INT32, SINT32, GROUP, MESSAGE... (See Descriptors#Type) + boolean isPacked, + String fieldProtoTypeName) { + this.fieldNumber = fieldNumber; + this.fieldName = Objects.requireNonNull(fieldName); + this.javaType = javaType; + this.encodingType = encodingType; + this.protoFieldType = protoFieldType; + this.isPacked = isPacked; + this.fieldProtoTypeName = Objects.requireNonNull(fieldProtoTypeName); + } + } + + protected CelLiteDescriptor(String version, List messageInfoList) { + Map protoFqnMap = + new HashMap<>(getMapInitialCapacity(messageInfoList.size())); + for (MessageLiteDescriptor msgInfo : messageInfoList) { + protoFqnMap.put(msgInfo.getProtoTypeName(), msgInfo); + } + + this.version = version; + this.protoFqnToDescriptors = Collections.unmodifiableMap(protoFqnMap); + } + + /** + * Returns a capacity that is sufficient to keep the map from being resized as long as it grows no + * larger than expectedSize and the load factor is ≥ its default (0.75). + */ + private static int getMapInitialCapacity(int expectedSize) { + if (expectedSize < 3) { + return expectedSize + 1; + } + + // See https://github.com/openjdk/jdk/commit/3e393047e12147a81e2899784b943923fc34da8e. 0.75 is + // used as a load factor. + return (int) ceil(expectedSize / 0.75); + } +} diff --git a/protobuf/src/main/java/dev/cel/protobuf/CelLiteDescriptorGenerator.java b/protobuf/src/main/java/dev/cel/protobuf/CelLiteDescriptorGenerator.java new file mode 100644 index 000000000..8c4eaea1c --- /dev/null +++ b/protobuf/src/main/java/dev/cel/protobuf/CelLiteDescriptorGenerator.java @@ -0,0 +1,217 @@ +// 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.protobuf; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.io.Files; +import com.google.protobuf.DescriptorProtos.FileDescriptorProto; +import com.google.protobuf.DescriptorProtos.FileDescriptorSet; +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.protobuf.JavaFileGenerator.GeneratedClass; +import dev.cel.protobuf.JavaFileGenerator.JavaFileGeneratorOption; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import picocli.CommandLine; +import picocli.CommandLine.Model.OptionSpec; +import picocli.CommandLine.Option; + +final class CelLiteDescriptorGenerator implements Callable { + + private static final String DEFAULT_CEL_LITE_DESCRIPTOR_CLASS_SUFFIX = "CelLiteDescriptor"; + + @Option( + names = {"--out"}, + description = "Outpath for the CelLiteDescriptor") + private String outPath = ""; + + @Option( + names = {"--descriptor_set"}, + split = ",", + description = + "Paths to the descriptor set (from proto_library) that the CelLiteDescriptor is to be" + + " generated from (comma-separated)") + private List targetDescriptorSetPath = new ArrayList<>(); + + @Option( + names = {"--transitive_descriptor_set"}, + split = ",", + description = "Paths to the transitive set of descriptors (comma-separated)") + private List transitiveDescriptorSetPath = new ArrayList<>(); + + @Option( + names = {"--overridden_descriptor_class_suffix"}, + description = "Suffix name for the generated CelLiteDescriptor Java class") + private String overriddenDescriptorClassSuffix = ""; + + @Option( + names = {"--version"}, + description = "CEL-Java version") + private String version = ""; + + @Option( + names = {"--debug"}, + description = "Prints debug output") + private boolean debug = false; + + private DebugPrinter debugPrinter; + + @Override + public Integer call() throws Exception { + Preconditions.checkArgument(!targetDescriptorSetPath.isEmpty()); + + ImmutableList.Builder generatedClassesBuilder = ImmutableList.builder(); + for (String descriptorFilePath : targetDescriptorSetPath) { + debugPrinter.print("Target descriptor file path: " + descriptorFilePath); + String targetDescriptorProtoPath = extractProtoPath(descriptorFilePath); + debugPrinter.print("Target descriptor proto path: " + targetDescriptorProtoPath); + FileDescriptorSet transitiveDescriptorSet = + combineFileDescriptors(transitiveDescriptorSetPath); + + FileDescriptor targetFileDescriptor = null; + ImmutableSet transitiveFileDescriptors = + CelDescriptorUtil.getFileDescriptorsFromFileDescriptorSet(transitiveDescriptorSet); + for (FileDescriptor fd : transitiveFileDescriptors) { + if (fd.getFullName().equals(targetDescriptorProtoPath)) { + targetFileDescriptor = fd; + break; + } + } + + if (targetFileDescriptor == null) { + throw new IllegalArgumentException( + String.format( + "Target descriptor %s not found from transitive set of descriptors!", + targetDescriptorProtoPath)); + } + + ImmutableList generatedClasses = + codegenCelLiteDescriptors(targetFileDescriptor); + generatedClassesBuilder.addAll(generatedClasses); + } + + JavaFileGenerator.writeSrcJar(outPath, generatedClassesBuilder.build()); + + return 0; + } + + private ImmutableList codegenCelLiteDescriptors( + FileDescriptor targetFileDescriptor) throws Exception { + String javaPackageName = GeneratorNames.getFileJavaPackage(targetFileDescriptor.toProto()); + String javaClassName; + + List descriptors = targetFileDescriptor.getMessageTypes(); + if (descriptors.isEmpty()) { + throw new IllegalArgumentException("File descriptor does not contain any messages!"); + } + + ProtoDescriptorCollector descriptorCollector = + ProtoDescriptorCollector.newInstance(debugPrinter); + + ImmutableList.Builder generatedClassBuilder = ImmutableList.builder(); + for (Descriptor messageDescriptor : descriptors) { + javaClassName = messageDescriptor.getName(); + String javaSuffixName = + overriddenDescriptorClassSuffix.isEmpty() + ? DEFAULT_CEL_LITE_DESCRIPTOR_CLASS_SUFFIX + : overriddenDescriptorClassSuffix; + javaClassName += javaSuffixName; + + debugPrinter.print( + String.format( + "Fully qualified descriptor java class name: %s.%s", javaPackageName, javaClassName)); + + generatedClassBuilder.add( + JavaFileGenerator.generateClass( + JavaFileGeneratorOption.newBuilder() + .setVersion(version) + .setDescriptorClassName(javaClassName) + .setPackageName(javaPackageName) + .setDescriptorMetadataList( + descriptorCollector.collectCodegenMetadata(messageDescriptor)) + .build())); + } + + return generatedClassBuilder.build(); + } + + private String extractProtoPath(String descriptorPath) { + FileDescriptorSet fds = load(descriptorPath); + if (fds.getFileList().isEmpty()) { + throw new IllegalArgumentException( + "FileDescriptorSet did not contain any descriptors: " + descriptorPath); + } + + // A direct descriptor set may contain one or more files (ex: extensions), but the first + // argument is always the original .proto file. + FileDescriptorProto fileDescriptorProto = fds.getFile(0); + return fileDescriptorProto.getName(); + } + + private FileDescriptorSet combineFileDescriptors(List descriptorPaths) { + FileDescriptorSet.Builder combinedDescriptorBuilder = FileDescriptorSet.newBuilder(); + + for (String descriptorPath : descriptorPaths) { + FileDescriptorSet loadedFds = load(descriptorPath); + combinedDescriptorBuilder.addAllFile(loadedFds.getFileList()); + } + + return combinedDescriptorBuilder.build(); + } + + private static FileDescriptorSet load(String descriptorSetPath) { + try { + byte[] descriptorBytes = Files.toByteArray(new File(descriptorSetPath)); + return FileDescriptorSet.parseFrom(descriptorBytes, ExtensionRegistry.getEmptyRegistry()); + } catch (IOException e) { + throw new IllegalArgumentException( + "Failed to load FileDescriptorSet from path: " + descriptorSetPath, e); + } + } + + private void printAllFlags(CommandLine cmd) { + debugPrinter.print("Flag values:"); + debugPrinter.print("-------------------------------------------------------------"); + for (OptionSpec option : cmd.getCommandSpec().options()) { + debugPrinter.print(option.longestName() + ": " + option.getValue()); + } + debugPrinter.print("-------------------------------------------------------------"); + } + + private void initializeDebugPrinter() { + this.debugPrinter = DebugPrinter.newInstance(debug); + } + + public static void main(String[] args) { + CelLiteDescriptorGenerator celLiteDescriptorGenerator = new CelLiteDescriptorGenerator(); + CommandLine cmd = new CommandLine(celLiteDescriptorGenerator); + cmd.parseArgs(args); + celLiteDescriptorGenerator.initializeDebugPrinter(); + celLiteDescriptorGenerator.printAllFlags(cmd); + + int exitCode = cmd.execute(args); + System.exit(exitCode); + } + + CelLiteDescriptorGenerator() {} +} diff --git a/protobuf/src/main/java/dev/cel/protobuf/DebugPrinter.java b/protobuf/src/main/java/dev/cel/protobuf/DebugPrinter.java new file mode 100644 index 000000000..34a09ce98 --- /dev/null +++ b/protobuf/src/main/java/dev/cel/protobuf/DebugPrinter.java @@ -0,0 +1,36 @@ +// 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.protobuf; + +import picocli.CommandLine.Help.Ansi; + +final class DebugPrinter { + + private final boolean debug; + + static DebugPrinter newInstance(boolean debug) { + return new DebugPrinter(debug); + } + + void print(String message) { + if (debug) { + System.out.println(Ansi.ON.string("@|cyan [CelLiteDescriptorGenerator] |@" + message)); + } + } + + private DebugPrinter(boolean debug) { + this.debug = debug; + } +} diff --git a/protobuf/src/main/java/dev/cel/protobuf/JavaFileGenerator.java b/protobuf/src/main/java/dev/cel/protobuf/JavaFileGenerator.java new file mode 100644 index 000000000..0eb177a96 --- /dev/null +++ b/protobuf/src/main/java/dev/cel/protobuf/JavaFileGenerator.java @@ -0,0 +1,139 @@ +// 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.protobuf; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.io.ByteStreams; +// CEL-Internal-3 +import freemarker.template.Configuration; +import freemarker.template.DefaultObjectWrapperBuilder; +import freemarker.template.Template; +import freemarker.template.TemplateException; +import freemarker.template.Version; +import java.io.ByteArrayInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringWriter; +import java.io.Writer; +import java.util.Collection; +import java.util.Locale; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +final class JavaFileGenerator { + + private static final String HELPER_CLASS_TEMPLATE_FILE = "cel_lite_descriptor_template.txt"; + + static GeneratedClass generateClass(JavaFileGeneratorOption option) + throws IOException, TemplateException { + Version version = Configuration.VERSION_2_3_32; + Configuration cfg = new Configuration(version); + cfg.setClassForTemplateLoading(JavaFileGenerator.class, "templates/"); + cfg.setDefaultEncoding("UTF-8"); + cfg.setBooleanFormat("c"); + cfg.setAPIBuiltinEnabled(true); + cfg.setNumberFormat("#"); // Prevent thousandth separator in numbers (eg: 1000 instead of 1,000) + DefaultObjectWrapperBuilder wrapperBuilder = new DefaultObjectWrapperBuilder(version); + wrapperBuilder.setExposeFields(true); + cfg.setObjectWrapper(wrapperBuilder.build()); + + Template template = cfg.getTemplate(HELPER_CLASS_TEMPLATE_FILE); + Writer out = new StringWriter(); + template.process(option.getTemplateMap(), out); + + return GeneratedClass.create( + /* packageName= */ option.packageName(), + /* className= */ option.descriptorClassName(), + /* code= */ out.toString()); + } + + static void writeSrcJar(String srcjarFilePath, Collection generatedClasses) + throws IOException { + if (!srcjarFilePath.toLowerCase(Locale.getDefault()).endsWith(".srcjar")) { + throw new IllegalArgumentException("File must end with .srcjar, provided: " + srcjarFilePath); + } + try (FileOutputStream fos = new FileOutputStream(srcjarFilePath); + ZipOutputStream zos = new ZipOutputStream(fos)) { + for (GeneratedClass generatedClass : generatedClasses) { + // Replace com.foo.bar to com/foo/bar.java in order to conform with package location + String javaFileName = generatedClass.fullyQualifiedClassName().replace('.', '/') + ".java"; + ZipEntry entry = new ZipEntry(javaFileName); + zos.putNextEntry(entry); + + try (InputStream inputStream = + new ByteArrayInputStream(generatedClass.code().getBytes(UTF_8))) { + ByteStreams.copy(inputStream, zos); + } + } + + zos.closeEntry(); + } + } + + @AutoValue + abstract static class GeneratedClass { + abstract String fullyQualifiedClassName(); + + abstract String code(); + + static GeneratedClass create(String packageName, String className, String code) { + return new AutoValue_JavaFileGenerator_GeneratedClass(packageName + "." + className, code); + } + } + + @AutoValue + abstract static class JavaFileGeneratorOption { + abstract String packageName(); + + abstract String descriptorClassName(); + + abstract String version(); + + abstract ImmutableList descriptorMetadataList(); + + ImmutableMap getTemplateMap() { + return ImmutableMap.of( + "package_name", packageName(), + "descriptor_class_name", descriptorClassName(), + "version", version(), + "descriptor_metadata_list", descriptorMetadataList()); + } + + @AutoValue.Builder + abstract static class Builder { + abstract Builder setPackageName(String packageName); + + abstract Builder setDescriptorClassName(String className); + + abstract Builder setVersion(String version); + + abstract Builder setDescriptorMetadataList( + ImmutableList messageInfo); + + abstract JavaFileGeneratorOption build(); + } + + static Builder newBuilder() { + return new AutoValue_JavaFileGenerator_JavaFileGeneratorOption.Builder(); + } + } + + private JavaFileGenerator() {} +} diff --git a/protobuf/src/main/java/dev/cel/protobuf/LiteDescriptorCodegenMetadata.java b/protobuf/src/main/java/dev/cel/protobuf/LiteDescriptorCodegenMetadata.java new file mode 100644 index 000000000..19372b9b4 --- /dev/null +++ b/protobuf/src/main/java/dev/cel/protobuf/LiteDescriptorCodegenMetadata.java @@ -0,0 +1,142 @@ +// 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.protobuf; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import dev.cel.common.annotations.Internal; +import dev.cel.protobuf.CelLiteDescriptor.FieldLiteDescriptor; +import dev.cel.protobuf.CelLiteDescriptor.FieldLiteDescriptor.EncodingType; +import org.jspecify.annotations.Nullable; + +/** + * LiteDescriptorCodegenMetadata holds metadata collected from a full protobuf descriptor pertinent + * for generating a {@link CelLiteDescriptor}. + * + *

The class properties here are almost identical to CelLiteDescriptor, except it contains + * extraneous information such as the fully qualified class names to support codegen, which do not + * need to be present on a CelLiteDescriptor instance. + * + *

Note: Properties must be of primitive types. + * + *

Note: JavaBeans prefix (e.g: getFoo) is required for compatibility with freemarker. + * + *

CEL Library Internals. Do Not Use. + */ +@AutoValue +@Internal +public abstract class LiteDescriptorCodegenMetadata { + + public abstract String getProtoTypeName(); + + public abstract ImmutableList getFieldDescriptors(); + + // @Nullable note: A java class name is not populated for maps, even though it behaves like a + // message. + public abstract @Nullable String getJavaClassName(); + + @AutoValue.Builder + abstract static class Builder { + + abstract Builder setProtoTypeName(String protoTypeName); + + abstract Builder setJavaClassName(String javaClassName); + + abstract ImmutableList.Builder fieldDescriptorsBuilder(); + + @CanIgnoreReturnValue + Builder addFieldDescriptor(FieldLiteDescriptorMetadata fieldDescriptor) { + this.fieldDescriptorsBuilder().add(fieldDescriptor); + return this; + } + + abstract LiteDescriptorCodegenMetadata build(); + } + + static Builder newBuilder() { + return new AutoValue_LiteDescriptorCodegenMetadata.Builder(); + } + + /** + * Metadata collected from a protobuf message's FieldDescriptor. This is used to codegen {@link + * FieldLiteDescriptor}. + */ + @AutoValue + public abstract static class FieldLiteDescriptorMetadata { + + public abstract int getFieldNumber(); + + public abstract String getFieldName(); + + // Fully-qualified name to the Java Type enumeration (ex: + // dev.cel.protobuf.CelLiteDescriptor.FieldLiteDescriptor.INT) + public String getJavaTypeEnumName() { + return getFullyQualifiedEnumName(getJavaType()); + } + + // Fully-qualified name to the EncodingType enumeration (ex: + // dev.cel.protobuf.CelLiteDescriptor.FieldLiteDescriptor.SINGULAR) + public String getEncodingTypeEnumName() { + return getFullyQualifiedEnumName(getEncodingType()); + } + + // Fully-qualified name to the Proto Type enumeration (ex: + // dev.cel.protobuf.CelLiteDescriptor.FieldLiteDescriptor.INT) + public String getProtoFieldTypeEnumName() { + return getFullyQualifiedEnumName(getProtoFieldType()); + } + + public abstract boolean getIsPacked(); + + public abstract String getFieldProtoTypeName(); + + abstract FieldLiteDescriptor.JavaType getJavaType(); + + abstract FieldLiteDescriptor.Type getProtoFieldType(); + + abstract EncodingType getEncodingType(); + + private static String getFullyQualifiedEnumName(Object enumValue) { + String enumClassName = enumValue.getClass().getName(); + return (enumClassName + "." + enumValue).replace('$', '.'); + } + + @AutoValue.Builder + abstract static class Builder { + + abstract Builder setFieldNumber(int fieldNumber); + + abstract Builder setFieldName(String fieldName); + + abstract Builder setJavaType(FieldLiteDescriptor.JavaType javaTypeEnum); + + abstract Builder setEncodingType(EncodingType encodingTypeEnum); + + abstract Builder setProtoFieldType(FieldLiteDescriptor.Type protoFieldTypeEnum); + + abstract Builder setIsPacked(boolean isPacked); + + abstract Builder setFieldProtoTypeName(String fieldProtoTypeName); + + abstract FieldLiteDescriptorMetadata build(); + } + + static FieldLiteDescriptorMetadata.Builder newBuilder() { + return new AutoValue_LiteDescriptorCodegenMetadata_FieldLiteDescriptorMetadata.Builder() + .setFieldProtoTypeName(""); + } + } +} diff --git a/protobuf/src/main/java/dev/cel/protobuf/ProtoDescriptorCollector.java b/protobuf/src/main/java/dev/cel/protobuf/ProtoDescriptorCollector.java new file mode 100644 index 000000000..c2fe20557 --- /dev/null +++ b/protobuf/src/main/java/dev/cel/protobuf/ProtoDescriptorCollector.java @@ -0,0 +1,196 @@ +// 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.protobuf; + +import static com.google.common.collect.ImmutableList.toImmutableList; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.protobuf.Descriptors; +import com.google.protobuf.Descriptors.Descriptor; +import com.google.protobuf.Descriptors.FieldDescriptor.JavaType; +import com.google.protobuf.Descriptors.FileDescriptor; +import com.google.protobuf.GeneratorNames; +import dev.cel.common.internal.WellKnownProto; +import dev.cel.protobuf.CelLiteDescriptor.FieldLiteDescriptor; +import dev.cel.protobuf.CelLiteDescriptor.FieldLiteDescriptor.EncodingType; +import dev.cel.protobuf.LiteDescriptorCodegenMetadata.FieldLiteDescriptorMetadata; + +/** + * ProtoDescriptorCollector inspects a {@link FileDescriptor} to collect message information into + * {@link LiteDescriptorCodegenMetadata}. This is later utilized to create an instance of {@code + * MessageLiteDescriptor}. + */ +final class ProtoDescriptorCollector { + + private final DebugPrinter debugPrinter; + + ImmutableList collectCodegenMetadata(Descriptor descriptor) { + ImmutableList.Builder descriptorListBuilder = + ImmutableList.builder(); + ImmutableList descriptorList = + collectNested(descriptor).stream() + // Don't collect WKTs. They are included in the default descriptor pool. + .filter(d -> !WellKnownProto.getByTypeName(d.getFullName()).isPresent()) + .collect(toImmutableList()); + + for (Descriptor messageDescriptor : descriptorList) { + LiteDescriptorCodegenMetadata.Builder descriptorCodegenBuilder = + LiteDescriptorCodegenMetadata.newBuilder(); + for (Descriptors.FieldDescriptor fieldDescriptor : messageDescriptor.getFields()) { + FieldLiteDescriptorMetadata.Builder fieldDescriptorCodegenBuilder = + FieldLiteDescriptorMetadata.newBuilder() + .setFieldNumber(fieldDescriptor.getNumber()) + .setFieldName(fieldDescriptor.getName()) + .setIsPacked(fieldDescriptor.isPacked()) + .setJavaType(adaptJavaType(fieldDescriptor.getJavaType())) + .setProtoFieldType(adaptFieldProtoType(fieldDescriptor.getType())); + + switch (fieldDescriptor.getJavaType()) { + case ENUM: + fieldDescriptorCodegenBuilder.setFieldProtoTypeName( + fieldDescriptor.getEnumType().getFullName()); + break; + case MESSAGE: + fieldDescriptorCodegenBuilder.setFieldProtoTypeName( + fieldDescriptor.getMessageType().getFullName()); + break; + default: + break; + } + + if (fieldDescriptor.isMapField()) { + fieldDescriptorCodegenBuilder.setEncodingType(EncodingType.MAP); + } else if (fieldDescriptor.isRepeated()) { + fieldDescriptorCodegenBuilder.setEncodingType(EncodingType.LIST); + } else { + fieldDescriptorCodegenBuilder.setEncodingType(EncodingType.SINGULAR); + } + + descriptorCodegenBuilder.addFieldDescriptor(fieldDescriptorCodegenBuilder.build()); + + debugPrinter.print( + String.format( + "Collecting message %s, for field %s, type: %s", + messageDescriptor.getFullName(), + fieldDescriptor.getFullName(), + fieldDescriptor.getType())); + } + + descriptorCodegenBuilder.setProtoTypeName(messageDescriptor.getFullName()); + // Maps are resolved as an actual Java map, and doesn't have a MessageLite.Builder associated. + if (!messageDescriptor.getOptions().getMapEntry()) { + String sanitizedJavaClassName = + GeneratorNames.getBytecodeClassName(messageDescriptor).replace('$', '.'); + descriptorCodegenBuilder.setJavaClassName(sanitizedJavaClassName); + } + + descriptorListBuilder.add(descriptorCodegenBuilder.build()); + } + + return descriptorListBuilder.build(); + } + + static ProtoDescriptorCollector newInstance(DebugPrinter debugPrinter) { + return new ProtoDescriptorCollector(debugPrinter); + } + + private static FieldLiteDescriptor.Type adaptFieldProtoType( + Descriptors.FieldDescriptor.Type type) { + switch (type) { + case DOUBLE: + return FieldLiteDescriptor.Type.DOUBLE; + case FLOAT: + return FieldLiteDescriptor.Type.FLOAT; + case INT64: + return FieldLiteDescriptor.Type.INT64; + case UINT64: + return FieldLiteDescriptor.Type.UINT64; + case INT32: + return FieldLiteDescriptor.Type.INT32; + case FIXED64: + return FieldLiteDescriptor.Type.FIXED64; + case FIXED32: + return FieldLiteDescriptor.Type.FIXED32; + case BOOL: + return FieldLiteDescriptor.Type.BOOL; + case STRING: + return FieldLiteDescriptor.Type.STRING; + case MESSAGE: + return FieldLiteDescriptor.Type.MESSAGE; + case BYTES: + return FieldLiteDescriptor.Type.BYTES; + case UINT32: + return FieldLiteDescriptor.Type.UINT32; + case ENUM: + return FieldLiteDescriptor.Type.ENUM; + case SFIXED32: + return FieldLiteDescriptor.Type.SFIXED32; + case SFIXED64: + return FieldLiteDescriptor.Type.SFIXED64; + case SINT32: + return FieldLiteDescriptor.Type.SINT32; + case SINT64: + return FieldLiteDescriptor.Type.SINT64; + case GROUP: + return FieldLiteDescriptor.Type.GROUP; + } + + throw new IllegalArgumentException("Unknown Type: " + type); + } + + private static FieldLiteDescriptor.JavaType adaptJavaType(JavaType javaType) { + switch (javaType) { + case INT: + return FieldLiteDescriptor.JavaType.INT; + case LONG: + return FieldLiteDescriptor.JavaType.LONG; + case FLOAT: + return FieldLiteDescriptor.JavaType.FLOAT; + case DOUBLE: + return FieldLiteDescriptor.JavaType.DOUBLE; + case BOOLEAN: + return FieldLiteDescriptor.JavaType.BOOLEAN; + case STRING: + return FieldLiteDescriptor.JavaType.STRING; + case BYTE_STRING: + return FieldLiteDescriptor.JavaType.BYTE_STRING; + case ENUM: + return FieldLiteDescriptor.JavaType.ENUM; + case MESSAGE: + return FieldLiteDescriptor.JavaType.MESSAGE; + } + + throw new IllegalArgumentException("Unknown JavaType: " + javaType); + } + + private static ImmutableSet collectNested(Descriptor descriptor) { + ImmutableSet.Builder builder = ImmutableSet.builder(); + collectNested(builder, descriptor); + return builder.build(); + } + + private static void collectNested( + ImmutableSet.Builder builder, Descriptor descriptor) { + builder.add(descriptor); + for (Descriptor nested : descriptor.getNestedTypes()) { + collectNested(builder, nested); + } + } + + private ProtoDescriptorCollector(DebugPrinter debugPrinter) { + this.debugPrinter = debugPrinter; + } +} diff --git a/protobuf/src/main/java/dev/cel/protobuf/templates/cel_lite_descriptor_template.txt b/protobuf/src/main/java/dev/cel/protobuf/templates/cel_lite_descriptor_template.txt new file mode 100644 index 000000000..2d8515146 --- /dev/null +++ b/protobuf/src/main/java/dev/cel/protobuf/templates/cel_lite_descriptor_template.txt @@ -0,0 +1,76 @@ +// 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. + +/** + * Generated by CEL-Java library. DO NOT EDIT! + * Version: ${version} + */ + +package ${package_name}; + +import dev.cel.common.annotations.Generated; +import dev.cel.protobuf.CelLiteDescriptor; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Generated("dev.cel.protobuf.CelLiteDescriptorGenerator") +public final class ${descriptor_class_name} extends CelLiteDescriptor { + + private static final ${descriptor_class_name} DESCRIPTOR = new ${descriptor_class_name}(); + + public static ${descriptor_class_name} getDescriptor() { + return DESCRIPTOR; + } + + private static List newDescriptors() { + List descriptors = new ArrayList<>(${descriptor_metadata_list?size}); + List fieldDescriptors; + <#list descriptor_metadata_list as descriptor_metadata> + + fieldDescriptors = new ArrayList<>(${descriptor_metadata.fieldDescriptors?size}); + <#list descriptor_metadata.fieldDescriptors as field_descriptor> + fieldDescriptors.add(new FieldLiteDescriptor( + ${field_descriptor.fieldNumber}, + "${field_descriptor.fieldName}", + ${field_descriptor.javaTypeEnumName}, + ${field_descriptor.encodingTypeEnumName}, + ${field_descriptor.protoFieldTypeEnumName}, + ${field_descriptor.isPacked}, + "${field_descriptor.fieldProtoTypeName}" + )); + + + descriptors.add( + new MessageLiteDescriptor( + "${descriptor_metadata.protoTypeName}", + fieldDescriptors, + <#if descriptor_metadata.javaClassName??> + ${descriptor_metadata.javaClassName}::newBuilder + <#else> + () -> null + + ) + ); + + + return Collections.unmodifiableList(descriptors); + } + + private ${descriptor_class_name}() { + super("${version}", newDescriptors()); + } +} \ No newline at end of file diff --git a/protobuf/src/test/java/dev/cel/protobuf/BUILD.bazel b/protobuf/src/test/java/dev/cel/protobuf/BUILD.bazel new file mode 100644 index 000000000..58e298b29 --- /dev/null +++ b/protobuf/src/test/java/dev/cel/protobuf/BUILD.bazel @@ -0,0 +1,38 @@ +load("@rules_java//java:defs.bzl", "java_test") + +package( + default_applicable_licenses = ["//:license"], + default_testonly = True, +) + +java_test( + name = "cel_lite_descriptor_test", + srcs = ["CelLiteDescriptorTest.java"], + test_class = "dev.cel.protobuf.CelLiteDescriptorTest", + deps = [ + "//:java_truth", + "//protobuf:cel_lite_descriptor", + "//testing/protos:test_all_types_cel_java_proto3_lite", + "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto_lite", + "@maven//:com_google_testparameterinjector_test_parameter_injector", + "@maven//:junit_junit", + ], +) + +java_test( + name = "proto_descriptor_collector_test", + srcs = ["ProtoDescriptorCollectorTest.java"], + test_class = "dev.cel.protobuf.ProtoDescriptorCollectorTest", + runtime_deps = ["@maven//:com_google_protobuf_protobuf_java"], + deps = [ + "//:java_truth", + "//protobuf:debug_printer", + "//protobuf:lite_descriptor_codegen_metadata", + "//protobuf:proto_descriptor_collector", + "//testing/protos:multi_file_java_proto", + "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", + "@maven//:com_google_guava_guava", + "@maven//:com_google_testparameterinjector_test_parameter_injector", + "@maven//:junit_junit", + ], +) diff --git a/protobuf/src/test/java/dev/cel/protobuf/CelLiteDescriptorTest.java b/protobuf/src/test/java/dev/cel/protobuf/CelLiteDescriptorTest.java new file mode 100644 index 000000000..1ceed29bb --- /dev/null +++ b/protobuf/src/test/java/dev/cel/protobuf/CelLiteDescriptorTest.java @@ -0,0 +1,149 @@ +// 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.protobuf; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.expr.conformance.proto3.TestAllTypesCelLiteDescriptor; +import dev.cel.protobuf.CelLiteDescriptor.FieldLiteDescriptor; +import dev.cel.protobuf.CelLiteDescriptor.FieldLiteDescriptor.EncodingType; +import dev.cel.protobuf.CelLiteDescriptor.FieldLiteDescriptor.JavaType; +import dev.cel.protobuf.CelLiteDescriptor.MessageLiteDescriptor; +import java.util.Map; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public class CelLiteDescriptorTest { + + private static final TestAllTypesCelLiteDescriptor TEST_ALL_TYPES_CEL_LITE_DESCRIPTOR = + TestAllTypesCelLiteDescriptor.getDescriptor(); + + @Test + public void getProtoTypeNamesToDescriptors_containsAllMessages() { + Map protoNamesToDescriptors = + TEST_ALL_TYPES_CEL_LITE_DESCRIPTOR.getProtoTypeNamesToDescriptors(); + + assertThat(protoNamesToDescriptors).containsKey("cel.expr.conformance.proto3.TestAllTypes"); + assertThat(protoNamesToDescriptors) + .containsKey("cel.expr.conformance.proto3.TestAllTypes.NestedMessage"); + } + + @Test + public void testAllTypesMessageLiteDescriptor_fullyQualifiedNames() { + MessageLiteDescriptor testAllTypesDescriptor = + TEST_ALL_TYPES_CEL_LITE_DESCRIPTOR + .getProtoTypeNamesToDescriptors() + .get("cel.expr.conformance.proto3.TestAllTypes"); + + assertThat(testAllTypesDescriptor.getProtoTypeName()) + .isEqualTo("cel.expr.conformance.proto3.TestAllTypes"); + } + + @Test + public void fieldDescriptor_getByFieldNumber() { + MessageLiteDescriptor testAllTypesDescriptor = + TEST_ALL_TYPES_CEL_LITE_DESCRIPTOR + .getProtoTypeNamesToDescriptors() + .get("cel.expr.conformance.proto3.TestAllTypes"); + + FieldLiteDescriptor fieldLiteDescriptor = testAllTypesDescriptor.getByFieldNumberOrThrow(14); + + assertThat(fieldLiteDescriptor.getFieldName()).isEqualTo("single_string"); + } + + @Test + public void fieldDescriptor_scalarField() { + MessageLiteDescriptor testAllTypesDescriptor = + TEST_ALL_TYPES_CEL_LITE_DESCRIPTOR + .getProtoTypeNamesToDescriptors() + .get("cel.expr.conformance.proto3.TestAllTypes"); + FieldLiteDescriptor fieldLiteDescriptor = + testAllTypesDescriptor.getByFieldNameOrThrow("single_string"); + + assertThat(fieldLiteDescriptor.getEncodingType()).isEqualTo(EncodingType.SINGULAR); + assertThat(fieldLiteDescriptor.getJavaType()).isEqualTo(JavaType.STRING); + assertThat(fieldLiteDescriptor.getProtoFieldType()).isEqualTo(FieldLiteDescriptor.Type.STRING); + } + + @Test + public void fieldDescriptor_primitiveField_fullyQualifiedNames() { + MessageLiteDescriptor testAllTypesDescriptor = + TEST_ALL_TYPES_CEL_LITE_DESCRIPTOR + .getProtoTypeNamesToDescriptors() + .get("cel.expr.conformance.proto3.TestAllTypes"); + FieldLiteDescriptor fieldLiteDescriptor = + testAllTypesDescriptor.getByFieldNameOrThrow("single_string"); + + assertThat(fieldLiteDescriptor.getFieldProtoTypeName()).isEmpty(); + } + + @Test + public void fieldDescriptor_mapField() { + MessageLiteDescriptor testAllTypesDescriptor = + TEST_ALL_TYPES_CEL_LITE_DESCRIPTOR + .getProtoTypeNamesToDescriptors() + .get("cel.expr.conformance.proto3.TestAllTypes"); + FieldLiteDescriptor fieldLiteDescriptor = + testAllTypesDescriptor.getByFieldNameOrThrow("map_bool_string"); + + assertThat(fieldLiteDescriptor.getEncodingType()).isEqualTo(EncodingType.MAP); + assertThat(fieldLiteDescriptor.getJavaType()).isEqualTo(JavaType.MESSAGE); + assertThat(fieldLiteDescriptor.getProtoFieldType()).isEqualTo(FieldLiteDescriptor.Type.MESSAGE); + } + + @Test + public void fieldDescriptor_repeatedField() { + MessageLiteDescriptor testAllTypesDescriptor = + TEST_ALL_TYPES_CEL_LITE_DESCRIPTOR + .getProtoTypeNamesToDescriptors() + .get("cel.expr.conformance.proto3.TestAllTypes"); + FieldLiteDescriptor fieldLiteDescriptor = + testAllTypesDescriptor.getByFieldNameOrThrow("repeated_int64"); + + assertThat(fieldLiteDescriptor.getEncodingType()).isEqualTo(EncodingType.LIST); + assertThat(fieldLiteDescriptor.getJavaType()).isEqualTo(JavaType.LONG); + assertThat(fieldLiteDescriptor.getIsPacked()).isTrue(); + assertThat(fieldLiteDescriptor.getProtoFieldType()).isEqualTo(FieldLiteDescriptor.Type.INT64); + } + + @Test + public void fieldDescriptor_nestedMessage() { + MessageLiteDescriptor testAllTypesDescriptor = + TEST_ALL_TYPES_CEL_LITE_DESCRIPTOR + .getProtoTypeNamesToDescriptors() + .get("cel.expr.conformance.proto3.TestAllTypes"); + FieldLiteDescriptor fieldLiteDescriptor = + testAllTypesDescriptor.getByFieldNameOrThrow("standalone_message"); + + assertThat(fieldLiteDescriptor.getEncodingType()).isEqualTo(EncodingType.SINGULAR); + assertThat(fieldLiteDescriptor.getJavaType()).isEqualTo(JavaType.MESSAGE); + assertThat(fieldLiteDescriptor.getProtoFieldType()).isEqualTo(FieldLiteDescriptor.Type.MESSAGE); + } + + @Test + public void fieldDescriptor_nestedMessage_fullyQualifiedNames() { + MessageLiteDescriptor testAllTypesDescriptor = + TEST_ALL_TYPES_CEL_LITE_DESCRIPTOR + .getProtoTypeNamesToDescriptors() + .get("cel.expr.conformance.proto3.TestAllTypes"); + FieldLiteDescriptor fieldLiteDescriptor = + testAllTypesDescriptor.getByFieldNameOrThrow("standalone_message"); + + assertThat(fieldLiteDescriptor.getFieldProtoTypeName()) + .isEqualTo("cel.expr.conformance.proto3.TestAllTypes.NestedMessage"); + } +} diff --git a/protobuf/src/test/java/dev/cel/protobuf/ProtoDescriptorCollectorTest.java b/protobuf/src/test/java/dev/cel/protobuf/ProtoDescriptorCollectorTest.java new file mode 100644 index 000000000..9e1e30005 --- /dev/null +++ b/protobuf/src/test/java/dev/cel/protobuf/ProtoDescriptorCollectorTest.java @@ -0,0 +1,75 @@ +// 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.protobuf; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableList; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.expr.conformance.proto3.NestedTestAllTypes; +import dev.cel.expr.conformance.proto3.TestAllTypes; +import dev.cel.testing.testdata.MultiFile; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public class ProtoDescriptorCollectorTest { + + @Test + public void collectCodegenMetadata_containsAllDescriptors() { + ProtoDescriptorCollector collector = + ProtoDescriptorCollector.newInstance(DebugPrinter.newInstance(false)); + + ImmutableList testAllTypesDescriptors = + collector.collectCodegenMetadata(TestAllTypes.getDescriptor()); + ImmutableList nestedTestAllTypesDescriptors = + collector.collectCodegenMetadata(NestedTestAllTypes.getDescriptor()); + + // All proto messages, including transitive ones + maps + assertThat(testAllTypesDescriptors).hasSize(165); + assertThat(nestedTestAllTypesDescriptors).hasSize(1); + } + + @Test + public void collectCodegenMetadata_withProtoDependencies_containsAllDescriptors() { + ProtoDescriptorCollector collector = + ProtoDescriptorCollector.newInstance(DebugPrinter.newInstance(false)); + + ImmutableList descriptors = + collector.collectCodegenMetadata(MultiFile.getDescriptor()); + + assertThat(descriptors).hasSize(3); + assertThat( + descriptors.stream() + .filter(d -> d.getProtoTypeName().equals("dev.cel.testing.testdata.MultiFile")) + .findAny()) + .isPresent(); + } + + @Test + public void collectCodegenMetadata_withProtoDependencies_doesNotContainImportedProto() { + ProtoDescriptorCollector collector = + ProtoDescriptorCollector.newInstance(DebugPrinter.newInstance(false)); + + ImmutableList descriptors = + collector.collectCodegenMetadata(MultiFile.getDescriptor()); + + assertThat( + descriptors.stream() + .filter(d -> d.getProtoTypeName().equals("dev.cel.testing.testdata.SingleFile")) + .findAny()) + .isEmpty(); + } +} diff --git a/publish/BUILD.bazel b/publish/BUILD.bazel index 4f16dc50c..69622aada 100644 --- a/publish/BUILD.bazel +++ b/publish/BUILD.bazel @@ -1,62 +1,165 @@ -load("@rules_jvm_external//:defs.bzl", "java_export") load("@bazel_common//tools/maven:pom_file.bzl", "pom_file") +load("@rules_jvm_external//:defs.bzl", "java_export") load("//publish:cel_version.bzl", "CEL_VERSION") # Note: These targets must reference the build targets in `src` directly in # order to properly generate the maven dependencies in pom.xml. +# keep sorted +COMMON_TARGETS = [ + "//common/src/main/java/dev/cel/common:cel_ast", + "//common/src/main/java/dev/cel/common:cel_descriptor_util", + "//common/src/main/java/dev/cel/common:cel_exception", + "//common/src/main/java/dev/cel/common:cel_source", + "//common/src/main/java/dev/cel/common:container", + "//common/src/main/java/dev/cel/common:error_codes", + "//common/src/main/java/dev/cel/common:operator", + "//common/src/main/java/dev/cel/common:options", + "//common/src/main/java/dev/cel/common:source", + "//common/src/main/java/dev/cel/common/internal", + "//common/src/main/java/dev/cel/common/internal:cel_descriptor_pools", + "//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", +] + +# keep sorted RUNTIME_TARGETS = [ "//runtime/src/main/java/dev/cel/runtime", "//runtime/src/main/java/dev/cel/runtime:base", "//runtime/src/main/java/dev/cel/runtime:interpreter", - "//runtime/src/main/java/dev/cel/runtime:runtime_helper", + "//runtime/src/main/java/dev/cel/runtime:late_function_binding", + "//runtime/src/main/java/dev/cel/runtime:runtime_factory", + "//runtime/src/main/java/dev/cel/runtime:runtime_helpers", + "//runtime/src/main/java/dev/cel/runtime:runtime_legacy_impl", + "//runtime/src/main/java/dev/cel/runtime:standard_functions", "//runtime/src/main/java/dev/cel/runtime:unknown_attributes", ] +# keep sorted +LITE_RUNTIME_TARGETS = [ + "//common/src/main/java/dev/cel/common:proto_ast_android", # Note: included due to generated protos requiring protolite dependency + "//common/src/main/java/dev/cel/common/values:proto_message_lite_value_provider_android", + "//protobuf/src/main/java/dev/cel/protobuf:cel_lite_descriptor", + "//runtime/src/main/java/dev/cel/runtime:lite_runtime_android", + "//runtime/src/main/java/dev/cel/runtime:lite_runtime_factory_android", + "//runtime/src/main/java/dev/cel/runtime:lite_runtime_library_android", + "//runtime/src/main/java/dev/cel/runtime:standard_functions_android", +] + +# keep sorted COMPILER_TARGETS = [ - "//parser/src/main/java/dev/cel/parser", - "//parser/src/main/java/dev/cel/parser:parser_builder", "//checker/src/main/java/dev/cel/checker:checker", "//checker/src/main/java/dev/cel/checker:checker_builder", - "//checker/src/main/java/dev/cel/checker:proto_type_mask", "//checker/src/main/java/dev/cel/checker:proto_expr_visitor", + "//checker/src/main/java/dev/cel/checker:proto_type_mask", "//compiler/src/main/java/dev/cel/compiler", "//compiler/src/main/java/dev/cel/compiler:compiler_builder", + "//parser/src/main/java/dev/cel/parser", + "//parser/src/main/java/dev/cel/parser:parser_builder", + "//parser/src/main/java/dev/cel/parser:parser_factory", + "//parser/src/main/java/dev/cel/parser:unparser", ] +# keep sorted VALIDATOR_TARGETS = [ "//validator/src/main/java/dev/cel/validator", - "//validator/src/main/java/dev/cel/validator:validator_builder", "//validator/src/main/java/dev/cel/validator:ast_validator", + "//validator/src/main/java/dev/cel/validator:validator_builder", "//validator/src/main/java/dev/cel/validator:validator_impl", - "//validator/src/main/java/dev/cel/validator/validators:timestamp", "//validator/src/main/java/dev/cel/validator/validators:duration", - "//validator/src/main/java/dev/cel/validator/validators:regex", "//validator/src/main/java/dev/cel/validator/validators:homogeneous_literal", + "//validator/src/main/java/dev/cel/validator/validators:regex", + "//validator/src/main/java/dev/cel/validator/validators:timestamp", ] +# keep sorted OPTIMIZER_TARGETS = [ "//optimizer/src/main/java/dev/cel/optimizer", - "//optimizer/src/main/java/dev/cel/optimizer:optimizer_builder", "//optimizer/src/main/java/dev/cel/optimizer:ast_optimizer", - "//optimizer/src/main/java/dev/cel/optimizer:optimization_exception", "//optimizer/src/main/java/dev/cel/optimizer:mutable_ast", + "//optimizer/src/main/java/dev/cel/optimizer:optimization_exception", + "//optimizer/src/main/java/dev/cel/optimizer:optimizer_builder", "//optimizer/src/main/java/dev/cel/optimizer:optimizer_impl", + "//optimizer/src/main/java/dev/cel/optimizer/optimizers:common_subexpression_elimination", "//optimizer/src/main/java/dev/cel/optimizer/optimizers:constant_folding", + "//optimizer/src/main/java/dev/cel/optimizer/optimizers:inlining", +] + +# keep sorted +POLICY_COMPILER_TARGETS = [ + "//policy/src/main/java/dev/cel/policy:compiled_rule", + "//policy/src/main/java/dev/cel/policy:compiler", + "//policy/src/main/java/dev/cel/policy:compiler_builder", + "//policy/src/main/java/dev/cel/policy:compiler_factory", + "//policy/src/main/java/dev/cel/policy:parser", + "//policy/src/main/java/dev/cel/policy:parser_builder", + "//policy/src/main/java/dev/cel/policy:parser_factory", + "//policy/src/main/java/dev/cel/policy:policy", + "//policy/src/main/java/dev/cel/policy:policy_parser_context", + "//policy/src/main/java/dev/cel/policy:source", + "//policy/src/main/java/dev/cel/policy:validation_exception", + "//policy/src/main/java/dev/cel/policy:yaml_parser", ] -V1ALPHA1_UTILITY_TARGETS = [ +# keep sorted +V1ALPHA1_AST_TARGETS = [ "//common/src/main/java/dev/cel/common:proto_v1alpha1_ast", ] +# keep sorted +CANONICAL_AST_TARGETS = [ + "//common/src/main/java/dev/cel/common:proto_ast", +] + +# keep sorted EXTENSION_TARGETS = [ "//extensions/src/main/java/dev/cel/extensions", "//extensions/src/main/java/dev/cel/extensions:optional_library", ] -ALL_TARGETS = [ +# keep sorted +BUNDLE_TARGETS = [ "//bundle/src/main/java/dev/cel/bundle:cel", -] + RUNTIME_TARGETS + COMPILER_TARGETS + EXTENSION_TARGETS + V1ALPHA1_UTILITY_TARGETS + OPTIMIZER_TARGETS + VALIDATOR_TARGETS + "//bundle/src/main/java/dev/cel/bundle:environment", + "//bundle/src/main/java/dev/cel/bundle:environment_yaml_parser", +] + +CEL_MISC_TARGETS = BUNDLE_TARGETS + EXTENSION_TARGETS + OPTIMIZER_TARGETS + VALIDATOR_TARGETS + POLICY_COMPILER_TARGETS + +# Excluded from the JAR as their source of truth is elsewhere +EXCLUDED_TARGETS = [ + "@com_google_googleapis//google/api/expr/v1alpha1:expr_java_proto", +] + +JAVA_DOC_OPTIONS = [ + "-Xdoclint:none", + "--ignore-source-errors", +] + +pom_file( + name = "cel_common_pom", + substitutions = { + "CEL_VERSION": CEL_VERSION, + "CEL_ARTIFACT_ID": "common", + "PACKAGE_NAME": "CEL Java Common", + "PACKAGE_DESC": "Common dependencies for Common Expression Language for Java.", + }, + targets = COMMON_TARGETS, + template_file = "pom_template.xml", +) + +java_export( + name = "cel_common", + deploy_env = EXCLUDED_TARGETS, + javadocopts = JAVA_DOC_OPTIONS, + maven_coordinates = "dev.cel:common:%s" % CEL_VERSION, + pom_template = ":cel_common_pom", + exports = COMMON_TARGETS, +) pom_file( name = "cel_pom", @@ -66,18 +169,29 @@ pom_file( "PACKAGE_NAME": "CEL Java", "PACKAGE_DESC": "Common Expression Language for Java. This include both the compilation and runtime packages.", }, - targets = ALL_TARGETS, + targets = [ + ":cel_common", + ":cel_protobuf", + ":cel_compiler", + ":cel_runtime", + ":cel_v1alpha1", + ] + CEL_MISC_TARGETS, template_file = "pom_template.xml", ) java_export( name = "cel", - deploy_env = [ - "@com_google_googleapis//google/api/expr/v1alpha1:expr_java_proto", - ], + deploy_env = EXCLUDED_TARGETS, + javadocopts = JAVA_DOC_OPTIONS, maven_coordinates = "dev.cel:cel:%s" % CEL_VERSION, pom_template = ":cel_pom", - runtime_deps = ALL_TARGETS, + exports = [ + ":cel_common", + ":cel_compiler", + ":cel_protobuf", + ":cel_runtime", + ":cel_v1alpha1", + ] + CEL_MISC_TARGETS, ) pom_file( @@ -88,18 +202,23 @@ pom_file( "PACKAGE_NAME": "CEL Java Compiler", "PACKAGE_DESC": "Common Expression Language Compiler for Java", }, - targets = COMPILER_TARGETS, + targets = [ + ":cel_common", + ":cel_protobuf", + ] + COMPILER_TARGETS, template_file = "pom_template.xml", ) java_export( name = "cel_compiler", - deploy_env = [ - "@com_google_googleapis//google/api/expr/v1alpha1:expr_java_proto", - ], + deploy_env = EXCLUDED_TARGETS, + javadocopts = JAVA_DOC_OPTIONS, maven_coordinates = "dev.cel:compiler:%s" % CEL_VERSION, pom_template = ":cel_compiler_pom", - runtime_deps = COMPILER_TARGETS, + exports = [ + ":cel_common", + ":cel_protobuf", + ] + COMPILER_TARGETS, ) pom_file( @@ -110,104 +229,90 @@ pom_file( "PACKAGE_NAME": "CEL Java Runtime", "PACKAGE_DESC": "Common Expression Language Runtime for Java", }, - targets = RUNTIME_TARGETS, + targets = [ + ":cel_common", + ] + RUNTIME_TARGETS, template_file = "pom_template.xml", ) java_export( name = "cel_runtime", - deploy_env = [ - "@com_google_googleapis//google/api/expr/v1alpha1:expr_java_proto", - ], + deploy_env = EXCLUDED_TARGETS, + javadocopts = JAVA_DOC_OPTIONS, maven_coordinates = "dev.cel:runtime:%s" % CEL_VERSION, pom_template = ":cel_runtime_pom", - runtime_deps = RUNTIME_TARGETS, + exports = [ + ":cel_common", + ] + RUNTIME_TARGETS, ) pom_file( - name = "cel_extensions_pom", - substitutions = { - "CEL_VERSION": CEL_VERSION, - "CEL_ARTIFACT_ID": "extensions", - "PACKAGE_NAME": "CEL Java Extensions", - "PACKAGE_DESC": "Common Expression Language Extensions for Java", - }, - targets = EXTENSION_TARGETS, - template_file = "pom_template.xml", -) - -java_export( - name = "cel_extensions", - deploy_env = [ - "@com_google_googleapis//google/api/expr/v1alpha1:expr_java_proto", - ], - maven_coordinates = "dev.cel:extensions:%s" % CEL_VERSION, - pom_template = ":cel_extensions_pom", - runtime_deps = EXTENSION_TARGETS, -) - -pom_file( - name = "cel_validators_pom", + name = "cel_v1alpha1_pom", substitutions = { "CEL_VERSION": CEL_VERSION, - "CEL_ARTIFACT_ID": "validators", - "PACKAGE_NAME": "CEL Java Validators", - "PACKAGE_DESC": "Common Expression Language Validators for Java", + "CEL_ARTIFACT_ID": "v1alpha1", + "PACKAGE_NAME": "CEL Java v1alpha1 Utility", + "PACKAGE_DESC": "Common Expression Language Utility for supporting v1alpha1 protobuf definitions", }, - targets = VALIDATOR_TARGETS, + targets = [ + ":cel_common", + ] + V1ALPHA1_AST_TARGETS, template_file = "pom_template.xml", ) java_export( - name = "cel_validators", - deploy_env = [ - "@com_google_googleapis//google/api/expr/v1alpha1:expr_java_proto", - ], - maven_coordinates = "dev.cel:validators:%s" % CEL_VERSION, - pom_template = ":cel_validators_pom", - runtime_deps = VALIDATOR_TARGETS, + name = "cel_v1alpha1", + deploy_env = EXCLUDED_TARGETS, + javadocopts = JAVA_DOC_OPTIONS, + maven_coordinates = "dev.cel:v1alpha1:%s" % CEL_VERSION, + pom_template = ":cel_v1alpha1_pom", + exports = [ + ":cel_common", + ] + V1ALPHA1_AST_TARGETS, ) pom_file( - name = "cel_optimizers_pom", + name = "cel_protobuf_pom", substitutions = { "CEL_VERSION": CEL_VERSION, - "CEL_ARTIFACT_ID": "optimizers", - "PACKAGE_NAME": "CEL Java Optimizers", - "PACKAGE_DESC": "Common Expression Language Optimizers for Java", + "CEL_ARTIFACT_ID": "protobuf", + "PACKAGE_NAME": "CEL Java Protobuf adapter", + "PACKAGE_DESC": "Common Expression Language Adapter for converting canonical cel.expr protobuf definitions", }, - targets = OPTIMIZER_TARGETS, + targets = [ + ":cel_common", + ] + CANONICAL_AST_TARGETS, template_file = "pom_template.xml", ) java_export( - name = "cel_optimizers", - deploy_env = [ - "@com_google_googleapis//google/api/expr/v1alpha1:expr_java_proto", - ], - maven_coordinates = "dev.cel:optimizers:%s" % CEL_VERSION, - pom_template = ":cel_optimizers_pom", - runtime_deps = OPTIMIZER_TARGETS, + name = "cel_protobuf", + deploy_env = EXCLUDED_TARGETS, + javadocopts = JAVA_DOC_OPTIONS, + maven_coordinates = "dev.cel:protobuf:%s" % CEL_VERSION, + pom_template = ":cel_protobuf_pom", + exports = [ + ":cel_common", + ] + CANONICAL_AST_TARGETS, ) pom_file( - name = "cel_v1alpha1_pom", + name = "cel_runtime_android_pom", substitutions = { "CEL_VERSION": CEL_VERSION, - "CEL_ARTIFACT_ID": "v1alpha1", - "PACKAGE_NAME": "CEL Java v1alpha1 Utility", - "PACKAGE_DESC": "Common Expression Language Utility for supporting v1alpha1 protobuf definitions", + "CEL_ARTIFACT_ID": "runtime-android", + "PACKAGE_NAME": "CEL Java Runtime for Android", + "PACKAGE_DESC": "Common Expression Language Lite Runtime for Java (Suitable for Android)", }, - targets = V1ALPHA1_UTILITY_TARGETS, + targets = LITE_RUNTIME_TARGETS, template_file = "pom_template.xml", ) java_export( - name = "cel_v1alpha1", - deploy_env = [ - "@com_google_googleapis//google/api/expr/v1alpha1:expr_java_proto", - ], - maven_coordinates = "dev.cel:v1alpha1:%s" % CEL_VERSION, - pom_template = ":cel_v1alpha1_pom", - runtime_deps = V1ALPHA1_UTILITY_TARGETS, + name = "cel_runtime_android", + deploy_env = EXCLUDED_TARGETS, + javadocopts = JAVA_DOC_OPTIONS, + maven_coordinates = "dev.cel:runtime-android:%s" % CEL_VERSION, + pom_template = ":cel_runtime_android_pom", + exports = LITE_RUNTIME_TARGETS, ) diff --git a/publish/cel_version.bzl b/publish/cel_version.bzl index b918db871..4ceb4bfa4 100644 --- a/publish/cel_version.bzl +++ b/publish/cel_version.bzl @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. """Maven artifact version for CEL.""" -CEL_VERSION = "0.3.0" +CEL_VERSION = "0.13.1" diff --git a/publish/pom_template.xml b/publish/pom_template.xml index 41b4aa4bb..ec5c08be6 100644 --- a/publish/pom_template.xml +++ b/publish/pom_template.xml @@ -20,7 +20,7 @@ CEL_ARTIFACT_ID - {generated_bzl_deps} + {dependencies} @@ -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 - \ No newline at end of file + diff --git a/publish/publish.sh b/publish/publish.sh index 0814b16df..28d0f0f53 100755 --- a/publish/publish.sh +++ b/publish/publish.sh @@ -24,26 +24,59 @@ # 1. You must create a pgp certificate and upload it to keyserver.ubuntu.com. See https://blog.sonatype.com/2010/01/how-to-generate-pgp-signatures-with-maven/ # 2. You will need to enter the key's password. The prompt appears in GUI, not in terminal. The publish operation will eventually timeout if the password is not entered. +# Note, to run script: Bazel and jq are required -ALL_TARGETS=("//publish:cel.publish" "//publish:cel_compiler.publish" "//publish:cel_runtime.publish" "//publish:cel_extensions.publish" "//publish:cel_optimizers.publish" "//publish:cel_validators.publish" "//publish:cel_v1alpha1.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") +JDK8_FLAGS="--java_language_version=8 --java_runtime_version=8" function publish_maven_remote() { maven_repo_url=$1 - # Credentials should be read from maven config (settings.xml) once it - # is supported by bazelbuild: - # https://github.com/bazelbuild/rules_jvm_external/issues/679 - read -p "maven_user: " maven_user - read -s -p "maven_password: " maven_password - for PUBLISH_TARGET in "${ALL_TARGETS[@]}" - do - bazel run --stamp \ - --define "maven_repo=$maven_repo_url" \ - --define gpg_sign=true \ - --define "maven_user=$maven_user" \ - --define "maven_password=$maven_password" \ - $PUBLISH_TARGET - done + # Credentials should be read from maven config (settings.xml) once it + # is supported by bazelbuild: + # https://github.com/bazelbuild/rules_jvm_external/issues/679 + read -p "maven_user: " maven_user + read -s -p "maven_password: " maven_password + # Upload artifacts to staging repository + for PUBLISH_TARGET in "${ALL_TARGETS[@]}" + do + bazel run --stamp \ + --define "maven_repo=$maven_repo_url" \ + --define gpg_sign=true \ + --define "maven_user=$maven_user" \ + --define "maven_password=$maven_password" \ + $PUBLISH_TARGET \ + $JDK8_FLAGS + done + + # Begin creating a staging deployment in central maven + auth_token=$(printf "%s:%s" "$maven_user" "$maven_password" | base64) + repository_key=$(curl -s -X GET \ + -H "Authorization:Bearer $auth_token" \ + "https://ossrh-staging-api.central.sonatype.com/manual/search/repositories?ip=any&profile_id=dev.cel" | \ + jq -r '.repositories[] | select(.state=="open") | .key' | head -n 1) + echo "" + if [[ -n "$repository_key" ]]; then + echo "Open repository key:" + echo "$repository_key" + + echo "Creating deployment..." + post_response=$(curl -s -w "\n%{http_code}" -X POST \ + -H "Authorization: Bearer $auth_token" \ + "https://ossrh-staging-api.central.sonatype.com/manual/upload/repository/$repository_key") + + http_code=$(tail -n1 <<< "$post_response") + response_body=$(sed '$ d' <<< "$post_response") + + echo "----------------------------------------" + echo "Deployment API Response (HTTP Status: $http_code):" + echo "$response_body" + echo "----------------------------------------" + echo "" + echo "Proceed to https://central.sonatype.com/publishing/deployments to finalize publishing." + else + echo "No open repository was found. Likely an indication that artifacts were not uploaded." + fi } version=$(CEL Library Internals. Do Not Use. + */ +@Internal +public final class AccumulatedUnknowns { + private static final int MAX_UNKNOWN_ATTRIBUTE_SIZE = 500_000; + private final Set exprIds; + private final Set attributes; + + Set exprIds() { + return exprIds; + } + + 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 + public AccumulatedUnknowns merge(AccumulatedUnknowns arg) { + enforceMaxAttributeSize(this.attributes, arg.attributes); + this.exprIds.addAll(arg.exprIds); + this.attributes.addAll(arg.attributes); + return this; + } + + static AccumulatedUnknowns create(Long... ids) { + return create(Arrays.asList(ids)); + } + + static AccumulatedUnknowns create(Collection ids) { + return create(ids, new ArrayList<>()); + } + + public static AccumulatedUnknowns create( + Collection exprIds, Collection attributes) { + return new AccumulatedUnknowns(new HashSet<>(exprIds), new HashSet<>(attributes)); + } + + private static void enforceMaxAttributeSize( + Set lhsAttributes, Set rhsAttributes) { + if (lhsAttributes.size() + rhsAttributes.size() > MAX_UNKNOWN_ATTRIBUTE_SIZE) { + throw new IllegalArgumentException( + String.format( + "Exceeded maximum allowed unknown attributes when merging: %s, %s", + lhsAttributes.size(), rhsAttributes.size())); + } + } + + private AccumulatedUnknowns(Set exprIds, Set attributes) { + this.exprIds = exprIds; + this.attributes = attributes; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/Activation.java b/runtime/src/main/java/dev/cel/runtime/Activation.java index c51e77e0c..074a461a2 100644 --- a/runtime/src/main/java/dev/cel/runtime/Activation.java +++ b/runtime/src/main/java/dev/cel/runtime/Activation.java @@ -20,18 +20,9 @@ import com.google.common.collect.ImmutableMap; import com.google.protobuf.ByteString; import com.google.protobuf.ByteString.ByteIterator; -import com.google.protobuf.Descriptors.FieldDescriptor; -import com.google.protobuf.Message; -import dev.cel.common.CelOptions; -import dev.cel.common.ExprFeatures; import dev.cel.common.annotations.Internal; -import dev.cel.common.internal.DefaultMessageFactory; -import dev.cel.common.internal.DynamicProto; -import dev.cel.common.internal.ProtoAdapter; -import java.util.HashMap; import java.util.Map; -import java.util.Optional; -import org.jspecify.nullness.Nullable; +import org.jspecify.annotations.Nullable; /** * An object which allows to bind names to values. @@ -51,12 +42,12 @@ public abstract class Activation implements GlobalResolver { @Override public @Nullable Object resolve(String name) { - return null; + return GlobalResolver.EMPTY.resolve(name); } @Override public String toString() { - return "{}"; + return GlobalResolver.EMPTY.toString(); } }; @@ -137,60 +128,6 @@ public String toString() { }; } - /** - * Creates an {@code Activation} from a {@code Message} where each field in the message is exposed - * as a top-level variable in the {@code Activation}. - * - *

Unset message fields are published with the default value for the field type. However, an - * unset {@code google.protobuf.Any} value is not a valid CEL value, and will be published as an - * {@code Exception} value on the {@code Activation} just as though an unset {@code Any} would if - * it were accessed during a CEL evaluation. - * - *

Note, this call does not support unsigned integer fields properly and encodes them as long - * values. If {@link ExprFeatures#ENABLE_UNSIGNED_LONGS} is in use, use {@link #fromProto(Message, - * CelOptions)} to ensure that the message fields are properly designated as {@code UnsignedLong} - * values. - */ - public static Activation fromProto(Message message) { - return fromProto(message, CelOptions.LEGACY); - } - - /** - * Creates an {@code Activation} from a {@code Message} where each field in the message is exposed - * as a top-level variable in the {@code Activation}. - * - *

Unset message fields are published with the default value for the field type. However, an - * unset {@code google.protobuf.Any} value is not a valid CEL value, and will be published as an - * {@code Exception} value on the {@code Activation} just as though an unset {@code Any} would if - * it were accessed during a CEL evaluation. - */ - public static Activation fromProto(Message message, CelOptions celOptions) { - Map variables = new HashMap<>(); - Map msgFieldValues = message.getAllFields(); - - ProtoAdapter protoAdapter = - new ProtoAdapter( - DynamicProto.create(DefaultMessageFactory.INSTANCE), celOptions.enableUnsignedLongs()); - - for (FieldDescriptor field : message.getDescriptorForType().getFields()) { - // Get the value of the field set on the message, if present, otherwise use reflection to - // get the default value for the field using the FieldDescriptor. - Object fieldValue = msgFieldValues.getOrDefault(field, message.getField(field)); - try { - Optional adapted = protoAdapter.adaptFieldToValue(field, fieldValue); - variables.put(field.getName(), adapted.orElse(null)); - } catch (IllegalArgumentException e) { - variables.put( - field.getName(), - new InterpreterException.Builder( - "illegal field value. field=%s, value=%s", field.getName(), fieldValue) - .setCause(e) - .build()); - } - } - return copyOf(variables); - } - /** * Extends this binder by another binder. Names will be attempted to first resolve in the other * binder, then in this binder. diff --git a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel index f9d3dc6a2..17160e346 100644 --- a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel @@ -1,3 +1,6 @@ +load("@rules_java//java:defs.bzl", "java_library") +load("//:cel_android_rules.bzl", "cel_android_library") + package( default_applicable_licenses = ["//:license"], default_visibility = [ @@ -6,261 +9,1419 @@ package( ], ) +# keep sorted BASE_SOURCES = [ "DefaultMetadata.java", - "IncompleteData.java", - "InterpreterException.java", "MessageProvider.java", - "Metadata.java", "Registrar.java", - "StandardFunctions.java", - "StandardTypeResolver.java", - "TypeResolver.java", ] +# keep sorted INTERPRETER_SOURCES = [ - "Activation.java", "CallArgumentChecker.java", - "DefaultDispatcher.java", "DefaultInterpreter.java", + "Interpreter.java", + "RuntimeUnknownResolver.java", + "UnknownTrackingInterpretable.java", +] + +# keep sorted +DESCRIPTOR_MESSAGE_PROVIDER_SOURCES = [ "DescriptorMessageProvider.java", - "Dispatcher.java", "DynamicMessageFactory.java", + "MessageFactory.java", +] + +# keep sorted +LITE_RUNTIME_SOURCES = [ + "CelLiteRuntime.java", + "CelLiteRuntimeBuilder.java", + "CelLiteRuntimeLibrary.java", +] + +# keep sorted +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 +INTERPRABLE_SOURCES = [ "GlobalResolver.java", "Interpretable.java", - "Interpreter.java", - "InterpreterUtil.java", - "MessageFactory.java", - "PartialMessage.java", - "PartialMessageOrBuilder.java", - "RuntimeTypeProvider.java", - "RuntimeUnknownResolver.java", - "UnknownTrackingInterpretable.java", +] + +# keep sorted +DISPATCHER_SOURCES = [ + "DefaultDispatcher.java", ] java_library( - name = "base", - srcs = BASE_SOURCES, + name = "runtime_type_provider", + srcs = ["RuntimeTypeProvider.java"], tags = [ ], deps = [ - ":runtime_helper", - "//:auto_value", - "//common", - "//common:error_codes", - "//common:options", - "//common:runtime_exception", + ":base", + "//common/annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +cel_android_library( + name = "runtime_type_provider_android", + srcs = ["RuntimeTypeProvider.java"], + visibility = ["//visibility:private"], + deps = [ + ":base_android", "//common/annotations", - "//common/internal:comparison_functions", - "//common/internal:default_message_factory", - "//common/internal:dynamic_proto", - "//common/internal:safe_string_formatter", - "//common/types", - "//common/types:type_providers", - "@cel_spec//proto/cel/expr:expr_java_proto", "@maven//:com_google_errorprone_error_prone_annotations", - "@maven//:com_google_guava_guava", - "@maven//:com_google_protobuf_protobuf_java", - "@maven//:com_google_protobuf_protobuf_java_util", - "@maven//:com_google_re2j_re2j", - "@maven//:org_jspecify_jspecify", ], ) java_library( - name = "interpreter", - srcs = INTERPRETER_SOURCES, - deprecation = "Please use CEL-Java Fluent APIs //runtime:runtime instead", + name = "descriptor_message_provider", + srcs = DESCRIPTOR_MESSAGE_PROVIDER_SOURCES, tags = [ ], - exports = [":base"], deps = [ - ":base", - ":evaluation_listener", - ":runtime_helper", - ":unknown_attributes", - "//:auto_value", - "//common", - "//common:error_codes", - "//common:features", + ":runtime_type_provider", + "//common:cel_descriptor_util", + "//common:cel_descriptors", "//common:options", - "//common:runtime_exception", "//common/annotations", - "//common/ast", + "//common/exceptions:attribute_not_found", "//common/internal:cel_descriptor_pools", "//common/internal:default_message_factory", "//common/internal:dynamic_proto", "//common/internal:proto_message_factory", "//common/types:cel_types", - "//common/types:type_providers", - "@cel_spec//proto/cel/expr:expr_java_proto", - "@maven//:com_google_code_findbugs_annotations", + "//common/values:cel_byte_string", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", - "@maven//:com_google_protobuf_protobuf_java_util", "@maven//:org_jspecify_jspecify", ], ) java_library( - name = "runtime_helper", - srcs = [ - "RuntimeEquality.java", - "RuntimeHelpers.java", - ], + name = "dispatcher", + srcs = DISPATCHER_SOURCES, tags = [ ], - # NOTE: do not grow this dependencies arbitrarily deps = [ + ":evaluation_exception", + ":evaluation_exception_builder", + ":function_binding", + ":function_overload", + ":function_resolver", + ":resolved_overload", + "//:auto_value", "//common:error_codes", - "//common:options", - "//common:runtime_exception", "//common/annotations", - "//common/internal:comparison_functions", - "//common/internal:converter", - "//common/internal:dynamic_proto", - "//common/internal:proto_equality", + "@maven//:com_google_code_findbugs_annotations", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", - "@maven//:com_google_protobuf_protobuf_java", - "@maven//:com_google_re2j_re2j", - "@maven//:org_threeten_threeten_extra", ], ) -# keep sorted -RUNTIME_SOURCES = [ - "CelEvaluationException.java", - "CelFunctionOverload.java", - "CelRuntime.java", - "CelRuntimeBuilder.java", - "CelRuntimeFactory.java", - "CelRuntimeLegacyImpl.java", - "CelRuntimeLibrary.java", - "CelVariableResolver.java", - "UnknownContext.java", -] - -java_library( - name = "runtime", - srcs = RUNTIME_SOURCES, +cel_android_library( + name = "dispatcher_android", + srcs = DISPATCHER_SOURCES, tags = [ ], deps = [ - ":evaluation_listener", - ":runtime_type_provider_legacy", - ":unknown_attributes", + ":evaluation_exception", + ":evaluation_exception_builder", + ":function_binding_android", + ":function_overload_android", + ":function_resolver_android", + ":resolved_overload_android", "//:auto_value", - "//common", "//common:error_codes", - "//common:options", "//common/annotations", - "//common/internal:cel_descriptor_pools", - "//common/internal:default_message_factory", - "//common/internal:dynamic_proto", - "//common/internal:proto_message_factory", - "//common/types:cel_types", - "//common/values:cel_value_provider", - "//common/values:proto_message_value_provider", - "//runtime:interpreter", "@maven//:com_google_code_findbugs_annotations", "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", + ], +) + +java_library( + name = "activation", + srcs = ["Activation.java"], + tags = [ + ], + deps = [ + ":interpretable", + ":runtime_helpers", + "//common/annotations", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", "@maven//:org_jspecify_jspecify", ], ) -# keep sorted -UNKNOWN_ATTRIBUTE_SOURCES = [ - "CelAttribute.java", - "CelAttributePattern.java", - "CelAttributeResolver.java", - "CelUnknownSet.java", -] +cel_android_library( + name = "activation_android", + srcs = ["Activation.java"], + tags = [ + ], + deps = [ + ":interpretable_android", + ":runtime_helpers_android", + "//common/annotations", + "@maven//:org_jspecify_jspecify", + "@maven_android//:com_google_guava_guava", + "@maven_android//:com_google_protobuf_protobuf_javalite", + ], +) -# keep sorted -UNKNOWN_OPTIONS_SOURCES = [ - "CelAttributeParser.java", -] +java_library( + name = "proto_message_activation_factory", + srcs = ["ProtoMessageActivationFactory.java"], + tags = [ + ], + deps = [ + ":activation", + ":evaluation_exception_builder", + "//common:options", + "//common/internal:default_message_factory", + "//common/internal:dynamic_proto", + "@maven//:com_google_protobuf_protobuf_java", + ], +) java_library( - name = "unknown_options", - srcs = UNKNOWN_OPTIONS_SOURCES, + name = "type_resolver", + srcs = ["TypeResolver.java"], tags = [ ], deps = [ - ":unknown_attributes", - "//common", - "//common:compiler_common", - "//parser", - "//parser:operator", - "//parser:parser_builder", - "@cel_spec//proto/cel/expr:expr_java_proto", + "//common/annotations", + "//common/types", + "//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", ], ) -java_library( - name = "unknown_attributes", - srcs = UNKNOWN_ATTRIBUTE_SOURCES, +cel_android_library( + name = "type_resolver_android", + srcs = ["TypeResolver.java"], tags = [ ], deps = [ - "//:auto_value", + "//common/annotations", + "//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", - "@maven//:com_google_re2j_re2j", + "@maven_android//:com_google_guava_guava", + "@maven_android//:com_google_protobuf_protobuf_javalite", ], ) java_library( - name = "runtime_type_provider_legacy", - srcs = ["RuntimeTypeProviderLegacyImpl.java"], + name = "descriptor_type_resolver", + srcs = ["DescriptorTypeResolver.java"], + tags = [ + ], deps = [ - ":unknown_attributes", - "//common:options", + ":type_resolver", "//common/annotations", - "//common/internal:cel_descriptor_pools", - "//common/internal:dynamic_proto", "//common/types", "//common/types:type_providers", "//common/values", - "//common/values:cel_value", - "//common/values:cel_value_provider", - "//common/values:proto_message_value", - "//runtime:interpreter", - "@cel_spec//proto/cel/expr:expr_java_proto", "@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 = "interpreter_util", - srcs = ["InterpreterUtil.java"], + name = "base", + srcs = BASE_SOURCES, tags = [ ], deps = [ - ":base", - "//common:error_codes", + ":function_overload", + ":metadata", + "//common:cel_ast", "//common/annotations", - "@cel_spec//proto/cel/expr:expr_java_proto", "@maven//:com_google_errorprone_error_prone_annotations", - "@maven//:org_jspecify_jspecify", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "base_android", + srcs = BASE_SOURCES, + visibility = ["//visibility:private"], + deps = [ + ":function_overload_android", + ":metadata", + "//common:cel_ast_android", + "//common/annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", ], ) java_library( - name = "evaluation_listener", - srcs = ["CelEvaluationListener.java"], + name = "interpreter", + srcs = INTERPRETER_SOURCES, tags = [ ], + exports = [":base"], deps = [ + ":accumulated_unknowns", + ":base", + ":concatenated_list_view", + ":dispatcher", + ":evaluation_exception", + ":evaluation_exception_builder", + ":evaluation_listener", + ":function_resolver", + ":interpretable", + ":interpreter_util", + ":metadata", + ":resolved_overload", + ":runtime_helpers", + ":runtime_type_provider", + ":type_resolver", + ":unknown_attributes", + "//:auto_value", + "//common:cel_ast", + "//common:error_codes", + "//common:options", + "//common/annotations", "//common/ast", + "//common/exceptions:runtime_exception", + "//common/types", + "//common/types:type_providers", + "//common/values:cel_byte_string", + "@maven//:com_google_code_findbugs_annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + ], +) + +cel_android_library( + name = "interpreter_android", + srcs = INTERPRETER_SOURCES, + visibility = ["//visibility:private"], + deps = [ + ":accumulated_unknowns_android", + ":base_android", + ":concatenated_list_view", + ":dispatcher_android", + ":evaluation_exception", + ":evaluation_exception_builder", + ":evaluation_listener_android", + ":function_resolver_android", + ":interpretable_android", + ":interpreter_util_android", + ":metadata", + ":resolved_overload_android", + ":runtime_helpers_android", + ":runtime_type_provider_android", + ":type_resolver_android", + ":unknown_attributes_android", + "//:auto_value", + "//common:cel_ast_android", + "//common:error_codes", + "//common:options", + "//common/annotations", + "//common/ast:ast_android", + "//common/exceptions:runtime_exception", + "//common/types:type_providers_android", + "//common/types:types_android", + "//common/values:cel_byte_string", "@maven//:com_google_code_findbugs_annotations", "@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", + ], +) + +java_library( + name = "runtime_equality", + srcs = [ + "RuntimeEquality.java", + ], + tags = [ + ], + deps = [ + ":runtime_helpers", + "//common:options", + "//common/annotations", + "//common/exceptions:attribute_not_found", + "//common/internal:comparison_functions", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + ], +) + +cel_android_library( + name = "runtime_equality_android", + srcs = ["RuntimeEquality.java"], + tags = [ + ], + deps = [ + ":runtime_helpers_android", + "//common:options", + "//common/annotations", + "//common/exceptions:attribute_not_found", + "//common/internal:comparison_functions_android", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", + "@maven_android//:com_google_protobuf_protobuf_javalite", + ], +) + +java_library( + name = "proto_message_runtime_equality", + srcs = [ + "ProtoMessageRuntimeEquality.java", + ], + tags = [ + ], + deps = [ + ":proto_message_runtime_helpers", + ":runtime_equality", + "//common:options", + "//common/annotations", + "//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", + ], +) + +cel_android_library( + name = "runtime_helpers_android", + srcs = ["RuntimeHelpers.java"], + tags = [ + ], + deps = [ + ":concatenated_list_view", + "//common:options", + "//common/annotations", + "//common/exceptions:divide_by_zero", + "//common/exceptions:index_out_of_bounds", + "//common/exceptions:numeric_overflow", + "//common/internal:converter", + "//common/values:values_android", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_re2j_re2j", + "@maven//:org_threeten_threeten_extra", + "@maven_android//:com_google_guava_guava", + "@maven_android//:com_google_protobuf_protobuf_javalite", + ], +) + +java_library( + name = "runtime_helpers", + srcs = [ + "RuntimeHelpers.java", + ], + tags = [ + ], + deps = [ + ":concatenated_list_view", + "//common:options", + "//common/annotations", + "//common/exceptions:divide_by_zero", + "//common/exceptions:index_out_of_bounds", + "//common/exceptions:numeric_overflow", + "//common/internal:converter", + "//common/values", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + "@maven//:com_google_re2j_re2j", + "@maven//:org_threeten_threeten_extra", + ], +) + +java_library( + name = "proto_message_runtime_helpers", + srcs = [ + "ProtoMessageRuntimeHelpers.java", + ], + tags = [ + ], + deps = [ + ":runtime_helpers", + "//common:options", + "//common/annotations", + "//common/internal:dynamic_proto", + "@maven//:com_google_protobuf_protobuf_java", + ], +) + +# keep sorted +RUNTIME_SOURCES = [ + "CelInternalRuntimeLibrary.java", + "CelRuntime.java", + "CelRuntimeBuilder.java", + "CelRuntimeLibrary.java", + "ProgramImpl.java", + "UnknownContext.java", +] + +LATE_FUNCTION_BINDING_SOURCES = [ + "CelLateFunctionBindings.java", +] + +java_library( + name = "late_function_binding", + srcs = LATE_FUNCTION_BINDING_SOURCES, + tags = [ + ], + deps = [ + ":dispatcher", + ":evaluation_exception", + ":function_binding", + ":function_resolver", + ":resolved_overload", + "//:auto_value", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "late_function_binding_android", + srcs = LATE_FUNCTION_BINDING_SOURCES, + tags = [ + ], + deps = [ + ":dispatcher_android", + ":evaluation_exception", + ":function_binding_android", + ":function_resolver_android", + ":resolved_overload_android", + "//:auto_value", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", + ], +) + +java_library( + name = "lite_runtime_library", + srcs = ["CelLiteRuntimeLibrary.java"], + deps = [":lite_runtime"], +) + +cel_android_library( + name = "lite_runtime_library_android", + srcs = ["CelLiteRuntimeLibrary.java"], + deps = [":lite_runtime_android"], +) + +java_library( + name = "evaluation_exception", + srcs = [ + "CelEvaluationException.java", + ], + # used_by_android + tags = [ + ], + deps = [ + "//common:cel_exception", + "//common:error_codes", + "@maven//:org_jspecify_jspecify", + ], +) + +java_library( + name = "evaluation_exception_builder", + srcs = ["CelEvaluationExceptionBuilder.java"], + # used_by_android + tags = [ + ], + deps = [ + ":evaluation_exception", + ":metadata", + "//common:error_codes", + "//common/annotations", + "//common/exceptions:runtime_exception", + "//common/internal:safe_string_formatter", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:org_jspecify_jspecify", + ], +) + +java_library( + name = "metadata", + srcs = ["Metadata.java"], + # used_by_android + tags = [ + ], + deps = [ + "//common/annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +java_library( + name = "interpretable", + srcs = INTERPRABLE_SOURCES, + tags = [ + ], + deps = [ + ":evaluation_exception", + ":evaluation_listener", + ":function_resolver", + "//common/annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:org_jspecify_jspecify", + ], +) + +cel_android_library( + name = "interpretable_android", + srcs = INTERPRABLE_SOURCES, + tags = [ + ], + deps = [ + ":evaluation_exception", + ":evaluation_listener_android", + ":function_resolver_android", + "//common/annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:org_jspecify_jspecify", + ], +) + +java_library( + name = "standard_functions", + srcs = ["CelStandardFunctions.java"], + tags = [ + ], + deps = [ + ":function_binding", + ":runtime_equality", + "//common:operator", + "//common:options", + "//common/annotations", + "//runtime/standard:add", + "//runtime/standard:bool", + "//runtime/standard:bytes", + "//runtime/standard:contains", + "//runtime/standard:divide", + "//runtime/standard:double", + "//runtime/standard:duration", + "//runtime/standard:dyn", + "//runtime/standard:ends_with", + "//runtime/standard:equals", + "//runtime/standard:get_date", + "//runtime/standard:get_day_of_month", + "//runtime/standard:get_day_of_week", + "//runtime/standard:get_day_of_year", + "//runtime/standard:get_full_year", + "//runtime/standard:get_hours", + "//runtime/standard:get_milliseconds", + "//runtime/standard:get_minutes", + "//runtime/standard:get_month", + "//runtime/standard:get_seconds", + "//runtime/standard:greater", + "//runtime/standard:greater_equals", + "//runtime/standard:in", + "//runtime/standard:index", + "//runtime/standard:int", + "//runtime/standard:less", + "//runtime/standard:less_equals", + "//runtime/standard:logical_not", + "//runtime/standard:matches", + "//runtime/standard:modulo", + "//runtime/standard:multiply", + "//runtime/standard:negate", + "//runtime/standard:not_equals", + "//runtime/standard:not_strictly_false", + "//runtime/standard:size", + "//runtime/standard:standard_function", + "//runtime/standard:standard_overload", + "//runtime/standard:starts_with", + "//runtime/standard:string", + "//runtime/standard:subtract", + "//runtime/standard:timestamp", + "//runtime/standard:uint", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "standard_functions_android", + srcs = ["CelStandardFunctions.java"], + tags = [ + ], + deps = [ + ":function_binding_android", + ":runtime_equality_android", + "//common:operator_android", + "//common:options", + "//common/annotations", + "//runtime/standard:add_android", + "//runtime/standard:bool_android", + "//runtime/standard:bytes_android", + "//runtime/standard:contains_android", + "//runtime/standard:divide_android", + "//runtime/standard:double_android", + "//runtime/standard:duration_android", + "//runtime/standard:dyn_android", + "//runtime/standard:ends_with_android", + "//runtime/standard:equals_android", + "//runtime/standard:get_date_android", + "//runtime/standard:get_day_of_month_android", + "//runtime/standard:get_day_of_week_android", + "//runtime/standard:get_day_of_year_android", + "//runtime/standard:get_full_year_android", + "//runtime/standard:get_hours_android", + "//runtime/standard:get_milliseconds_android", + "//runtime/standard:get_minutes_android", + "//runtime/standard:get_month_android", + "//runtime/standard:get_seconds_android", + "//runtime/standard:greater_android", + "//runtime/standard:greater_equals_android", + "//runtime/standard:in_android", + "//runtime/standard:index_android", + "//runtime/standard:int_android", + "//runtime/standard:less_android", + "//runtime/standard:less_equals_android", + "//runtime/standard:logical_not_android", + "//runtime/standard:matches_android", + "//runtime/standard:modulo_android", + "//runtime/standard:multiply_android", + "//runtime/standard:negate_android", + "//runtime/standard:not_equals_android", + "//runtime/standard:not_strictly_false_android", + "//runtime/standard:size_android", + "//runtime/standard:standard_function_android", + "//runtime/standard:standard_overload_android", + "//runtime/standard:starts_with_android", + "//runtime/standard:string_android", + "//runtime/standard:subtract_android", + "//runtime/standard:timestamp_android", + "//runtime/standard:uint_android", + "@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", + ], +) + +java_library( + name = "function_binding", + srcs = FUNCTION_BINDING_SOURCES, + tags = [ + ], + deps = [ + ":evaluation_exception", + ":function_overload", + "//common/annotations", + "//common/exceptions:overload_not_found", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "function_binding_android", + srcs = FUNCTION_BINDING_SOURCES, + tags = [ + ], + 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", + ], +) + +java_library( + name = "function_resolver", + srcs = ["CelFunctionResolver.java"], + tags = [ + ], + deps = [ + ":evaluation_exception", + ":resolved_overload", + "@maven//:com_google_code_findbugs_annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +cel_android_library( + name = "function_resolver_android", + srcs = ["CelFunctionResolver.java"], + deps = [ + ":evaluation_exception", + ":resolved_overload_android", + "@maven//:com_google_code_findbugs_annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +java_library( + name = "function_overload", + srcs = [ + "CelFunctionOverload.java", + "OptimizedFunctionOverload.java", + ], + tags = [ + ], + deps = [ + ":evaluation_exception", + ":unknown_attributes", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "function_overload_android", + srcs = [ + "CelFunctionOverload.java", + "OptimizedFunctionOverload.java", + ], + deps = [ + ":evaluation_exception", + ":unknown_attributes_android", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", + ], +) + +java_library( + name = "runtime_planner_impl", + srcs = ["CelRuntimeImpl.java"], + tags = [ + ], + deps = [ + ":descriptor_type_resolver", + ":dispatcher", + ":evaluation_exception", + ":evaluation_listener", + ":function_binding", + ":function_resolver", + ":partial_vars", + ":program", + ":proto_message_runtime_equality", + ":runtime", + ":runtime_equality", + ":standard_functions", + ":variable_resolver", + "//:auto_value", + "//common:cel_ast", + "//common:cel_descriptor_util", + "//common:cel_descriptors", + "//common:container", + "//common:options", + "//common/annotations", + "//common/internal:cel_descriptor_pools", + "//common/internal:default_message_factory", + "//common/internal:dynamic_proto", + "//common/types:default_type_provider", + "//common/types:message_type_provider", + "//common/types:type_providers", + "//common/values", + "//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", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + "@maven//:org_jspecify_jspecify", + ], +) + +java_library( + name = "runtime_legacy_impl", + srcs = ["CelRuntimeLegacyImpl.java"], + tags = [ + ], + deps = [ + ":cel_value_runtime_type_provider", + ":descriptor_message_provider", + ":descriptor_type_resolver", + ":dispatcher", + ":function_binding", + ":interpreter", + ":proto_message_runtime_equality", + ":runtime", + ":runtime_equality", + ":runtime_type_provider", + ":standard_functions", + "//common:cel_ast", + "//common:cel_descriptor_util", + "//common:cel_descriptors", + "//common:container", + "//common:options", + "//common/annotations", + "//common/internal:cel_descriptor_pools", + "//common/internal:default_message_factory", + "//common/internal:dynamic_proto", + "//common/internal:proto_message_factory", + "//common/types:cel_types", + "//common/types:type_providers", + "//common/values", + "//common/values:cel_value_provider", + "//common/values:proto_message_value_provider", + "//runtime/standard:int", + "//runtime/standard:timestamp", + "@maven//:com_google_code_findbugs_annotations", + "@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 = "runtime_factory", + srcs = ["CelRuntimeFactory.java"], + tags = [ + ], + deps = [ + ":runtime", + ":runtime_legacy_impl", + ":runtime_planner_impl", + "//common:options", + ], +) + +java_library( + name = "runtime", + srcs = RUNTIME_SOURCES, + tags = [ + ], + deps = [ + ":activation", + ":evaluation_exception", + ":evaluation_listener", + ":function_binding", + ":function_resolver", + ":interpretable", + ":interpreter", + ":partial_vars", + ":program", + ":proto_message_activation_factory", + ":runtime_equality", + ":standard_functions", + ":unknown_attributes", + ":variable_resolver", + "//:auto_value", + "//common:cel_ast", + "//common:container", + "//common:options", + "//common/annotations", + "//common/types:type_providers", + "//common/values:cel_value_provider", + "@maven//:com_google_code_findbugs_annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + ], +) + +java_library( + name = "lite_runtime", + srcs = LITE_RUNTIME_SOURCES, + tags = [ + ], + deps = [ + ":evaluation_exception", + ":function_binding", + ":program", + "//:auto_value", + "//common:cel_ast", + "//common:options", + "//common/annotations", + "//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", + ], +) + +java_library( + name = "lite_runtime_impl", + srcs = LITE_RUNTIME_IMPL_SOURCES, + 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:options", + "//common/values:cel_value_provider", + "//runtime/standard:standard_function", + "@maven//:com_google_code_findbugs_annotations", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "lite_program_impl", + srcs = LITE_PROGRAM_IMPL_SOURCES, + deps = [ + ":activation", + ":evaluation_exception", + ":function_resolver", + ":interpretable", + ":partial_vars", + ":program", + ":variable_resolver", + "//:auto_value", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +cel_android_library( + name = "lite_program_impl_android", + srcs = LITE_PROGRAM_IMPL_SOURCES, + deps = [ + ":activation_android", + ":evaluation_exception", + ":function_resolver_android", + ":interpretable_android", + ":partial_vars_android", + ":program_android", + ":variable_resolver", + "//:auto_value", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +cel_android_library( + name = "lite_runtime_impl_android", + srcs = LITE_RUNTIME_IMPL_SOURCES, + 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:options", + "//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//:com_google_guava_guava", + "@maven_android//:com_google_guava_guava", + ], +) + +java_library( + name = "lite_runtime_factory", + srcs = [ + "CelLiteRuntimeFactory.java", + ], + tags = [ + ], + deps = [ + ":lite_runtime", + ":lite_runtime_impl", + "//common/annotations", + ], +) + +cel_android_library( + name = "lite_runtime_factory_android", + srcs = [ + "CelLiteRuntimeFactory.java", + ], + tags = [ + ], + deps = [ + ":lite_runtime_android", + ":lite_runtime_impl_android", + "//common/annotations", + ], +) + +# keep sorted +UNKNOWN_ATTRIBUTE_SOURCES = [ + "CelAttribute.java", + "CelAttributePattern.java", + "CelAttributeResolver.java", + "CelUnknownSet.java", +] + +# keep sorted +UNKNOWN_OPTIONS_SOURCES = [ + "CelAttributeParser.java", +] + +java_library( + name = "unknown_options", + srcs = UNKNOWN_OPTIONS_SOURCES, + tags = [ + ], + deps = [ + ":unknown_attributes", + "//common:cel_ast", + "//common:compiler_common", + "//common:operator", + "//common/ast", + "//parser:parser_builder", + "//parser:parser_factory", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "unknown_attributes", + srcs = UNKNOWN_ATTRIBUTE_SOURCES, + tags = [ + ], + deps = [ + "//:auto_value", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven//:com_google_re2j_re2j", + ], +) + +cel_android_library( + name = "unknown_attributes_android", + srcs = UNKNOWN_ATTRIBUTE_SOURCES, + tags = [ + ], + deps = [ + "//:auto_value", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_re2j_re2j", + "@maven_android//:com_google_guava_guava", + ], +) + +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"], + tags = [ + ], + deps = [ + ":accumulated_unknowns", + ":evaluation_exception", + ":unknown_attributes", + "//common/annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven//:org_jspecify_jspecify", + ], +) + +cel_android_library( + name = "interpreter_util_android", + srcs = ["InterpreterUtil.java"], + visibility = ["//visibility:private"], + deps = [ + ":accumulated_unknowns_android", + ":evaluation_exception", + ":unknown_attributes_android", + "//common/annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:org_jspecify_jspecify", + "@maven_android//:com_google_guava_guava", + ], +) + +java_library( + name = "evaluation_listener", + srcs = ["CelEvaluationListener.java"], + tags = [ + ], + deps = [ + "//common/ast", + "@maven//:com_google_code_findbugs_annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +cel_android_library( + name = "evaluation_listener_android", + srcs = ["CelEvaluationListener.java"], + visibility = ["//visibility:private"], + deps = [ + "//common/ast:ast_android", + "@maven//:com_google_code_findbugs_annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +cel_android_library( + name = "lite_runtime_android", + srcs = LITE_RUNTIME_SOURCES, + tags = [ + ], + deps = [ + ":evaluation_exception", + ":function_binding_android", + ":program_android", + "//:auto_value", + "//common:cel_ast_android", + "//common:options", + "//common/annotations", + "//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", + ], +) + +java_library( + name = "concatenated_list_view", + srcs = ["ConcatenatedListView.java"], + # used_by_android + tags = [ + ], + deps = ["//common/annotations"], +) + +java_library( + name = "accumulated_unknowns", + srcs = ["AccumulatedUnknowns.java"], + 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", + ], +) + +java_library( + name = "resolved_overload", + srcs = ["CelResolvedOverload.java"], + 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", + ], +) + +cel_android_library( + name = "resolved_overload_android", + srcs = ["CelResolvedOverload.java"], + 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_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "partial_vars_android", + srcs = ["PartialVars.java"], + tags = [ + ], + deps = [ + ":variable_resolver", + "//:auto_value", + "//runtime:unknown_attributes_android", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", + ], +) + +java_library( + name = "program", + srcs = ["Program.java"], + tags = [ + ], + deps = [ + ":evaluation_exception", + ":function_resolver", + ":partial_vars", + ":variable_resolver", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +cel_android_library( + name = "program_android", + srcs = ["Program.java"], + tags = [ + ], + deps = [ + ":evaluation_exception", + ":function_resolver_android", + ":partial_vars_android", + ":variable_resolver", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +java_library( + name = "internal_function_binder", + srcs = ["InternalFunctionBinder.java"], + tags = [ + ], + deps = [ + ":function_binding", + ":function_overload", + "//common/annotations", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "internal_function_binder_andriod", + srcs = ["InternalFunctionBinder.java"], + tags = [ + ], + deps = [ + ":function_binding_android", + ":function_overload_android", + "//common/annotations", + "@maven_android//:com_google_guava_guava", + ], +) + +java_library( + name = "variable_resolver", + srcs = [ + "CelVariableResolver.java", + "HierarchicalVariableResolver.java", + ], + # used_by_android + tags = [ ], ) diff --git a/runtime/src/main/java/dev/cel/runtime/CallArgumentChecker.java b/runtime/src/main/java/dev/cel/runtime/CallArgumentChecker.java index a3bd6e4b7..76a942927 100644 --- a/runtime/src/main/java/dev/cel/runtime/CallArgumentChecker.java +++ b/runtime/src/main/java/dev/cel/runtime/CallArgumentChecker.java @@ -14,8 +14,6 @@ package dev.cel.runtime; -import dev.cel.expr.ExprValue; -import dev.cel.expr.UnknownSet; import dev.cel.common.annotations.Internal; import java.util.ArrayList; import java.util.Optional; @@ -32,13 +30,13 @@ @Internal class CallArgumentChecker { private final ArrayList exprIds; - private Optional unknowns; private final RuntimeUnknownResolver resolver; private final boolean acceptPartial; + private Optional unknowns; private CallArgumentChecker(RuntimeUnknownResolver resolver, boolean acceptPartial) { - exprIds = new ArrayList<>(); - unknowns = Optional.empty(); + this.exprIds = new ArrayList<>(); + this.unknowns = Optional.empty(); this.resolver = resolver; this.acceptPartial = acceptPartial; } @@ -63,27 +61,31 @@ static CallArgumentChecker createAcceptingPartial(RuntimeUnknownResolver resolve return new CallArgumentChecker(resolver, true); } - private static Optional mergeOptionalUnknowns( - Optional lhs, Optional rhs) { + private static Optional mergeOptionalUnknowns( + Optional lhs, Optional rhs) { return lhs.isPresent() ? rhs.isPresent() ? Optional.of(lhs.get().merge(rhs.get())) : lhs : rhs; } /** Determine if the call argument is unknown and accumulate if so. */ void checkArg(DefaultInterpreter.IntermediateResult arg) { // Handle attribute tracked unknowns. - Optional argUnknowns = maybeUnknownFromArg(arg); + Optional argUnknowns = maybeUnknownFromArg(arg); unknowns = mergeOptionalUnknowns(unknowns, argUnknowns); // support for ExprValue unknowns. - if (InterpreterUtil.isUnknown(arg.value())) { - ExprValue exprValue = (ExprValue) arg.value(); - exprIds.addAll(exprValue.getUnknown().getExprsList()); + if (InterpreterUtil.isAccumulatedUnknowns(arg.value())) { + AccumulatedUnknowns unknownSet = (AccumulatedUnknowns) arg.value(); + exprIds.addAll(unknownSet.exprIds()); } } - private Optional maybeUnknownFromArg(DefaultInterpreter.IntermediateResult arg) { - if (arg.value() instanceof CelUnknownSet) { - return Optional.of((CelUnknownSet) arg.value()); + private Optional maybeUnknownFromArg( + DefaultInterpreter.IntermediateResult arg) { + if (arg.value() instanceof AccumulatedUnknowns) { + AccumulatedUnknowns celUnknownSet = (AccumulatedUnknowns) arg.value(); + if (!celUnknownSet.attributes().isEmpty()) { + return Optional.of((AccumulatedUnknowns) arg.value()); + } } if (!acceptPartial) { return resolver.maybePartialUnknown(arg.attribute()); @@ -98,8 +100,7 @@ Optional maybeUnknowns() { } if (!exprIds.isEmpty()) { - return Optional.of( - ExprValue.newBuilder().setUnknown(UnknownSet.newBuilder().addAllExprs(exprIds)).build()); + return Optional.of(AccumulatedUnknowns.create(exprIds)); } return Optional.empty(); diff --git a/runtime/src/main/java/dev/cel/runtime/CelAttribute.java b/runtime/src/main/java/dev/cel/runtime/CelAttribute.java index 6080dbaa1..f04418e0c 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/CelAttribute.java @@ -17,7 +17,6 @@ import com.google.auto.value.AutoOneOf; import com.google.auto.value.AutoValue; import com.google.common.base.Preconditions; -import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.primitives.UnsignedLong; import com.google.errorprone.annotations.Immutable; @@ -184,9 +183,13 @@ public static CelAttribute create(String rootIdentifier) { */ public static CelAttribute fromQualifiedIdentifier(String qualifiedIdentifier) { ImmutableList.Builder qualifiers = ImmutableList.builder(); - Splitter.on(".") - .split(qualifiedIdentifier) - .forEach((element) -> qualifiers.add(Qualifier.ofString(element))); + int start = 0; + int next; + while ((next = qualifiedIdentifier.indexOf('.', start)) != -1) { + qualifiers.add(Qualifier.ofString(qualifiedIdentifier.substring(start, next))); + start = next + 1; + } + qualifiers.add(Qualifier.ofString(qualifiedIdentifier.substring(start))); return new AutoValue_CelAttribute(qualifiers.build()); } @@ -206,7 +209,7 @@ public CelAttribute qualify(Qualifier qualifier) { return EMPTY; } return new AutoValue_CelAttribute( - ImmutableList.builder().addAll(qualifiers()).add(qualifier).build()); + ImmutableList.builderWithExpectedSize(qualifiers().size() + 1).addAll(qualifiers()).add(qualifier).build()); } @Override diff --git a/runtime/src/main/java/dev/cel/runtime/CelAttributeParser.java b/runtime/src/main/java/dev/cel/runtime/CelAttributeParser.java index 4ad4722da..c64250e4c 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelAttributeParser.java +++ b/runtime/src/main/java/dev/cel/runtime/CelAttributeParser.java @@ -16,18 +16,18 @@ import static com.google.common.collect.ImmutableList.toImmutableList; -import dev.cel.expr.Constant; -import dev.cel.expr.Expr; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; -import com.google.common.primitives.UnsignedLong; import dev.cel.common.CelAbstractSyntaxTree; -import dev.cel.common.CelProtoAbstractSyntaxTree; import dev.cel.common.CelValidationException; 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.ast.CelExpr.CelCall; +import dev.cel.common.ast.CelExpr.ExprKind.Kind; import dev.cel.parser.CelParser; import dev.cel.parser.CelParserFactory; -import dev.cel.parser.Operator; import java.util.ArrayDeque; /** @@ -78,19 +78,18 @@ private static String unescape(String s) { return b.toString(); } - private static CelAttribute.Qualifier parseConst(Constant constExpr) { - switch (constExpr.getConstantKindCase()) { - case BOOL_VALUE: - return CelAttribute.Qualifier.ofBool(constExpr.getBoolValue()); + private static CelAttribute.Qualifier parseConst(CelConstant constExpr) { + switch (constExpr.getKind()) { + case BOOLEAN_VALUE: + return CelAttribute.Qualifier.ofBool(constExpr.booleanValue()); case INT64_VALUE: - return CelAttribute.Qualifier.ofInt(constExpr.getInt64Value()); + return CelAttribute.Qualifier.ofInt(constExpr.int64Value()); case UINT64_VALUE: - return CelAttribute.Qualifier.ofUint(UnsignedLong.fromLongBits(constExpr.getUint64Value())); + return CelAttribute.Qualifier.ofUint(constExpr.uint64Value()); case STRING_VALUE: - return CelAttribute.Qualifier.ofString(unescape(constExpr.getStringValue())); + return CelAttribute.Qualifier.ofString(unescape(constExpr.stringValue())); default: - throw new IllegalArgumentException( - "Unsupported const expr kind: " + constExpr.getConstantKindCase()); + throw new IllegalArgumentException("Unsupported const expr kind: " + constExpr.getKind()); } } @@ -111,35 +110,34 @@ public static CelAttributePattern parsePattern(String attribute) { try { CelAbstractSyntaxTree ast = result.getAst(); ArrayDeque qualifiers = new ArrayDeque<>(); - // TODO: Traverse using CelExpr - Expr node = CelProtoAbstractSyntaxTree.fromCelAst(ast).getExpr(); + CelExpr node = ast.getExpr(); while (node != null) { - switch (node.getExprKindCase()) { - case IDENT_EXPR: - qualifiers.addFirst(CelAttribute.Qualifier.ofString(node.getIdentExpr().getName())); + switch (node.getKind()) { + case IDENT: + qualifiers.addFirst(CelAttribute.Qualifier.ofString(node.ident().name())); node = null; break; - case CALL_EXPR: - Expr.Call callExpr = node.getCallExpr(); - if (!callExpr.getFunction().equals(Operator.INDEX.getFunction()) - || callExpr.getArgsCount() != 2 - || !callExpr.getArgs(1).hasConstExpr()) { + case CALL: + CelCall callExpr = node.call(); + if (!callExpr.function().equals(Operator.INDEX.getFunction()) + || callExpr.args().size() != 2 + || !callExpr.args().get(1).getKind().equals(Kind.CONSTANT)) { throw new IllegalArgumentException( String.format( "Unsupported call expr: %s(%s)", - callExpr.getFunction(), + callExpr.function(), Joiner.on(", ") .join( - callExpr.getArgsList().stream() - .map(Expr::getExprKindCase) + callExpr.args().stream() + .map(CelExpr::getKind) .collect(toImmutableList())))); } - qualifiers.addFirst(parseConst(callExpr.getArgs(1).getConstExpr())); - node = callExpr.getArgs(0); + qualifiers.addFirst(parseConst(callExpr.args().get(1).constant())); + node = callExpr.args().get(0); break; - case SELECT_EXPR: - String field = node.getSelectExpr().getField(); - node = node.getSelectExpr().getOperand(); + case SELECT: + String field = node.select().field(); + node = node.select().operand(); if (field.equals("_" + WILDCARD_ESCAPE)) { qualifiers.addFirst(CelAttribute.Qualifier.ofWildCard()); break; @@ -148,7 +146,7 @@ public static CelAttributePattern parsePattern(String attribute) { break; default: throw new IllegalArgumentException( - "Unsupported expr kind in attribute: " + node.getExprKindCase()); + "Unsupported expr kind in attribute: " + node.exprKind()); } } return CelAttributePattern.create(ImmutableList.copyOf(qualifiers)); 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/CelEvaluationException.java b/runtime/src/main/java/dev/cel/runtime/CelEvaluationException.java index 313077ecf..e4c028360 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelEvaluationException.java +++ b/runtime/src/main/java/dev/cel/runtime/CelEvaluationException.java @@ -16,6 +16,7 @@ import dev.cel.common.CelErrorCode; import dev.cel.common.CelException; +import org.jspecify.annotations.Nullable; /** * CelEvaluationException encapsulates the potential issues which could arise during the @@ -27,15 +28,11 @@ public CelEvaluationException(String message) { super(message); } - public CelEvaluationException(String message, Throwable cause) { + public CelEvaluationException(String message, @Nullable Throwable cause) { super(message, cause); } - CelEvaluationException(InterpreterException cause) { - super(cause.getMessage(), cause.getCause()); - } - - CelEvaluationException(InterpreterException cause, CelErrorCode errorCode) { - super(cause.getMessage(), cause.getCause(), errorCode); + CelEvaluationException(String message, @Nullable Throwable cause, CelErrorCode errorCode) { + super(message, cause, errorCode); } } diff --git a/runtime/src/main/java/dev/cel/runtime/CelEvaluationExceptionBuilder.java b/runtime/src/main/java/dev/cel/runtime/CelEvaluationExceptionBuilder.java new file mode 100644 index 000000000..986788bac --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/CelEvaluationExceptionBuilder.java @@ -0,0 +1,102 @@ +// 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.errorprone.annotations.CanIgnoreReturnValue; +import dev.cel.common.CelErrorCode; +import dev.cel.common.annotations.Internal; +import dev.cel.common.exceptions.CelRuntimeException; +import dev.cel.common.internal.SafeStringFormatter; +import org.jspecify.annotations.Nullable; + +/** CEL Library Internals. Do not use. */ +@Internal +public final class CelEvaluationExceptionBuilder { + + private String message = ""; + private Throwable cause; + private CelErrorCode errorCode; + private String errorLocation; + + @CanIgnoreReturnValue + public CelEvaluationExceptionBuilder setCause(@Nullable Throwable cause) { + this.cause = cause; + return this; + } + + @CanIgnoreReturnValue + public CelEvaluationExceptionBuilder setErrorCode(CelErrorCode errorCode) { + this.errorCode = errorCode; + return this; + } + + @CanIgnoreReturnValue + public CelEvaluationExceptionBuilder setMetadata(Metadata metadata, long exprId) { + if (metadata.hasPosition(exprId)) { + this.errorLocation = + SafeStringFormatter.format( + " at %s:%d", metadata.getLocation(), metadata.getPosition(exprId)); + } + + return this; + } + + /** + * Constructs a new {@link CelEvaluationException} instance. + * + *

CEL Library Internals. Do not use. + */ + @Internal + public CelEvaluationException build() { + return new CelEvaluationException( + SafeStringFormatter.format("evaluation error%s: %s", errorLocation, message), + cause, + errorCode); + } + + /** + * Constructs a new builder for {@link CelEvaluationException} + * + *

CEL Library Internals. Do not use. + */ + @Internal + public static CelEvaluationExceptionBuilder newBuilder(String message, Object... args) { + return new CelEvaluationExceptionBuilder(SafeStringFormatter.format(message, args)); + } + + /** + * Constructs a new builder for {@link CelEvaluationException} + * + *

CEL Library Internals. Do not use. + */ + @Internal + public static CelEvaluationExceptionBuilder newBuilder(CelRuntimeException celRuntimeException) { + // Intercept the cause to prevent including the cause's class name in the exception message. + String message = + celRuntimeException.getCause() == null + ? celRuntimeException.getMessage() + : celRuntimeException.getCause().getMessage(); + + return new CelEvaluationExceptionBuilder(message) + .setErrorCode(celRuntimeException.getErrorCode()) + .setCause(celRuntimeException); + } + + private CelEvaluationExceptionBuilder(String message) { + this.message = message == null ? "" : message; + this.errorCode = CelErrorCode.INTERNAL_ERROR; + this.errorLocation = ""; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/CelEvaluationListener.java b/runtime/src/main/java/dev/cel/runtime/CelEvaluationListener.java index 6c00390cc..0afc1f5e2 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelEvaluationListener.java +++ b/runtime/src/main/java/dev/cel/runtime/CelEvaluationListener.java @@ -32,9 +32,4 @@ public interface CelEvaluationListener { * @param evaluatedResult Evaluated result. */ void callback(CelExpr expr, Object evaluatedResult); - - /** Construct a listener that does nothing. */ - static CelEvaluationListener noOpListener() { - return (arg1, arg2) -> {}; - } } diff --git a/runtime/src/main/java/dev/cel/runtime/CelFunctionBinding.java b/runtime/src/main/java/dev/cel/runtime/CelFunctionBinding.java new file mode 100644 index 000000000..98991d383 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/CelFunctionBinding.java @@ -0,0 +1,122 @@ +// 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 static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.errorprone.annotations.Immutable; +import java.util.Collection; + +/** + * Binding consisting of an overload id, a Java-native argument signature, and an overload + * definition. + * + *

While the CEL function has a human-readable {@code camelCase} name, overload ids should use + * the following convention where all {@code } names should be ASCII lower-cased. For types + * prefer the unparameterized simple name of time, or unqualified name of any proto-based type: + * + *

    + *
  • unary member function: _ + *
  • binary member function: __ + *
  • unary global function: _ + *
  • binary global function: __ + *
  • global function: ___ + *
+ * + *

Examples: string_startsWith_string, mathMax_list, lessThan_money_money + */ +@Immutable +public interface CelFunctionBinding { + String getOverloadId(); + + ImmutableList> getArgTypes(); + + CelFunctionOverload getDefinition(); + + boolean isStrict(); + + /** Create a unary function binding from the {@code overloadId}, {@code arg}, and {@code impl}. */ + @SuppressWarnings("unchecked") // Safe from CelFunctionOverload.canHandle check before invocation + static CelFunctionBinding from( + String overloadId, Class arg, CelFunctionOverload.Unary impl) { + return from( + overloadId, + ImmutableList.of(arg), + new 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") // Safe from CelFunctionOverload.canHandle check before invocation + static CelFunctionBinding from( + String overloadId, Class arg1, Class arg2, CelFunctionOverload.Binary impl) { + return from( + overloadId, + ImmutableList.of(arg1, arg2), + new 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}. */ + static CelFunctionBinding from( + String overloadId, Iterable> argTypes, CelFunctionOverload impl) { + return new FunctionBindingImpl( + overloadId, ImmutableList.copyOf(argTypes), impl, /* isStrict= */ true); + } + + + /** See {@link #fromOverloads(String, Collection)}. */ + static ImmutableSet fromOverloads( + String functionName, CelFunctionBinding... overloadBindings) { + return fromOverloads(functionName, ImmutableList.copyOf(overloadBindings)); + } + + /** + * Creates a set of bindings for a function, enabling dynamic dispatch logic to select the correct + * overload at runtime based on argument types. + */ + 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."); + + 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 2ea1cb529..c5f75096d 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelFunctionOverload.java +++ b/runtime/src/main/java/dev/cel/runtime/CelFunctionOverload.java @@ -1,4 +1,4 @@ -// Copyright 2022 Google LLC +// 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. @@ -14,16 +14,19 @@ package dev.cel.runtime; +import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.Immutable; +import java.util.Map; /** Interface describing the general signature of all CEL custom function implementations. */ @Immutable @FunctionalInterface public interface CelFunctionOverload { - /** Evaluate a set of arguments throwing a {@code CelEvaluationException} on error. */ + /** 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. @@ -43,4 +46,58 @@ interface Unary { interface Binary { Object apply(T1 arg1, T2 arg2) throws CelEvaluationException; } + + /** + * Returns true if the overload's expected argument types match the types of the given arguments. + */ + static boolean canHandle( + Object[] arguments, ImmutableList> parameterTypes, boolean isStrict) { + if (parameterTypes.size() != arguments.length) { + return false; + } + for (int i = 0; i < parameterTypes.size(); i++) { + Class paramType = parameterTypes.get(i); + Object arg = arguments[i]; + boolean result = canHandleArg(arg, paramType, isStrict); + if (!result) { + return false; + } + } + return true; + } + + static boolean canHandle(Object arg, ImmutableList> parameterTypes, boolean isStrict) { + if (parameterTypes.size() != 1) { + return false; + } + return canHandleArg(arg, parameterTypes.get(0), isStrict); + } + + static boolean canHandle( + Object arg1, Object arg2, ImmutableList> parameterTypes, boolean isStrict) { + if (parameterTypes.size() != 2) { + return false; + } + return canHandleArg(arg1, parameterTypes.get(0), isStrict) + && canHandleArg(arg2, parameterTypes.get(1), isStrict); + } + + 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; + } + + 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 new file mode 100644 index 000000000..2fb136a1a --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/CelFunctionResolver.java @@ -0,0 +1,53 @@ +// 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. + +package dev.cel.runtime; + +import javax.annotation.concurrent.ThreadSafe; +import java.util.Collection; +import java.util.Optional; + +/** + * Interface to a resolver for CEL functions based on the function name, overload ids, and + * arguments. + */ +@ThreadSafe +public interface CelFunctionResolver { + + /** + * Finds a specific function overload to invoke based on given parameters. + * + * @param functionName the logical name of the function being invoked. + * @param overloadIds A list of function overload ids. The dispatcher selects the unique overload + * 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, + */ + Optional findOverloadMatchingArgs( + String functionName, Collection overloadIds, Object[] args) + throws CelEvaluationException; + + /** + * Finds a specific function overload to invoke based on given parameters, scanning all available + * overloads if necessary. + * + * @param functionName the logical name of the function being invoked. + * @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. + */ + Optional findOverloadMatchingArgs(String functionName, Object[] args) + throws CelEvaluationException; +} diff --git a/runtime/src/main/java/dev/cel/runtime/CelInternalRuntimeLibrary.java b/runtime/src/main/java/dev/cel/runtime/CelInternalRuntimeLibrary.java new file mode 100644 index 000000000..b6a6f02b2 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/CelInternalRuntimeLibrary.java @@ -0,0 +1,35 @@ +// 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 dev.cel.common.CelOptions; +import dev.cel.common.annotations.Internal; + +/** + * CelInternalRuntimeLibrary defines the interface to extend functionalities beyond the CEL standard + * functions for {@link CelRuntime}, with access to runtime internals. This is not intended for + * general use. + * + *

CEL Library Internals. Do Not Use. + */ +@Internal +public interface CelInternalRuntimeLibrary extends CelRuntimeLibrary { + + /** + * Configures the runtime to support the library implementation, such as adding function bindings. + */ + void setRuntimeOptions( + CelRuntimeBuilder runtimeBuilder, RuntimeEquality runtimeEquality, CelOptions celOptions); +} diff --git a/runtime/src/main/java/dev/cel/runtime/CelLateFunctionBindings.java b/runtime/src/main/java/dev/cel/runtime/CelLateFunctionBindings.java new file mode 100644 index 000000000..2da08120c --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/CelLateFunctionBindings.java @@ -0,0 +1,77 @@ +// 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. + +package dev.cel.runtime; + +import static com.google.common.collect.ImmutableMap.toImmutableMap; + +import com.google.common.collect.ImmutableMap; +import com.google.errorprone.annotations.Immutable; +import java.util.Arrays; +import java.util.Collection; +import java.util.Optional; + +/** + * Collection of {@link CelFunctionBinding} values which are intended to be created once + * per-evaluation, rather than once per-program setup. + */ +@Immutable +public final class CelLateFunctionBindings implements CelFunctionResolver { + + private final ImmutableMap functions; + + private CelLateFunctionBindings(ImmutableMap functions) { + this.functions = functions; + } + + @Override + public Optional findOverloadMatchingArgs( + String functionName, Collection overloadIds, Object[] args) + throws CelEvaluationException { + return DefaultDispatcher.findOverloadMatchingArgs(functionName, overloadIds, functions, args); + } + + @Override + public Optional findOverloadMatchingArgs(String functionName, Object[] args) + throws CelEvaluationException { + return DefaultDispatcher.findOverloadMatchingArgs( + functionName, functions.keySet(), functions, args); + } + + public static CelLateFunctionBindings from(CelFunctionBinding... functions) { + return from(Arrays.asList(functions)); + } + + public static CelLateFunctionBindings from(Collection functions) { + return new CelLateFunctionBindings( + functions.stream() + .collect( + toImmutableMap( + CelFunctionBinding::getOverloadId, + CelLateFunctionBindings::createResolvedOverload))); + } + + private static CelResolvedOverload createResolvedOverload(CelFunctionBinding binding) { + String functionName = binding.getOverloadId(); + if (binding instanceof InternalCelFunctionBinding) { + functionName = ((InternalCelFunctionBinding) binding).getFunctionName(); + } + return CelResolvedOverload.of( + functionName, + binding.getOverloadId(), + binding.getDefinition(), + binding.isStrict(), + binding.getArgTypes()); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/CelLiteRuntime.java b/runtime/src/main/java/dev/cel/runtime/CelLiteRuntime.java new file mode 100644 index 000000000..af8e918f6 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/CelLiteRuntime.java @@ -0,0 +1,35 @@ +// 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 javax.annotation.concurrent.ThreadSafe; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.annotations.Beta; + +/** + * CelLiteRuntime creates executable {@link Program} instances from {@link CelAbstractSyntaxTree} + * values. + * + *

CelLiteRuntime supports protolite messages, and does not directly depend on full-version of + * the protobuf, making it suitable for use in Android. + */ +@ThreadSafe +@Beta +public interface CelLiteRuntime { + + Program createProgram(CelAbstractSyntaxTree ast) throws CelEvaluationException; + + CelLiteRuntimeBuilder toRuntimeBuilder(); +} diff --git a/runtime/src/main/java/dev/cel/runtime/CelLiteRuntimeBuilder.java b/runtime/src/main/java/dev/cel/runtime/CelLiteRuntimeBuilder.java new file mode 100644 index 000000000..48b51274d --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/CelLiteRuntimeBuilder.java @@ -0,0 +1,67 @@ +// 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.errorprone.annotations.CanIgnoreReturnValue; +import com.google.errorprone.annotations.CheckReturnValue; +import dev.cel.common.CelOptions; +import dev.cel.common.values.CelValueProvider; +import dev.cel.runtime.standard.CelStandardFunction; + +/** Interface for building an instance of {@link CelLiteRuntime} */ +public interface CelLiteRuntimeBuilder { + + /** Set the {@code CelOptions} used to enable fixes and features for this CEL instance. */ + @CanIgnoreReturnValue + CelLiteRuntimeBuilder setOptions(CelOptions options); + + /** + * Set the standard functions to enable in the runtime. These can be found in {@code + * dev.cel.runtime.standard} package. By default, lite runtime does not include any standard + * functions on its own. + */ + @CanIgnoreReturnValue + CelLiteRuntimeBuilder setStandardFunctions(CelStandardFunction... standardFunctions); + + @CanIgnoreReturnValue + CelLiteRuntimeBuilder setStandardFunctions( + Iterable standardFunctions); + + /** Add one or more {@link CelFunctionBinding} objects to the CEL runtime. */ + @CanIgnoreReturnValue + CelLiteRuntimeBuilder addFunctionBindings(CelFunctionBinding... bindings); + + /** Bind a collection of {@link CelFunctionBinding} objects to the runtime. */ + @CanIgnoreReturnValue + CelLiteRuntimeBuilder addFunctionBindings(Iterable bindings); + + /** + * Sets the {@link CelValueProvider} for resolving struct values during evaluation. Multiple + * providers can be combined using {@code CombinedCelValueProvider}. Note that if you intend to + * support proto messages in addition to custom struct values, protobuf value provider must be + * configured first before the custom value provider. + */ + @CanIgnoreReturnValue + CelLiteRuntimeBuilder setValueProvider(CelValueProvider celValueProvider); + + @CanIgnoreReturnValue + CelLiteRuntimeBuilder addLibraries(CelLiteRuntimeLibrary... libraries); + + @CanIgnoreReturnValue + CelLiteRuntimeBuilder addLibraries(Iterable libraries); + + @CheckReturnValue + CelLiteRuntime build(); +} diff --git a/runtime/src/main/java/dev/cel/runtime/CelLiteRuntimeFactory.java b/runtime/src/main/java/dev/cel/runtime/CelLiteRuntimeFactory.java new file mode 100644 index 000000000..260a62980 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/CelLiteRuntimeFactory.java @@ -0,0 +1,29 @@ +// 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 dev.cel.common.annotations.Beta; + +/** Factory class for producing a lite runtime environment. */ +@Beta +public final class CelLiteRuntimeFactory { + + /** Create a new builder for constructing a {@code CelLiteRuntime} instance. */ + public static CelLiteRuntimeBuilder newLiteRuntimeBuilder() { + return LiteRuntimeImpl.newBuilder(); + } + + private CelLiteRuntimeFactory() {} +} diff --git a/runtime/src/main/java/dev/cel/runtime/CelLiteRuntimeLibrary.java b/runtime/src/main/java/dev/cel/runtime/CelLiteRuntimeLibrary.java new file mode 100644 index 000000000..fff9ed93d --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/CelLiteRuntimeLibrary.java @@ -0,0 +1,27 @@ +// 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; + +/** + * CelLiteRuntimeLibrary defines the interface to extend functionalities beyond the CEL standard + * functions for {@link CelLiteRuntime}. + */ +public interface CelLiteRuntimeLibrary { + + /** + * Configures the runtime to support the library implementation, such as adding function bindings. + */ + void setRuntimeOptions(CelLiteRuntimeBuilder runtimeBuilder); +} diff --git a/runtime/src/main/java/dev/cel/runtime/CelResolvedOverload.java b/runtime/src/main/java/dev/cel/runtime/CelResolvedOverload.java new file mode 100644 index 000000000..fbe9a3289 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/CelResolvedOverload.java @@ -0,0 +1,132 @@ +// 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. + +package dev.cel.runtime; + +import com.google.auto.value.AutoValue; +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; + +/** + * Representation of a function overload which has been resolved to a specific set of argument types + * and a function definition. + */ +@AutoValue +@Immutable +@Internal +public abstract class CelResolvedOverload { + + /** The base function name. */ + public abstract String getFunctionName(); + + /** The overload id of the function. */ + public abstract String getOverloadId(); + + /** The types of the function parameters. */ + public abstract ImmutableList> getParameterTypes(); + + /* Denotes whether an overload is strict. + * + *

A strict function will not be invoked if any of its arguments are an error or unknown value. + * The runtime automatically propagates the error or unknown instead. + * + *

A non-strict function will be invoked even if its arguments contain errors or unknowns. The + * function's implementation is then responsible for handling these values. This is primarily used + * for short-circuiting logical operators (e.g., `||`, `&&`) and comprehension's + * internal @not_strictly_false function. + * + *

In a vast majority of cases, this should be set to true. + */ + public abstract boolean isStrict(); + + /** 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 function name, overload id, parameter types, and + * definition. + */ + public static CelResolvedOverload of( + String functionName, + String overloadId, + CelFunctionOverload definition, + boolean isStrict, + Class... parameterTypes) { + return of(functionName, overloadId, definition, isStrict, ImmutableList.copyOf(parameterTypes)); + } + + /** + * 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( + functionName, + overloadId, + ImmutableList.copyOf(parameterTypes), + isStrict, + definition, + optimizedDef); + } + + /** + * Returns true if the overload's expected argument types match the types of the given arguments. + */ + 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 2b28d015b..1e7fdcac8 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntime.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntime.java @@ -14,11 +14,7 @@ package dev.cel.runtime; -import com.google.auto.value.AutoValue; -import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.CanIgnoreReturnValue; -import com.google.errorprone.annotations.CheckReturnValue; import com.google.errorprone.annotations.Immutable; import javax.annotation.concurrent.ThreadSafe; import com.google.protobuf.Message; @@ -40,199 +36,77 @@ public interface CelRuntime { CelRuntimeBuilder toRuntimeBuilder(); /** Creates an evaluable {@code Program} instance which is thread-safe and immutable. */ - @AutoValue @Immutable - abstract class Program { - - /** Evaluate the expression without any variables. */ - public Object eval() throws CelEvaluationException { - return evalInternal(Activation.EMPTY); - } - - /** Evaluate the expression using a {@code mapValue} as the source of input variables. */ - public Object eval(Map mapValue) throws CelEvaluationException { - return evalInternal(Activation.copyOf(mapValue)); - } + interface Program extends dev.cel.runtime.Program { /** Evaluate the expression using {@code message} fields as the source of input variables. */ - public Object eval(Message message) throws CelEvaluationException { - return evalInternal(Activation.fromProto(message, getOptions())); - } - - /** Evaluate a compiled program with a custom variable {@code resolver}. */ - public Object eval(CelVariableResolver resolver) throws CelEvaluationException { - return evalInternal((name) -> resolver.find(name).orElse(null)); - } + Object eval(Message message) throws CelEvaluationException; /** * Trace evaluates a compiled program without any variables and invokes the listener as * evaluation progresses through the AST. */ - public Object trace(CelEvaluationListener listener) throws CelEvaluationException { - return evalInternal(Activation.EMPTY, listener); - } + Object trace(CelEvaluationListener listener) throws CelEvaluationException; /** * Trace evaluates a compiled program using a {@code mapValue} as the source of input variables. * The listener is invoked as evaluation progresses through the AST. */ - public Object trace(Map mapValue, CelEvaluationListener listener) - throws CelEvaluationException { - return evalInternal(Activation.copyOf(mapValue), listener); - } + Object trace(Map mapValue, CelEvaluationListener listener) + throws CelEvaluationException; /** * Trace evaluates a compiled program using {@code message} fields as the source of input * variables. The listener is invoked as evaluation progresses through the AST. */ - public Object trace(Message message, CelEvaluationListener listener) - throws CelEvaluationException { - return evalInternal(Activation.fromProto(message, getOptions()), listener); - } + Object trace(Message message, CelEvaluationListener listener) throws CelEvaluationException; /** * Trace evaluates a compiled program using a custom variable {@code resolver}. The listener is * invoked as evaluation progresses through the AST. */ - public Object trace(CelVariableResolver resolver, CelEvaluationListener listener) - throws CelEvaluationException { - return evalInternal((name) -> resolver.find(name).orElse(null), listener); - } + Object trace(CelVariableResolver resolver, CelEvaluationListener listener) + throws CelEvaluationException; /** - * Advance evaluation based on the current unknown context. - * - *

This represents one round of incremental evaluation and may return a final result or a - * CelUnknownSet. - * - *

If no unknowns are declared in the context or {@link CelOptions#enableUnknownTracking() - * UnknownTracking} is disabled, this is equivalent to eval. - */ - public Object advanceEvaluation(UnknownContext context) throws CelEvaluationException { - return evalInternal(context, CelEvaluationListener.noOpListener()); - } - - private Object evalInternal(GlobalResolver resolver) throws CelEvaluationException { - return evalInternal(resolver, CelEvaluationListener.noOpListener()); - } - - private Object evalInternal(GlobalResolver resolver, CelEvaluationListener listener) - throws CelEvaluationException { - return evalInternal(UnknownContext.create(resolver), listener); - } - - /** - * Evaluate an expr node with an UnknownContext (an activation annotated with which attributes - * are unknown). + * Trace evaluates a compiled program using a custom variable {@code resolver} and late-bound + * functions {@code lateBoundFunctionResolver}. The listener is invoked as evaluation progresses + * through the AST. */ - private Object evalInternal(UnknownContext context, CelEvaluationListener listener) - throws CelEvaluationException { - try { - Interpretable impl = getInterpretable(); - if (getOptions().enableUnknownTracking()) { - Preconditions.checkState( - impl instanceof UnknownTrackingInterpretable, - "Environment misconfigured. Requested unknown tracking without a compatible" - + " implementation."); - - UnknownTrackingInterpretable interpreter = (UnknownTrackingInterpretable) impl; - return interpreter.evalTrackingUnknowns( - RuntimeUnknownResolver.builder() - .setResolver(context.variableResolver()) - .setAttributeResolver(context.createAttributeResolver()) - .build(), - listener); - } else { - return impl.eval(context.variableResolver(), listener); - } - } catch (InterpreterException e) { - throw unwrapOrCreateEvaluationException(e); - } - } - - /** Get the underlying {@link Interpretable} for the {@code Program}. */ - abstract Interpretable getInterpretable(); - - /** Get the {@code CelOptions} configured for this program. */ - abstract CelOptions getOptions(); - - /** Instantiate a new {@code Program} from the input {@code interpretable}. */ - static Program from(Interpretable interpretable, CelOptions options) { - return new AutoValue_CelRuntime_Program(interpretable, options); - } - - @CheckReturnValue - private static CelEvaluationException unwrapOrCreateEvaluationException( - InterpreterException e) { - if (e.getCause() instanceof CelEvaluationException) { - return (CelEvaluationException) e.getCause(); - } - return new CelEvaluationException(e, e.getErrorCode()); - } - } - - /** - * Binding consisting of an overload id, a Java-native argument signature, and an overload - * definition. - * - *

While the CEL function has a human-readable {@code camelCase} name, overload ids should use - * the following convention where all {@code } names should be ASCII lower-cased. For types - * prefer the unparameterized simple name of time, or unqualified package name of any proto-based - * type: - * - *

    - *
  • unary member function: _ - *
  • binary member function: __ - *
  • unary global function: _ - *
  • binary global function: __ - *
  • global function: ___ - *
- * - *

Examples: string_startsWith_string, mathMax_list, lessThan_money_money - */ - @AutoValue - @Immutable - abstract class CelFunctionBinding { - - public abstract String getOverloadId(); - - abstract ImmutableList> getArgTypes(); - - abstract CelFunctionOverload getDefinition(); + Object trace( + CelVariableResolver resolver, + CelFunctionResolver lateBoundFunctionResolver, + CelEvaluationListener listener) + throws CelEvaluationException; /** - * Create a unary function binding from the {@code overloadId}, {@code arg}, and {@code impl}. + * Trace evaluates a compiled program using a {@code mapValue} as the source of input variables + * and late-bound functions {@code lateBoundFunctionResolver}. The listener is invoked as + * evaluation progresses through the AST. */ - @SuppressWarnings("unchecked") - public static CelFunctionBinding from( - String overloadId, Class arg, CelFunctionOverload.Unary impl) { - return new AutoValue_CelRuntime_CelFunctionBinding( - overloadId, ImmutableList.of(arg), (args) -> impl.apply((T) args[0])); - } + Object trace( + Map mapValue, + CelFunctionResolver lateBoundFunctionResolver, + CelEvaluationListener listener) + throws CelEvaluationException; /** - * Create a binary function binding from the {@code overloadId}, {@code arg1}, {@code arg2}, and - * {@code impl}. + * 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. */ - @SuppressWarnings("unchecked") - public static CelFunctionBinding from( - String overloadId, - Class arg1, - Class arg2, - CelFunctionOverload.Binary impl) { - return new AutoValue_CelRuntime_CelFunctionBinding( - overloadId, - ImmutableList.of(arg1, arg2), - (args) -> impl.apply((T1) args[0], (T2) args[1])); - } + Object trace(PartialVars partialVars, CelEvaluationListener listener) + throws CelEvaluationException; /** - * Create a function binding from the {@code overloadId}, {@code argTypes}, and {@code impl}. + * Advance evaluation based on the current unknown context. + * + *

This represents one round of incremental evaluation and may return a final result or a + * CelUnknownSet. + * + *

If no unknowns are declared in the context or {@link CelOptions#enableUnknownTracking() + * UnknownTracking} is disabled, this is equivalent to eval. */ - public static CelFunctionBinding from( - String overloadId, Iterable> argTypes, CelFunctionOverload impl) { - return new AutoValue_CelRuntime_CelFunctionBinding( - overloadId, ImmutableList.copyOf(argTypes), impl); - } + Object advanceEvaluation(UnknownContext context) throws CelEvaluationException; } } diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeBuilder.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeBuilder.java index 47187e4d3..87f11fde2 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeBuilder.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeBuilder.java @@ -21,7 +21,9 @@ import com.google.protobuf.Descriptors.FileDescriptor; import com.google.protobuf.ExtensionRegistry; import com.google.protobuf.Message; +import dev.cel.common.CelContainer; import dev.cel.common.CelOptions; +import dev.cel.common.types.CelTypeProvider; import dev.cel.common.values.CelValueProvider; import java.util.function.Function; @@ -33,20 +35,28 @@ public interface CelRuntimeBuilder { CelRuntimeBuilder setOptions(CelOptions options); /** - * Add one or more {@link CelRuntime.CelFunctionBinding} objects to the CEL runtime. + * Add one or more {@link CelFunctionBinding} objects to the CEL runtime. * *

Functions with duplicate overload ids will be replaced in favor of the new overload. */ @CanIgnoreReturnValue - CelRuntimeBuilder addFunctionBindings(CelRuntime.CelFunctionBinding... bindings); + CelRuntimeBuilder addFunctionBindings(CelFunctionBinding... bindings); /** - * Bind a collection of {@link CelRuntime.CelFunctionBinding} objects to the runtime. + * Bind a collection of {@link CelFunctionBinding} objects to the runtime. * *

Functions with duplicate overload ids will be replaced in favor of the new overload. */ @CanIgnoreReturnValue - CelRuntimeBuilder addFunctionBindings(Iterable bindings); + CelRuntimeBuilder addFunctionBindings(Iterable bindings); + + /** Adds bindings for functions that are allowed to be late-bound (resolved at execution time). */ + @CanIgnoreReturnValue + CelRuntimeBuilder addLateBoundFunctions(String... lateBoundFunctionNames); + + /** Adds bindings for functions that are allowed to be late-bound (resolved at execution time). */ + @CanIgnoreReturnValue + CelRuntimeBuilder addLateBoundFunctions(Iterable lateBoundFunctionNames); /** * Add message {@link Descriptor}s to the builder for type-checking and object creation at @@ -123,6 +133,13 @@ public interface CelRuntimeBuilder { @CanIgnoreReturnValue CelRuntimeBuilder addFileTypes(FileDescriptorSet fileDescriptorSet); + /** + * 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 + CelRuntimeBuilder setTypeProvider(CelTypeProvider celTypeProvider); + /** * Set a custom type factory for the runtime. * @@ -140,11 +157,12 @@ public interface CelRuntimeBuilder { CelRuntimeBuilder setTypeFactory(Function typeFactory); /** - * Sets the {@code celValueProvider} for resolving values during evaluation. The provided value - * provider will be used first before falling back to the built-in {@link - * dev.cel.common.values.ProtoMessageValueProvider} for resolving protobuf messages. + * Sets the {@link CelValueProvider} for resolving struct values during evaluation. Multiple + * providers can be combined using {@code CombinedCelValueProvider}. Note that if you intend to + * support proto messages in addition to custom struct values, protobuf value provider must be + * configured first before the custom value provider. * - *

Note {@link CelOptions#enableCelValue()} must be enabled or this method will be a no-op. + *

Note that this option is only supported for planner-based runtime. */ @CanIgnoreReturnValue CelRuntimeBuilder setValueProvider(CelValueProvider celValueProvider); @@ -153,6 +171,16 @@ public interface CelRuntimeBuilder { @CanIgnoreReturnValue CelRuntimeBuilder setStandardEnvironmentEnabled(boolean value); + /** + * Override the standard functions for the runtime. This can be used to subset the standard + * environment to only expose the desired function overloads to the runtime. + * + *

{@link #setStandardEnvironmentEnabled(boolean)} must be set to false for this to take + * effect. + */ + @CanIgnoreReturnValue + CelRuntimeBuilder setStandardFunctions(CelStandardFunctions standardFunctions); + /** Adds one or more libraries for runtime. */ @CanIgnoreReturnValue CelRuntimeBuilder addLibraries(CelRuntimeLibrary... libraries); @@ -168,6 +196,15 @@ public interface CelRuntimeBuilder { @CanIgnoreReturnValue CelRuntimeBuilder setExtensionRegistry(ExtensionRegistry extensionRegistry); + + /** + * Set the {@link CelContainer} to use as the namespace for resolving CEL expression variables and + * functions. + */ + @CanIgnoreReturnValue + CelRuntimeBuilder setContainer(CelContainer container); + + /** Build a new instance of the {@code CelRuntime}. */ @CheckReturnValue CelRuntime build(); diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeFactory.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeFactory.java index e19f9d765..6615b59e0 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeFactory.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeFactory.java @@ -28,8 +28,28 @@ public final class CelRuntimeFactory { public static CelRuntimeBuilder standardCelRuntimeBuilder() { 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 new file mode 100644 index 000000000..b02f64b61 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java @@ -0,0 +1,553 @@ +// 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.checkNotNull; + +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 com.google.errorprone.annotations.Immutable; +import com.google.protobuf.DescriptorProtos; +import com.google.protobuf.Descriptors; +import com.google.protobuf.ExtensionRegistry; +import com.google.protobuf.Message; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelContainer; +import dev.cel.common.CelDescriptorUtil; +import dev.cel.common.CelDescriptors; +import dev.cel.common.CelOptions; +import dev.cel.common.annotations.Internal; +import dev.cel.common.internal.CelDescriptorPool; +import dev.cel.common.internal.CombinedDescriptorPool; +import dev.cel.common.internal.DefaultDescriptorPool; +import dev.cel.common.internal.DefaultMessageFactory; +import dev.cel.common.internal.DynamicProto; +// CEL-Internal-1 +import dev.cel.common.types.CelTypeProvider; +import dev.cel.common.types.DefaultTypeProvider; +import dev.cel.common.types.ProtoMessageTypeProvider; +import dev.cel.common.values.CelValueConverter; +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 +public abstract class CelRuntimeImpl implements CelRuntime { + + abstract ProgramPlanner planner(); + + abstract CelOptions options(); + + abstract CelContainer container(); + + abstract ImmutableMap functionBindings(); + + abstract ImmutableSet fileDescriptors(); + + // Callers must guarantee that a custom runtime library is immutable. CEL provided ones are + // immutable by default. + @SuppressWarnings("Immutable") + @AutoValue.CopyAnnotations + abstract ImmutableSet runtimeLibraries(); + + abstract ImmutableSet lateBoundFunctionNames(); + + abstract CelStandardFunctions standardFunctions(); + + abstract @Nullable CelTypeProvider typeProvider(); + + abstract @Nullable CelValueProvider valueProvider(); + + // Extension registry is unmodifiable. Just not marked as such from Protobuf's implementation. + @SuppressWarnings("Immutable") + @AutoValue.CopyAnnotations + abstract @Nullable ExtensionRegistry extensionRegistry(); + + @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() { + + @Override + public Object eval() throws CelEvaluationException { + return program.eval(); + } + + @Override + public Object eval(Map mapValue) throws CelEvaluationException { + return program.eval(mapValue); + } + + @Override + public Object eval(Map mapValue, CelFunctionResolver lateBoundFunctionResolver) + throws CelEvaluationException { + return program.eval(mapValue, lateBoundFunctionResolver); + } + + @Override + public Object eval(Message message) throws CelEvaluationException { + PlannedProgram plannedProgram = (PlannedProgram) program; + return plannedProgram.evalOrThrow( + plannedProgram.interpretable(), + ProtoMessageActivationFactory.fromProto(message, plannedProgram.options()), + EMPTY_FUNCTION_RESOLVER, + /* partialVars= */ null, + /* listener= */ null); + } + + @Override + public Object eval(CelVariableResolver resolver) throws CelEvaluationException { + return program.eval(resolver); + } + + @Override + public Object eval( + CelVariableResolver resolver, CelFunctionResolver lateBoundFunctionResolver) + throws CelEvaluationException { + return program.eval(resolver, lateBoundFunctionResolver); + } + + @Override + public Object eval(PartialVars partialVars) throws CelEvaluationException { + return program.eval(partialVars); + } + + @Override + public Object trace(CelEvaluationListener listener) throws CelEvaluationException { + return ((PlannedProgram) program) + .trace(GlobalResolver.EMPTY, EMPTY_FUNCTION_RESOLVER, null, listener); + } + + @Override + public Object trace(Map mapValue, CelEvaluationListener listener) + throws CelEvaluationException { + return ((PlannedProgram) program) + .trace(Activation.copyOf(mapValue), EMPTY_FUNCTION_RESOLVER, null, listener); + } + + @Override + public Object trace(Message message, CelEvaluationListener listener) + throws CelEvaluationException { + 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 { + return ((PlannedProgram) program) + .trace( + (name) -> resolver.find(name).orElse(null), + EMPTY_FUNCTION_RESOLVER, + null, + listener); + } + + @Override + public Object trace( + CelVariableResolver resolver, + CelFunctionResolver lateBoundFunctionResolver, + CelEvaluationListener listener) + throws CelEvaluationException { + return ((PlannedProgram) program) + .trace( + (name) -> resolver.find(name).orElse(null), + lateBoundFunctionResolver, + null, + listener); + } + + @Override + public Object trace( + Map mapValue, + CelFunctionResolver lateBoundFunctionResolver, + CelEvaluationListener listener) + throws CelEvaluationException { + 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 + public Object advanceEvaluation(UnknownContext context) throws CelEvaluationException { + throw new UnsupportedOperationException("Unsupported operation."); + } + }; + } + + @Override + public abstract Builder toRuntimeBuilder(); + + /** + * 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()); + } + + /** Builder for {@link CelRuntimeImpl}. */ + @AutoValue.Builder + public abstract static class Builder implements CelRuntimeBuilder { + + public abstract Builder setPlanner(ProgramPlanner planner); + + @Override + public abstract Builder setOptions(CelOptions options); + + @Override + public abstract Builder setStandardFunctions(CelStandardFunctions standardFunctions); + + @Override + public abstract Builder setExtensionRegistry(ExtensionRegistry extensionRegistry); + + @Override + public abstract Builder setTypeProvider(CelTypeProvider celTypeProvider); + + @Override + public abstract Builder setValueProvider(CelValueProvider celValueProvider); + + @Override + public abstract Builder setContainer(CelContainer container); + + abstract CelOptions options(); + + abstract CelContainer container(); + + abstract CelTypeProvider typeProvider(); + + abstract CelValueProvider valueProvider(); + + abstract CelStandardFunctions standardFunctions(); + + abstract ExtensionRegistry extensionRegistry(); + + abstract ImmutableMap functionBindings(); + + abstract ImmutableSet.Builder fileDescriptorsBuilder(); + + abstract ImmutableSet.Builder runtimeLibrariesBuilder(); + + abstract ImmutableSet.Builder lateBoundFunctionNamesBuilder(); + + private final Map mutableFunctionBindings = new HashMap<>(); + + @Override + @CanIgnoreReturnValue + public Builder addFunctionBindings(CelFunctionBinding... bindings) { + checkNotNull(bindings); + return addFunctionBindings(Arrays.asList(bindings)); + } + + @Override + @CanIgnoreReturnValue + public Builder addFunctionBindings(Iterable bindings) { + checkNotNull(bindings); + bindings.forEach(o -> mutableFunctionBindings.putIfAbsent(o.getOverloadId(), o)); + return this; + } + + @Override + @CanIgnoreReturnValue + public Builder addLateBoundFunctions(String... lateBoundFunctionNames) { + checkNotNull(lateBoundFunctionNames); + return addLateBoundFunctions(Arrays.asList(lateBoundFunctionNames)); + } + + @Override + @CanIgnoreReturnValue + public Builder addLateBoundFunctions(Iterable lateBoundFunctionNames) { + checkNotNull(lateBoundFunctionNames); + this.lateBoundFunctionNamesBuilder().addAll(lateBoundFunctionNames); + return this; + } + + @Override + @CanIgnoreReturnValue + public Builder addMessageTypes(Descriptors.Descriptor... descriptors) { + checkNotNull(descriptors); + return addMessageTypes(Arrays.asList(descriptors)); + } + + @Override + @CanIgnoreReturnValue + public Builder addMessageTypes(Iterable descriptors) { + checkNotNull(descriptors); + return addFileTypes(CelDescriptorUtil.getFileDescriptorsForDescriptors(descriptors)); + } + + @Override + @CanIgnoreReturnValue + public Builder addFileTypes(DescriptorProtos.FileDescriptorSet fileDescriptorSet) { + checkNotNull(fileDescriptorSet); + return addFileTypes( + CelDescriptorUtil.getFileDescriptorsFromFileDescriptorSet(fileDescriptorSet)); + } + + @Override + @CanIgnoreReturnValue + public Builder addFileTypes(Descriptors.FileDescriptor... fileDescriptors) { + checkNotNull(fileDescriptors); + return addFileTypes(Arrays.asList(fileDescriptors)); + } + + @Override + @CanIgnoreReturnValue + public Builder addFileTypes(Iterable fileDescriptors) { + checkNotNull(fileDescriptors); + this.fileDescriptorsBuilder().addAll(fileDescriptors); + return this; + } + + @Override + @CanIgnoreReturnValue + public Builder addLibraries(CelRuntimeLibrary... libraries) { + checkNotNull(libraries); + return this.addLibraries(Arrays.asList(libraries)); + } + + @Override + @CanIgnoreReturnValue + public Builder addLibraries(Iterable libraries) { + checkNotNull(libraries); + this.runtimeLibrariesBuilder().addAll(libraries); + return this; + } + + abstract Builder setFunctionBindings(ImmutableMap value); + + @Override + public Builder setTypeFactory(Function typeFactory) { + throw new UnsupportedOperationException("Unsupported. Use a custom value provider instead."); + } + + @Override + public Builder setStandardEnvironmentEnabled(boolean value) { + throw new UnsupportedOperationException( + "Unsupported. Subset the environment using setStandardFunctions instead."); + } + + /** Throws if an unsupported flag in CelOptions is toggled. */ + private static void assertAllowedCelOptions(CelOptions celOptions) { + String prefix = "Misconfigured CelOptions: "; + if (!celOptions.enableUnsignedLongs()) { + throw new IllegalArgumentException(prefix + "enableUnsignedLongs cannot be disabled."); + } + if (!celOptions.unwrapWellKnownTypesOnFunctionDispatch()) { + throw new IllegalArgumentException( + prefix + "unwrapWellKnownTypesOnFunctionDispatch cannot be disabled."); + } + + // Disallowed options in favor of subsetting + String subsettingError = "Subset the environment instead using setStandardFunctions method."; + + if (!celOptions.enableTimestampEpoch()) { + throw new IllegalArgumentException( + prefix + "enableTimestampEpoch cannot be disabled. " + subsettingError); + } + + if (!celOptions.enableHeterogeneousNumericComparisons()) { + throw new IllegalArgumentException( + prefix + + "enableHeterogeneousNumericComparisons cannot be disabled. " + + subsettingError); + } + } + + abstract CelRuntimeImpl autoBuild(); + + private static DefaultDispatcher newDispatcher( + CelStandardFunctions standardFunctions, + Collection customFunctionBindings, + RuntimeEquality runtimeEquality, + CelOptions options) { + 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(), + binding.getDefinition()); + } + + 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(), + binding.getDefinition()); + } + + return builder.build(); + } + + private static CelDescriptorPool newDescriptorPool( + CelDescriptors celDescriptors, + ExtensionRegistry extensionRegistry) { + ImmutableList.Builder descriptorPools = new ImmutableList.Builder<>(); + + descriptorPools.add(DefaultDescriptorPool.create(celDescriptors, extensionRegistry)); + + return CombinedDescriptorPool.create(descriptorPools.build()); + } + + @Override + public CelRuntime build() { + CelOptions options = options(); + assertAllowedCelOptions(options); + CelDescriptors celDescriptors = + CelDescriptorUtil.getAllDescriptorsFromFileDescriptor(fileDescriptorsBuilder().build()); + + CelDescriptorPool descriptorPool = + newDescriptorPool( + celDescriptors, + extensionRegistry()); + DefaultMessageFactory defaultMessageFactory = DefaultMessageFactory.create(descriptorPool); + DynamicProto dynamicProto = DynamicProto.create(defaultMessageFactory); + CelValueProvider protoMessageValueProvider = + ProtoMessageValueProvider.newInstance(options(), dynamicProto); + RuntimeEquality runtimeEquality = ProtoMessageRuntimeEquality.create(dynamicProto, options()); + ImmutableSet runtimeLibraries = runtimeLibrariesBuilder().build(); + // Add libraries, such as extensions + for (CelRuntimeLibrary celLibrary : runtimeLibraries) { + if (celLibrary instanceof CelInternalRuntimeLibrary) { + ((CelInternalRuntimeLibrary) celLibrary) + .setRuntimeOptions(this, runtimeEquality, options()); + } else { + celLibrary.setRuntimeOptions(this); + } + } + + if (valueProvider() != null) { + protoMessageValueProvider = + CombinedCelValueProvider.combine(protoMessageValueProvider, valueProvider()); + } + CelValueConverter celValueConverter = protoMessageValueProvider.celValueConverter(); + + CelTypeProvider messageTypeProvider = + ProtoMessageTypeProvider.newBuilder() + .setCelDescriptors(celDescriptors) + .setAllowJsonFieldNames(options().enableJsonFieldNames()) + .setResolveTypeDependencies(options().resolveTypeDependencies()) + .build(); + + CelTypeProvider combinedTypeProvider = + new CelTypeProvider.CombinedCelTypeProvider( + DefaultTypeProvider.getInstance(), messageTypeProvider); + if (typeProvider() != null) { + combinedTypeProvider = + new CelTypeProvider.CombinedCelTypeProvider(combinedTypeProvider, typeProvider()); + } + + DescriptorTypeResolver descriptorTypeResolver = + DescriptorTypeResolver.create(combinedTypeProvider, celValueConverter); + TypeFunction typeFunction = TypeFunction.create(descriptorTypeResolver); + + mutableFunctionBindings.putAll(functionBindings()); + + for (CelFunctionBinding binding : + typeFunction.newFunctionBindings(options(), runtimeEquality)) { + mutableFunctionBindings.put(binding.getOverloadId(), binding); + } + + DefaultDispatcher dispatcher = + newDispatcher( + standardFunctions(), mutableFunctionBindings.values(), runtimeEquality, options()); + + ProgramPlanner planner = + ProgramPlanner.newPlanner( + combinedTypeProvider, + protoMessageValueProvider, + dispatcher, + celValueConverter, + container(), + options(), + lateBoundFunctionNamesBuilder().build()); + setPlanner(planner); + + setFunctionBindings(ImmutableMap.copyOf(mutableFunctionBindings)); + return autoBuild(); + } + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java index 19059506a..b9ce022cf 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java @@ -19,7 +19,6 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.errorprone.annotations.CanIgnoreReturnValue; import javax.annotation.concurrent.ThreadSafe; @@ -29,6 +28,7 @@ import com.google.protobuf.ExtensionRegistry; import com.google.protobuf.Message; import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelContainer; import dev.cel.common.CelDescriptorUtil; import dev.cel.common.CelDescriptors; import dev.cel.common.CelOptions; @@ -38,17 +38,20 @@ import dev.cel.common.internal.DefaultDescriptorPool; import dev.cel.common.internal.DefaultMessageFactory; import dev.cel.common.internal.DynamicProto; -// CEL-Internal-3 +// CEL-Internal-1 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; import java.util.HashMap; -import java.util.Map; import java.util.Optional; import java.util.function.Function; -import org.jspecify.nullness.Nullable; +import org.jspecify.annotations.Nullable; /** * {@code CelRuntime} implementation based on the legacy CEL-Java stack. @@ -63,19 +66,58 @@ public final class CelRuntimeLegacyImpl implements CelRuntime { private final Interpreter interpreter; private final CelOptions options; - // Builder is mutable by design. APIs must guarantee a new instance to be returned. + private final boolean standardEnvironmentEnabled; + + // Extension registry is thread-safe. Just not marked as such from Protobuf's implementation. + // CEL-Internal-4 + private final ExtensionRegistry extensionRegistry; + + // A user-provided custom type factory should presumably be thread-safe. This is documented, but + // not enforced. // CEL-Internal-4 - private final Builder runtimeBuilder; + 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. + // CEL-Internal-4 + private final ImmutableSet celRuntimeLibraries; + + private final ImmutableList celFunctionBindings; @Override public CelRuntime.Program createProgram(CelAbstractSyntaxTree ast) { checkState(ast.isChecked(), "programs must be created from checked expressions"); - return CelRuntime.Program.from(interpreter.createInterpretable(ast), options); + return ProgramImpl.from(interpreter.createInterpretable(ast), options); } @Override public CelRuntimeBuilder toRuntimeBuilder() { - return new Builder(runtimeBuilder); + CelRuntimeBuilder builder = + new Builder() + .setOptions(options) + // CEL-Internal-2 + .setStandardEnvironmentEnabled(standardEnvironmentEnabled) + .setExtensionRegistry(extensionRegistry) + .addFileTypes(fileDescriptors) + .addLibraries(celRuntimeLibraries) + .addFunctionBindings(celFunctionBindings); + + if (customTypeFactory != null) { + builder.setTypeFactory(customTypeFactory); + } + + if (overriddenStandardFunctions != null) { + builder.setStandardFunctions(overriddenStandardFunctions); + } + + if (celValueProvider != null) { + builder.setValueProvider(celValueProvider); + } + + return builder; } /** Create a new builder for constructing a {@code CelRuntime} instance. */ @@ -86,17 +128,22 @@ public static CelRuntimeBuilder newBuilder() { /** Builder class for {@code CelRuntimeLegacyImpl}. */ public static final class Builder implements CelRuntimeBuilder { - private final ImmutableSet.Builder fileTypes; - private final HashMap functionBindings; - private final ImmutableSet.Builder celRuntimeLibraries; + // The following properties are for testing purposes only. Do not expose to public. + @VisibleForTesting final ImmutableSet.Builder fileTypes; + + @VisibleForTesting final HashMap customFunctionBindings; + + @VisibleForTesting final ImmutableSet.Builder celRuntimeLibraries; + + @VisibleForTesting Function customTypeFactory; + @VisibleForTesting CelValueProvider celValueProvider; + @VisibleForTesting CelStandardFunctions overriddenStandardFunctions; - @SuppressWarnings("unused") private CelOptions options; - private boolean standardEnvironmentEnabled; - private Function customTypeFactory; private ExtensionRegistry extensionRegistry; - private CelValueProvider celValueProvider; + + private boolean standardEnvironmentEnabled; @Override public CelRuntimeBuilder setOptions(CelOptions options) { @@ -111,10 +158,22 @@ public CelRuntimeBuilder addFunctionBindings(CelFunctionBinding... bindings) { @Override public CelRuntimeBuilder addFunctionBindings(Iterable bindings) { - bindings.forEach(o -> functionBindings.putIfAbsent(o.getOverloadId(), o)); + bindings.forEach(o -> customFunctionBindings.putIfAbsent(o.getOverloadId(), o)); return this; } + @Override + public CelRuntimeBuilder addLateBoundFunctions(String... lateBoundFunctionNames) { + throw new UnsupportedOperationException( + "This method is not supported for the legacy runtime"); + } + + @Override + public CelRuntimeBuilder addLateBoundFunctions(Iterable lateBoundFunctionNames) { + throw new UnsupportedOperationException( + "This method is not supported for the legacy runtime"); + } + @Override public CelRuntimeBuilder addMessageTypes(Descriptor... descriptors) { return addMessageTypes(Arrays.asList(descriptors)); @@ -143,9 +202,9 @@ public CelRuntimeBuilder addFileTypes(FileDescriptorSet fileDescriptorSet) { } @Override - public CelRuntimeBuilder setTypeFactory(Function typeFactory) { - this.customTypeFactory = typeFactory; - return this; + public CelRuntimeBuilder setTypeProvider(CelTypeProvider celTypeProvider) { + throw new UnsupportedOperationException( + "setTypeProvider is not supported for legacy runtime"); } @Override @@ -154,12 +213,24 @@ public CelRuntimeBuilder setValueProvider(CelValueProvider celValueProvider) { return this; } + @Override + public CelRuntimeBuilder setTypeFactory(Function typeFactory) { + this.customTypeFactory = typeFactory; + return this; + } + @Override public CelRuntimeBuilder setStandardEnvironmentEnabled(boolean value) { standardEnvironmentEnabled = value; return this; } + @Override + public CelRuntimeBuilder setStandardFunctions(CelStandardFunctions standardFunctions) { + this.overriddenStandardFunctions = standardFunctions; + return this; + } + @Override public CelRuntimeBuilder addLibraries(CelRuntimeLibrary... libraries) { checkNotNull(libraries); @@ -180,32 +251,25 @@ public CelRuntimeBuilder setExtensionRegistry(ExtensionRegistry extensionRegistr return this; } - // The following getters exist for asserting immutability for collections held by this builder, - // and shouldn't be exposed to the public. - @VisibleForTesting - Map getFunctionBindings() { - return this.functionBindings; - } - - @VisibleForTesting - ImmutableSet.Builder getRuntimeLibraries() { - return this.celRuntimeLibraries; - } - - @VisibleForTesting - ImmutableSet.Builder getFileTypes() { - return this.fileTypes; + @Override + public CelRuntimeBuilder setContainer(CelContainer container) { + throw new UnsupportedOperationException( + "This method is not supported for the legacy runtime"); } /** Build a new {@code CelRuntimeLegacyImpl} instance from the builder config. */ @Override public CelRuntimeLegacyImpl build() { - // Add libraries, such as extensions - celRuntimeLibraries.build().forEach(celLibrary -> celLibrary.setRuntimeOptions(this)); + if (standardEnvironmentEnabled && overriddenStandardFunctions != null) { + throw new IllegalArgumentException( + "setStandardEnvironmentEnabled must be set to false to override standard function" + + " bindings."); + } + ImmutableSet fileDescriptors = fileTypes.build(); CelDescriptors celDescriptors = CelDescriptorUtil.getAllDescriptorsFromFileDescriptor( - fileTypes.build(), options.resolveTypeDependencies()); + fileDescriptors, options.resolveTypeDependencies()); CelDescriptorPool celDescriptorPool = newDescriptorPool( @@ -226,49 +290,129 @@ public CelRuntimeLegacyImpl build() { runtimeTypeFactory, DefaultMessageFactory.create(celDescriptorPool)); DynamicProto dynamicProto = DynamicProto.create(runtimeTypeFactory); + RuntimeEquality runtimeEquality = ProtoMessageRuntimeEquality.create(dynamicProto, options); - DefaultDispatcher dispatcher = DefaultDispatcher.create(options, dynamicProto); - if (standardEnvironmentEnabled) { - StandardFunctions.add(dispatcher, dynamicProto, options); + ImmutableSet runtimeLibraries = celRuntimeLibraries.build(); + // Add libraries, such as extensions + for (CelRuntimeLibrary celLibrary : runtimeLibraries) { + if (celLibrary instanceof CelInternalRuntimeLibrary) { + ((CelInternalRuntimeLibrary) celLibrary) + .setRuntimeOptions(this, runtimeEquality, options); + } else { + celLibrary.setRuntimeOptions(this); + } + } + + DefaultDispatcher.Builder dispatcherBuilder = DefaultDispatcher.newBuilder(); + for (CelFunctionBinding standardFunctionBinding : + newStandardFunctionBindings(runtimeEquality)) { + String functionName = standardFunctionBinding.getOverloadId(); + if (standardFunctionBinding instanceof InternalCelFunctionBinding) { + functionName = ((InternalCelFunctionBinding) standardFunctionBinding).getFunctionName(); + } + dispatcherBuilder.addOverload( + functionName, + standardFunctionBinding.getOverloadId(), + standardFunctionBinding.getArgTypes(), + standardFunctionBinding.isStrict(), + standardFunctionBinding.getDefinition()); } - ImmutableMap functionBindingMap = - ImmutableMap.copyOf(functionBindings); - functionBindingMap.forEach( - (String overloadId, CelFunctionBinding func) -> - dispatcher.add( - overloadId, - func.getArgTypes(), - (args) -> { - try { - return func.getDefinition().apply(args); - } catch (CelEvaluationException e) { - throw new InterpreterException.Builder(e.getMessage()) - .setCause(e) - .setErrorCode(e.getErrorCode()) - .build(); - } - })); + for (CelFunctionBinding customBinding : customFunctionBindings.values()) { + String functionName = customBinding.getOverloadId(); + if (customBinding instanceof InternalCelFunctionBinding) { + functionName = ((InternalCelFunctionBinding) customBinding).getFunctionName(); + } + dispatcherBuilder.addOverload( + functionName, + customBinding.getOverloadId(), + customBinding.getArgTypes(), + customBinding.isStrict(), + customBinding.getDefinition()); + } + CelValueConverter celValueConverter = CelValueConverter.getDefaultInstance(); RuntimeTypeProvider runtimeTypeProvider; if (options.enableCelValue()) { - CelValueProvider messageValueProvider = - ProtoMessageValueProvider.newInstance(dynamicProto, options); - if (celValueProvider != null) { - messageValueProvider = - new CelValueProvider.CombinedCelValueProvider(celValueProvider, messageValueProvider); + CelValueProvider messageValueProvider = celValueProvider; + + if (messageValueProvider == null) { + messageValueProvider = ProtoMessageValueProvider.newInstance(options, dynamicProto); } - runtimeTypeProvider = - new RuntimeTypeProviderLegacyImpl( - options, messageValueProvider, celDescriptorPool, dynamicProto); + runtimeTypeProvider = CelValueRuntimeTypeProvider.newInstance(messageValueProvider); + celValueConverter = messageValueProvider.celValueConverter(); } else { runtimeTypeProvider = new DescriptorMessageProvider(runtimeTypeFactory, options); + if (celValueProvider != null) { + celValueConverter = celValueProvider.celValueConverter(); + } } + DefaultInterpreter interpreter = + new DefaultInterpreter( + DescriptorTypeResolver.create(celValueConverter), + runtimeTypeProvider, + dispatcherBuilder.build(), + options); + return new CelRuntimeLegacyImpl( - new DefaultInterpreter(runtimeTypeProvider, dispatcher, options), options, this); + interpreter, + options, + standardEnvironmentEnabled, + extensionRegistry, + customTypeFactory, + overriddenStandardFunctions, + celValueProvider, + fileDescriptors, + runtimeLibraries, + ImmutableList.copyOf(customFunctionBindings.values())); + } + + private ImmutableSet newStandardFunctionBindings( + RuntimeEquality runtimeEquality) { + CelStandardFunctions celStandardFunctions; + if (standardEnvironmentEnabled) { + celStandardFunctions = + CelStandardFunctions.newBuilder() + .filterFunctions( + (standardFunction, standardOverload) -> { + switch (standardFunction) { + case INT: + if (standardOverload.equals(IntOverload.INT64_TO_INT64)) { + // Note that we require UnsignedLong flag here to avoid ambiguous + // overloads against "uint64_to_int64", because they both use the same + // Java Long class. We skip adding this identity function if the flag is + // disabled. + return options.enableUnsignedLongs(); + } + break; + case TIMESTAMP: + // TODO: Remove this flag guard once the feature has been + // auto-enabled. + if (standardOverload.equals(TimestampOverload.INT64_TO_TIMESTAMP)) { + return options.enableTimestampEpoch(); + } + break; + default: + if (!options.enableHeterogeneousNumericComparisons()) { + return !CelStandardFunctions.isHeterogeneousComparison( + standardOverload); + } + break; + } + + return true; + }) + .build(); + } else if (overriddenStandardFunctions != null) { + celStandardFunctions = overriddenStandardFunctions; + } else { + return ImmutableSet.of(); + } + + return celStandardFunctions.newFunctionBindings(runtimeEquality, options); } private static CelDescriptorPool newDescriptorPool( @@ -294,35 +438,33 @@ private static ProtoMessageFactory maybeCombineMessageFactory( private Builder() { this.options = CelOptions.newBuilder().build(); this.fileTypes = ImmutableSet.builder(); - this.functionBindings = new HashMap<>(); + this.customFunctionBindings = new HashMap<>(); this.celRuntimeLibraries = ImmutableSet.builder(); this.extensionRegistry = ExtensionRegistry.getEmptyRegistry(); this.customTypeFactory = null; } - - private Builder(Builder builder) { - // The following properties are either immutable or simple primitives, thus can be assigned - // directly. - this.options = builder.options; - this.extensionRegistry = builder.extensionRegistry; - this.customTypeFactory = builder.customTypeFactory; - // The following needs to be deep copied as they are collection builders - this.fileTypes = deepCopy(builder.fileTypes); - this.celRuntimeLibraries = deepCopy(builder.celRuntimeLibraries); - this.functionBindings = new HashMap<>(builder.functionBindings); - } - - private static ImmutableSet.Builder deepCopy(ImmutableSet.Builder builderToCopy) { - ImmutableSet.Builder newBuilder = ImmutableSet.builder(); - newBuilder.addAll(builderToCopy.build()); - return newBuilder; - } } private CelRuntimeLegacyImpl( - Interpreter interpreter, CelOptions options, Builder runtimeBuilder) { + Interpreter interpreter, + CelOptions options, + boolean standardEnvironmentEnabled, + ExtensionRegistry extensionRegistry, + @Nullable Function customTypeFactory, + @Nullable CelStandardFunctions overriddenStandardFunctions, + @Nullable CelValueProvider celValueProvider, + ImmutableSet fileDescriptors, + ImmutableSet celRuntimeLibraries, + ImmutableList celFunctionBindings) { this.interpreter = interpreter; this.options = options; - this.runtimeBuilder = new Builder(runtimeBuilder); + this.standardEnvironmentEnabled = standardEnvironmentEnabled; + this.extensionRegistry = extensionRegistry; + this.customTypeFactory = customTypeFactory; + this.overriddenStandardFunctions = overriddenStandardFunctions; + this.celValueProvider = celValueProvider; + this.fileDescriptors = fileDescriptors; + this.celRuntimeLibraries = celRuntimeLibraries; + this.celFunctionBindings = celFunctionBindings; } } diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeLibrary.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeLibrary.java index c07f7ce4b..803e40925 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeLibrary.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeLibrary.java @@ -15,10 +15,11 @@ package dev.cel.runtime; /** - * CelCompilerLibrary defines the interface to extend functionalities beyond the CEL standard + * CelRuntimeLibrary defines the interface to extend functionalities beyond the CEL standard * functions for {@link CelRuntime}. */ public interface CelRuntimeLibrary { + /** * Configures the runtime to support the library implementation, such as adding function bindings. */ diff --git a/runtime/src/main/java/dev/cel/runtime/CelStandardFunctions.java b/runtime/src/main/java/dev/cel/runtime/CelStandardFunctions.java new file mode 100644 index 000000000..39797e086 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/CelStandardFunctions.java @@ -0,0 +1,565 @@ +// 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. + +package dev.cel.runtime; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.ImmutableSet.toImmutableSet; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableMultimap; +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.Operator; +import dev.cel.common.annotations.Internal; +import dev.cel.runtime.standard.AddOperator; +import dev.cel.runtime.standard.AddOperator.AddOverload; +import dev.cel.runtime.standard.BoolFunction; +import dev.cel.runtime.standard.BoolFunction.BoolOverload; +import dev.cel.runtime.standard.BytesFunction; +import dev.cel.runtime.standard.BytesFunction.BytesOverload; +import dev.cel.runtime.standard.CelStandardFunction; +import dev.cel.runtime.standard.CelStandardOverload; +import dev.cel.runtime.standard.ContainsFunction; +import dev.cel.runtime.standard.ContainsFunction.ContainsOverload; +import dev.cel.runtime.standard.DivideOperator; +import dev.cel.runtime.standard.DivideOperator.DivideOverload; +import dev.cel.runtime.standard.DoubleFunction; +import dev.cel.runtime.standard.DoubleFunction.DoubleOverload; +import dev.cel.runtime.standard.DurationFunction; +import dev.cel.runtime.standard.DurationFunction.DurationOverload; +import dev.cel.runtime.standard.DynFunction; +import dev.cel.runtime.standard.DynFunction.DynOverload; +import dev.cel.runtime.standard.EndsWithFunction; +import dev.cel.runtime.standard.EndsWithFunction.EndsWithOverload; +import dev.cel.runtime.standard.EqualsOperator; +import dev.cel.runtime.standard.EqualsOperator.EqualsOverload; +import dev.cel.runtime.standard.GetDateFunction; +import dev.cel.runtime.standard.GetDateFunction.GetDateOverload; +import dev.cel.runtime.standard.GetDayOfMonthFunction; +import dev.cel.runtime.standard.GetDayOfMonthFunction.GetDayOfMonthOverload; +import dev.cel.runtime.standard.GetDayOfWeekFunction; +import dev.cel.runtime.standard.GetDayOfWeekFunction.GetDayOfWeekOverload; +import dev.cel.runtime.standard.GetDayOfYearFunction; +import dev.cel.runtime.standard.GetDayOfYearFunction.GetDayOfYearOverload; +import dev.cel.runtime.standard.GetFullYearFunction; +import dev.cel.runtime.standard.GetFullYearFunction.GetFullYearOverload; +import dev.cel.runtime.standard.GetHoursFunction; +import dev.cel.runtime.standard.GetHoursFunction.GetHoursOverload; +import dev.cel.runtime.standard.GetMillisecondsFunction; +import dev.cel.runtime.standard.GetMillisecondsFunction.GetMillisecondsOverload; +import dev.cel.runtime.standard.GetMinutesFunction; +import dev.cel.runtime.standard.GetMinutesFunction.GetMinutesOverload; +import dev.cel.runtime.standard.GetMonthFunction; +import dev.cel.runtime.standard.GetMonthFunction.GetMonthOverload; +import dev.cel.runtime.standard.GetSecondsFunction; +import dev.cel.runtime.standard.GetSecondsFunction.GetSecondsOverload; +import dev.cel.runtime.standard.GreaterEqualsOperator; +import dev.cel.runtime.standard.GreaterEqualsOperator.GreaterEqualsOverload; +import dev.cel.runtime.standard.GreaterOperator; +import dev.cel.runtime.standard.GreaterOperator.GreaterOverload; +import dev.cel.runtime.standard.InOperator; +import dev.cel.runtime.standard.InOperator.InOverload; +import dev.cel.runtime.standard.IndexOperator; +import dev.cel.runtime.standard.IndexOperator.IndexOverload; +import dev.cel.runtime.standard.IntFunction; +import dev.cel.runtime.standard.IntFunction.IntOverload; +import dev.cel.runtime.standard.LessEqualsOperator; +import dev.cel.runtime.standard.LessEqualsOperator.LessEqualsOverload; +import dev.cel.runtime.standard.LessOperator; +import dev.cel.runtime.standard.LessOperator.LessOverload; +import dev.cel.runtime.standard.LogicalNotOperator; +import dev.cel.runtime.standard.LogicalNotOperator.LogicalNotOverload; +import dev.cel.runtime.standard.MatchesFunction; +import dev.cel.runtime.standard.MatchesFunction.MatchesOverload; +import dev.cel.runtime.standard.ModuloOperator; +import dev.cel.runtime.standard.ModuloOperator.ModuloOverload; +import dev.cel.runtime.standard.MultiplyOperator; +import dev.cel.runtime.standard.MultiplyOperator.MultiplyOverload; +import dev.cel.runtime.standard.NegateOperator; +import dev.cel.runtime.standard.NegateOperator.NegateOverload; +import dev.cel.runtime.standard.NotEqualsOperator; +import dev.cel.runtime.standard.NotEqualsOperator.NotEqualsOverload; +import dev.cel.runtime.standard.NotStrictlyFalseFunction; +import dev.cel.runtime.standard.NotStrictlyFalseFunction.NotStrictlyFalseOverload; +import dev.cel.runtime.standard.SizeFunction; +import dev.cel.runtime.standard.SizeFunction.SizeOverload; +import dev.cel.runtime.standard.StartsWithFunction; +import dev.cel.runtime.standard.StartsWithFunction.StartsWithOverload; +import dev.cel.runtime.standard.StringFunction; +import dev.cel.runtime.standard.StringFunction.StringOverload; +import dev.cel.runtime.standard.SubtractOperator; +import dev.cel.runtime.standard.SubtractOperator.SubtractOverload; +import dev.cel.runtime.standard.TimestampFunction; +import dev.cel.runtime.standard.TimestampFunction.TimestampOverload; +import dev.cel.runtime.standard.UintFunction; +import dev.cel.runtime.standard.UintFunction.UintOverload; +import java.util.Collection; +import java.util.Map; + +/** Runtime function bindings for the standard functions in CEL. */ +@Immutable +public final class CelStandardFunctions { + private static final ImmutableSet HETEROGENEOUS_COMPARISON_OPERATORS = + ImmutableSet.of( + LessOverload.LESS_DOUBLE_UINT64, + LessOverload.LESS_INT64_UINT64, + LessOverload.LESS_UINT64_INT64, + LessOverload.LESS_INT64_DOUBLE, + LessOverload.LESS_DOUBLE_INT64, + LessOverload.LESS_UINT64_DOUBLE, + LessEqualsOverload.LESS_EQUALS_INT64_UINT64, + LessEqualsOverload.LESS_EQUALS_UINT64_INT64, + LessEqualsOverload.LESS_EQUALS_INT64_DOUBLE, + LessEqualsOverload.LESS_EQUALS_DOUBLE_INT64, + LessEqualsOverload.LESS_EQUALS_UINT64_DOUBLE, + LessEqualsOverload.LESS_EQUALS_DOUBLE_UINT64, + GreaterOverload.GREATER_INT64_UINT64, + GreaterOverload.GREATER_UINT64_INT64, + GreaterOverload.GREATER_INT64_DOUBLE, + GreaterOverload.GREATER_DOUBLE_INT64, + GreaterOverload.GREATER_UINT64_DOUBLE, + GreaterOverload.GREATER_DOUBLE_UINT64, + GreaterEqualsOverload.GREATER_EQUALS_INT64_UINT64, + GreaterEqualsOverload.GREATER_EQUALS_UINT64_INT64, + GreaterEqualsOverload.GREATER_EQUALS_INT64_DOUBLE, + GreaterEqualsOverload.GREATER_EQUALS_DOUBLE_INT64, + GreaterEqualsOverload.GREATER_EQUALS_UINT64_DOUBLE, + GreaterEqualsOverload.GREATER_EQUALS_DOUBLE_UINT64); + + private final ImmutableMultimap standardOverloads; + + public static final ImmutableSet ALL_STANDARD_FUNCTIONS = + ImmutableSet.of( + AddOperator.create(), + BoolFunction.create(), + BytesFunction.create(), + ContainsFunction.create(), + DivideOperator.create(), + DoubleFunction.create(), + DurationFunction.create(), + DynFunction.create(), + EndsWithFunction.create(), + EqualsOperator.create(), + GetDateFunction.create(), + GetDayOfMonthFunction.create(), + GetDayOfWeekFunction.create(), + GetDayOfYearFunction.create(), + GetFullYearFunction.create(), + GetHoursFunction.create(), + GetMillisecondsFunction.create(), + GetMinutesFunction.create(), + GetMonthFunction.create(), + GetSecondsFunction.create(), + GreaterEqualsOperator.create(), + GreaterOperator.create(), + IndexOperator.create(), + InOperator.create(), + IntFunction.create(), + LessEqualsOperator.create(), + LessOperator.create(), + LogicalNotOperator.create(), + MatchesFunction.create(), + ModuloOperator.create(), + MultiplyOperator.create(), + NegateOperator.create(), + NotEqualsOperator.create(), + SizeFunction.create(), + StartsWithFunction.create(), + StringFunction.create(), + SubtractOperator.create(), + TimestampFunction.create(), + UintFunction.create(), + NotStrictlyFalseFunction.create()); + + /** + * Enumeration of Standard Function bindings. + * + *

Note: The conditional, logical_or, logical_and, and type functions are currently + * special-cased, and does not appear in this enum. + */ + public enum StandardFunction { + LOGICAL_NOT(Operator.LOGICAL_NOT.getFunction(), LogicalNotOverload.LOGICAL_NOT), + IN(Operator.IN.getFunction(), InOverload.IN_LIST, InOverload.IN_MAP), + NOT_STRICTLY_FALSE( + Operator.NOT_STRICTLY_FALSE.getFunction(), NotStrictlyFalseOverload.NOT_STRICTLY_FALSE), + EQUALS(Operator.EQUALS.getFunction(), EqualsOverload.EQUALS), + NOT_EQUALS(Operator.NOT_EQUALS.getFunction(), NotEqualsOverload.NOT_EQUALS), + BOOL("bool", BoolOverload.BOOL_TO_BOOL, BoolOverload.STRING_TO_BOOL), + ADD( + Operator.ADD.getFunction(), + AddOverload.ADD_INT64, + AddOverload.ADD_UINT64, + AddOverload.ADD_DOUBLE, + AddOverload.ADD_STRING, + AddOverload.ADD_BYTES, + AddOverload.ADD_LIST, + AddOverload.ADD_TIMESTAMP_DURATION, + AddOverload.ADD_DURATION_TIMESTAMP, + AddOverload.ADD_DURATION_DURATION), + SUBTRACT( + Operator.SUBTRACT.getFunction(), + SubtractOverload.SUBTRACT_INT64, + SubtractOverload.SUBTRACT_TIMESTAMP_TIMESTAMP, + SubtractOverload.SUBTRACT_TIMESTAMP_DURATION, + SubtractOverload.SUBTRACT_UINT64, + SubtractOverload.SUBTRACT_DOUBLE, + SubtractOverload.SUBTRACT_DURATION_DURATION), + MULTIPLY( + Operator.MULTIPLY.getFunction(), + MultiplyOverload.MULTIPLY_INT64, + MultiplyOverload.MULTIPLY_DOUBLE, + MultiplyOverload.MULTIPLY_UINT64), + DIVIDE( + Operator.DIVIDE.getFunction(), + DivideOverload.DIVIDE_DOUBLE, + DivideOverload.DIVIDE_INT64, + DivideOverload.DIVIDE_UINT64), + MODULO( + Operator.MODULO.getFunction(), ModuloOverload.MODULO_INT64, ModuloOverload.MODULO_UINT64), + NEGATE( + Operator.NEGATE.getFunction(), NegateOverload.NEGATE_INT64, NegateOverload.NEGATE_DOUBLE), + INDEX(Operator.INDEX.getFunction(), IndexOverload.INDEX_LIST, IndexOverload.INDEX_MAP), + SIZE( + "size", + SizeOverload.SIZE_STRING, + SizeOverload.SIZE_BYTES, + SizeOverload.SIZE_LIST, + SizeOverload.SIZE_MAP, + SizeOverload.STRING_SIZE, + SizeOverload.BYTES_SIZE, + SizeOverload.LIST_SIZE, + SizeOverload.MAP_SIZE), + INT( + "int", + IntOverload.INT64_TO_INT64, + IntOverload.UINT64_TO_INT64, + IntOverload.DOUBLE_TO_INT64, + IntOverload.STRING_TO_INT64, + IntOverload.TIMESTAMP_TO_INT64), + UINT( + "uint", + UintOverload.UINT64_TO_UINT64, + UintOverload.INT64_TO_UINT64, + UintOverload.DOUBLE_TO_UINT64, + UintOverload.STRING_TO_UINT64), + DOUBLE( + "double", + DoubleOverload.DOUBLE_TO_DOUBLE, + DoubleOverload.INT64_TO_DOUBLE, + DoubleOverload.STRING_TO_DOUBLE, + DoubleOverload.UINT64_TO_DOUBLE), + STRING( + "string", + StringOverload.STRING_TO_STRING, + StringOverload.INT64_TO_STRING, + StringOverload.DOUBLE_TO_STRING, + StringOverload.BOOL_TO_STRING, + StringOverload.BYTES_TO_STRING, + StringOverload.TIMESTAMP_TO_STRING, + StringOverload.DURATION_TO_STRING, + StringOverload.UINT64_TO_STRING), + BYTES("bytes", BytesOverload.BYTES_TO_BYTES, BytesOverload.STRING_TO_BYTES), + DURATION( + "duration", DurationOverload.DURATION_TO_DURATION, DurationOverload.STRING_TO_DURATION), + TIMESTAMP( + "timestamp", + TimestampOverload.STRING_TO_TIMESTAMP, + TimestampOverload.TIMESTAMP_TO_TIMESTAMP, + TimestampOverload.INT64_TO_TIMESTAMP), + DYN("dyn", DynOverload.TO_DYN), + MATCHES("matches", MatchesOverload.MATCHES, MatchesOverload.MATCHES_STRING), + CONTAINS("contains", ContainsOverload.CONTAINS_STRING), + ENDS_WITH("endsWith", EndsWithOverload.ENDS_WITH_STRING), + STARTS_WITH("startsWith", StartsWithOverload.STARTS_WITH_STRING), + // Date/time Functions + GET_FULL_YEAR( + "getFullYear", + GetFullYearOverload.TIMESTAMP_TO_YEAR, + GetFullYearOverload.TIMESTAMP_TO_YEAR_WITH_TZ), + GET_MONTH( + "getMonth", + GetMonthOverload.TIMESTAMP_TO_MONTH, + GetMonthOverload.TIMESTAMP_TO_MONTH_WITH_TZ), + GET_DAY_OF_YEAR( + "getDayOfYear", + GetDayOfYearOverload.TIMESTAMP_TO_DAY_OF_YEAR, + GetDayOfYearOverload.TIMESTAMP_TO_DAY_OF_YEAR_WITH_TZ), + GET_DAY_OF_MONTH( + "getDayOfMonth", + GetDayOfMonthOverload.TIMESTAMP_TO_DAY_OF_MONTH, + GetDayOfMonthOverload.TIMESTAMP_TO_DAY_OF_MONTH_WITH_TZ), + GET_DATE( + "getDate", + GetDateOverload.TIMESTAMP_TO_DAY_OF_MONTH_1_BASED, + GetDateOverload.TIMESTAMP_TO_DAY_OF_MONTH_1_BASED_WITH_TZ), + GET_DAY_OF_WEEK( + "getDayOfWeek", + GetDayOfWeekOverload.TIMESTAMP_TO_DAY_OF_WEEK, + GetDayOfWeekOverload.TIMESTAMP_TO_DAY_OF_WEEK_WITH_TZ), + + GET_HOURS( + "getHours", + GetHoursOverload.TIMESTAMP_TO_HOURS, + GetHoursOverload.TIMESTAMP_TO_HOURS_WITH_TZ, + GetHoursOverload.DURATION_TO_HOURS), + GET_MINUTES( + "getMinutes", + GetMinutesOverload.TIMESTAMP_TO_MINUTES, + GetMinutesOverload.TIMESTAMP_TO_MINUTES_WITH_TZ, + GetMinutesOverload.DURATION_TO_MINUTES), + GET_SECONDS( + "getSeconds", + GetSecondsOverload.TIMESTAMP_TO_SECONDS, + GetSecondsOverload.TIMESTAMP_TO_SECONDS_WITH_TZ, + GetSecondsOverload.DURATION_TO_SECONDS), + GET_MILLISECONDS( + "getMilliseconds", + GetMillisecondsOverload.TIMESTAMP_TO_MILLISECONDS, + GetMillisecondsOverload.TIMESTAMP_TO_MILLISECONDS_WITH_TZ, + GetMillisecondsOverload.DURATION_TO_MILLISECONDS), + LESS( + Operator.LESS.getFunction(), + LessOverload.LESS_BOOL, + LessOverload.LESS_INT64, + LessOverload.LESS_UINT64, + LessOverload.LESS_DOUBLE, + LessOverload.LESS_STRING, + LessOverload.LESS_BYTES, + LessOverload.LESS_TIMESTAMP, + LessOverload.LESS_DURATION, + LessOverload.LESS_INT64_UINT64, + LessOverload.LESS_UINT64_INT64, + LessOverload.LESS_INT64_DOUBLE, + LessOverload.LESS_DOUBLE_INT64, + LessOverload.LESS_UINT64_DOUBLE, + LessOverload.LESS_DOUBLE_UINT64), + LESS_EQUALS( + Operator.LESS_EQUALS.getFunction(), + LessEqualsOverload.LESS_EQUALS_BOOL, + LessEqualsOverload.LESS_EQUALS_INT64, + LessEqualsOverload.LESS_EQUALS_UINT64, + LessEqualsOverload.LESS_EQUALS_DOUBLE, + LessEqualsOverload.LESS_EQUALS_STRING, + LessEqualsOverload.LESS_EQUALS_BYTES, + LessEqualsOverload.LESS_EQUALS_TIMESTAMP, + LessEqualsOverload.LESS_EQUALS_DURATION, + LessEqualsOverload.LESS_EQUALS_INT64_UINT64, + LessEqualsOverload.LESS_EQUALS_UINT64_INT64, + LessEqualsOverload.LESS_EQUALS_INT64_DOUBLE, + LessEqualsOverload.LESS_EQUALS_DOUBLE_INT64, + LessEqualsOverload.LESS_EQUALS_UINT64_DOUBLE, + LessEqualsOverload.LESS_EQUALS_DOUBLE_UINT64), + GREATER( + Operator.GREATER.getFunction(), + GreaterOverload.GREATER_BOOL, + GreaterOverload.GREATER_INT64, + GreaterOverload.GREATER_UINT64, + GreaterOverload.GREATER_DOUBLE, + GreaterOverload.GREATER_STRING, + GreaterOverload.GREATER_BYTES, + GreaterOverload.GREATER_TIMESTAMP, + GreaterOverload.GREATER_DURATION, + GreaterOverload.GREATER_INT64_UINT64, + GreaterOverload.GREATER_UINT64_INT64, + GreaterOverload.GREATER_INT64_DOUBLE, + GreaterOverload.GREATER_DOUBLE_INT64, + GreaterOverload.GREATER_UINT64_DOUBLE, + GreaterOverload.GREATER_DOUBLE_UINT64), + GREATER_EQUALS( + Operator.GREATER_EQUALS.getFunction(), + GreaterEqualsOverload.GREATER_EQUALS_BOOL, + GreaterEqualsOverload.GREATER_EQUALS_BYTES, + GreaterEqualsOverload.GREATER_EQUALS_DOUBLE, + GreaterEqualsOverload.GREATER_EQUALS_DURATION, + GreaterEqualsOverload.GREATER_EQUALS_INT64, + GreaterEqualsOverload.GREATER_EQUALS_STRING, + GreaterEqualsOverload.GREATER_EQUALS_TIMESTAMP, + GreaterEqualsOverload.GREATER_EQUALS_UINT64, + GreaterEqualsOverload.GREATER_EQUALS_INT64_UINT64, + GreaterEqualsOverload.GREATER_EQUALS_UINT64_INT64, + GreaterEqualsOverload.GREATER_EQUALS_INT64_DOUBLE, + GreaterEqualsOverload.GREATER_EQUALS_DOUBLE_INT64, + GreaterEqualsOverload.GREATER_EQUALS_UINT64_DOUBLE, + GreaterEqualsOverload.GREATER_EQUALS_DOUBLE_UINT64); + + private final String functionName; + private final ImmutableSet standardOverloads; + + StandardFunction(String functionName, CelStandardOverload... overloads) { + this.functionName = functionName; + this.standardOverloads = ImmutableSet.copyOf(overloads); + } + + @VisibleForTesting + ImmutableSet getOverloads() { + return standardOverloads; + } + } + + @VisibleForTesting + ImmutableSet getOverloads() { + return ImmutableSet.copyOf(standardOverloads.values()); + } + + @Internal + public ImmutableSet newFunctionBindings( + RuntimeEquality runtimeEquality, CelOptions celOptions) { + ImmutableSet.Builder builder = ImmutableSet.builder(); + + for (Map.Entry> entry : + standardOverloads.asMap().entrySet()) { + String functionName = entry.getKey(); + Collection overloads = entry.getValue(); + + ImmutableSet bindings = + overloads.stream() + .map(o -> o.newFunctionBinding(celOptions, runtimeEquality)) + .collect(toImmutableSet()); + + builder.addAll(CelFunctionBinding.fromOverloads(functionName, bindings)); + } + + return builder.build(); + } + + /** Builder for constructing the set of standard function/identifiers. */ + public static final class Builder { + private ImmutableSet includeFunctions; + private ImmutableSet excludeFunctions; + + private FunctionFilter functionFilter; + + private Builder() { + this.includeFunctions = ImmutableSet.of(); + this.excludeFunctions = ImmutableSet.of(); + } + + @CanIgnoreReturnValue + public Builder excludeFunctions(StandardFunction... functions) { + return excludeFunctions(ImmutableSet.copyOf(functions)); + } + + @CanIgnoreReturnValue + public Builder excludeFunctions(Iterable functions) { + this.excludeFunctions = checkNotNull(ImmutableSet.copyOf(functions)); + return this; + } + + @CanIgnoreReturnValue + public Builder includeFunctions(StandardFunction... functions) { + return includeFunctions(ImmutableSet.copyOf(functions)); + } + + @CanIgnoreReturnValue + public Builder includeFunctions(Iterable functions) { + this.includeFunctions = checkNotNull(ImmutableSet.copyOf(functions)); + return this; + } + + @CanIgnoreReturnValue + public Builder filterFunctions(FunctionFilter functionFilter) { + this.functionFilter = functionFilter; + return this; + } + + private static void assertOneSettingIsSet( + boolean a, boolean b, boolean c, String errorMessage) { + int count = 0; + if (a) { + count++; + } + if (b) { + count++; + } + if (c) { + count++; + } + + if (count > 1) { + throw new IllegalArgumentException(errorMessage); + } + } + + public CelStandardFunctions build() { + boolean hasIncludeFunctions = !this.includeFunctions.isEmpty(); + boolean hasExcludeFunctions = !this.excludeFunctions.isEmpty(); + boolean hasFilterFunction = this.functionFilter != null; + assertOneSettingIsSet( + hasIncludeFunctions, + hasExcludeFunctions, + hasFilterFunction, + "You may only populate one of the following builder methods: includeFunctions," + + " excludeFunctions or filterFunctions"); + + ImmutableMultimap.Builder standardOverloadBuilder = + ImmutableMultimap.builder(); + for (StandardFunction standardFunction : StandardFunction.values()) { + if (hasIncludeFunctions) { + if (this.includeFunctions.contains(standardFunction)) { + standardOverloadBuilder.putAll( + standardFunction.functionName, standardFunction.standardOverloads); + } + continue; + } + if (hasExcludeFunctions) { + if (!this.excludeFunctions.contains(standardFunction)) { + standardOverloadBuilder.putAll( + standardFunction.functionName, standardFunction.standardOverloads); + } + continue; + } + if (hasFilterFunction) { + for (CelStandardOverload standardOverload : standardFunction.standardOverloads) { + boolean includeOverload = functionFilter.include(standardFunction, standardOverload); + if (includeOverload) { + standardOverloadBuilder.put(standardFunction.functionName, standardOverload); + } + } + + continue; + } + + standardOverloadBuilder.putAll( + standardFunction.functionName, standardFunction.standardOverloads); + } + + return new CelStandardFunctions(standardOverloadBuilder.build()); + } + + /** + * Functional interface for filtering standard functions. Returning true in the callback will + * include the function in the environment. + */ + @FunctionalInterface + public interface FunctionFilter { + boolean include(StandardFunction standardFunction, CelStandardOverload standardOverload); + } + } + + /** Creates a new builder to configure CelStandardFunctions. */ + public static Builder newBuilder() { + return new Builder(); + } + + static boolean isHeterogeneousComparison(CelStandardOverload overload) { + return HETEROGENEOUS_COMPARISON_OPERATORS.contains(overload); + } + + private CelStandardFunctions(ImmutableMultimap standardOverloads) { + this.standardOverloads = standardOverloads; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/CelUnknownSet.java b/runtime/src/main/java/dev/cel/runtime/CelUnknownSet.java index 8cde13361..62d975f93 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelUnknownSet.java +++ b/runtime/src/main/java/dev/cel/runtime/CelUnknownSet.java @@ -16,6 +16,7 @@ import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; /** * Unknown set representation. @@ -28,22 +29,45 @@ */ @AutoValue public abstract class CelUnknownSet { + + /** + * Set of attributes with a series of selection or index operations marked unknown. This set is + * always empty if enableUnknownTracking is disabled in {@code CelOptions}. + */ public abstract ImmutableSet attributes(); - public static CelUnknownSet create(ImmutableSet attributes) { - return new AutoValue_CelUnknownSet(attributes); - } + /** Set of subexpression IDs that were decided to be unknown and in the critical path. */ + public abstract ImmutableSet unknownExprIds(); public static CelUnknownSet create(CelAttribute attribute) { return create(ImmutableSet.of(attribute)); } + public static CelUnknownSet create(ImmutableSet attributes) { + return create(attributes, ImmutableSet.of()); + } + + public static CelUnknownSet create(Long... unknownExprIds) { + return create(ImmutableSet.copyOf(unknownExprIds)); + } + + public static CelUnknownSet create(CelAttribute attribute, Iterable unknownExprIds) { + return create(ImmutableSet.of(attribute), ImmutableSet.copyOf(unknownExprIds)); + } + + static CelUnknownSet create(Iterable unknownExprIds) { + return create(ImmutableSet.of(), ImmutableSet.copyOf(unknownExprIds)); + } + + public static CelUnknownSet create( + ImmutableSet attributes, ImmutableSet unknownExprIds) { + return new AutoValue_CelUnknownSet(attributes, unknownExprIds); + } + public static CelUnknownSet union(CelUnknownSet lhs, CelUnknownSet rhs) { return create( - ImmutableSet.builder() - .addAll(lhs.attributes()) - .addAll(rhs.attributes()) - .build()); + Sets.union(lhs.attributes(), rhs.attributes()).immutableCopy(), + Sets.union(lhs.unknownExprIds(), rhs.unknownExprIds()).immutableCopy()); } public CelUnknownSet merge(CelUnknownSet rhs) { diff --git a/runtime/src/main/java/dev/cel/runtime/CelValueRuntimeTypeProvider.java b/runtime/src/main/java/dev/cel/runtime/CelValueRuntimeTypeProvider.java new file mode 100644 index 000000000..38365127c --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/CelValueRuntimeTypeProvider.java @@ -0,0 +1,137 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.errorprone.annotations.Immutable; +import com.google.protobuf.MessageLite; +import dev.cel.common.annotations.Internal; +import dev.cel.common.exceptions.CelAttributeNotFoundException; +import dev.cel.common.values.BaseProtoCelValueConverter; +import dev.cel.common.values.BaseProtoMessageValueProvider; +import dev.cel.common.values.CelValueProvider; +import dev.cel.common.values.CombinedCelValueProvider; +import dev.cel.common.values.SelectableValue; +import java.util.Map; +import java.util.NoSuchElementException; + +/** Bridge between the old RuntimeTypeProvider and CelValueProvider APIs. */ +@Internal +@Immutable +final class CelValueRuntimeTypeProvider implements RuntimeTypeProvider { + + private final CelValueProvider valueProvider; + private final BaseProtoCelValueConverter protoCelValueConverter; + private static final BaseProtoCelValueConverter DEFAULT_CEL_VALUE_CONVERTER = + new BaseProtoCelValueConverter() {}; + + static CelValueRuntimeTypeProvider newInstance(CelValueProvider valueProvider) { + BaseProtoCelValueConverter converter = DEFAULT_CEL_VALUE_CONVERTER; + + // Find the underlying ProtoCelValueConverter. + // This is required because DefaultInterpreter works with a resolved protobuf messages directly + // in evaluation flow. + // A new runtime should not directly depend on protobuf, thus this will not be needed in the + // future. + if (valueProvider instanceof BaseProtoMessageValueProvider) { + converter = ((BaseProtoMessageValueProvider) valueProvider).protoCelValueConverter(); + } else if (valueProvider instanceof CombinedCelValueProvider) { + converter = + ((CombinedCelValueProvider) valueProvider) + .valueProviders().stream() + .filter(p -> p instanceof BaseProtoMessageValueProvider) + .map(p -> ((BaseProtoMessageValueProvider) p).protoCelValueConverter()) + .findFirst() + .orElse(DEFAULT_CEL_VALUE_CONVERTER); + } + + return new CelValueRuntimeTypeProvider(valueProvider, converter); + } + + @Override + public Object createMessage(String messageName, Map values) { + return protoCelValueConverter.maybeUnwrap( + valueProvider + .newValue(messageName, values) + .orElseThrow( + () -> + new NoSuchElementException( + String.format("cannot resolve '%s' as a message", messageName)))); + } + + @Override + public Object selectField(Object message, String fieldName) { + if (message instanceof Map) { + Map map = (Map) message; + if (map.containsKey(fieldName)) { + return map.get(fieldName); + } + + throw CelAttributeNotFoundException.forMissingMapKey(fieldName); + } + + SelectableValue selectableValue = getSelectableValueOrThrow(message, fieldName); + Object value = selectableValue.select(fieldName); + + return protoCelValueConverter.maybeUnwrap(value); + } + + @Override + public Object hasField(Object message, String fieldName) { + SelectableValue selectableValue = getSelectableValueOrThrow(message, fieldName); + + return selectableValue.find(fieldName).isPresent(); + } + + @SuppressWarnings("unchecked") + private SelectableValue getSelectableValueOrThrow(Object obj, String fieldName) { + Object convertedCelValue = protoCelValueConverter.toRuntimeValue(obj); + + if (!(convertedCelValue instanceof SelectableValue)) { + throwInvalidFieldSelection(fieldName); + } + + return (SelectableValue) convertedCelValue; + } + + @Override + public Object adapt(String messageName, Object message) { + if (message instanceof CelUnknownSet) { + return message; // CelUnknownSet is handled specially for iterative evaluation. No need to + // adapt to CelValue. + } + + if (message instanceof MessageLite.Builder) { + message = ((MessageLite.Builder) message).build(); + } + + if (message instanceof MessageLite) { + return protoCelValueConverter.maybeUnwrap(protoCelValueConverter.toRuntimeValue(message)); + } + + return message; + } + + private static void throwInvalidFieldSelection(String fieldName) { + throw CelAttributeNotFoundException.forFieldResolution(fieldName); + } + + private CelValueRuntimeTypeProvider( + CelValueProvider valueProvider, BaseProtoCelValueConverter protoCelValueConverter) { + this.valueProvider = checkNotNull(valueProvider); + this.protoCelValueConverter = checkNotNull(protoCelValueConverter); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/CelVariableResolver.java b/runtime/src/main/java/dev/cel/runtime/CelVariableResolver.java index ea5cfb4ac..76e6bc76e 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelVariableResolver.java +++ b/runtime/src/main/java/dev/cel/runtime/CelVariableResolver.java @@ -50,9 +50,6 @@ public interface CelVariableResolver { */ static CelVariableResolver hierarchicalVariableResolver( CelVariableResolver primary, CelVariableResolver secondary) { - return (name) -> { - Optional value = primary.find(name); - return value.isPresent() ? value : secondary.find(name); - }; + return HierarchicalVariableResolver.newInstance(primary, secondary); } } diff --git a/runtime/src/main/java/dev/cel/runtime/ConcatenatedListView.java b/runtime/src/main/java/dev/cel/runtime/ConcatenatedListView.java new file mode 100644 index 000000000..c15e76f77 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/ConcatenatedListView.java @@ -0,0 +1,107 @@ +// 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 dev.cel.common.annotations.Internal; +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +/** + * A custom list view implementation that allows O(1) concatenation of two lists. Its primary + * purpose is to facilitate efficient accumulation of lists for later materialization. (ex: + * comprehensions that dispatch `add_list` to concat N lists together). + * + *

This does not support any of the standard list operations from {@link java.util.List}. + * + + *

CEL Library Internals. Do Not Use. + */ +@Internal +public final class ConcatenatedListView extends AbstractList { + private final List> sourceLists; + private int totalSize = 0; + + ConcatenatedListView() { + this.sourceLists = new ArrayList<>(); + } + + public ConcatenatedListView(Collection collection) { + this(); + addAll(collection); + } + + @Override + public boolean addAll(Collection collection) { + if (!(collection instanceof List)) { + // size() is O(1) iff it's a list + throw new IllegalStateException("addAll must be called with lists, not collections"); + } + + sourceLists.add((List) collection); + totalSize += collection.size(); + return true; + } + + @Override + public E get(int index) { + throw new UnsupportedOperationException("get method not supported."); + } + + @Override + public int size() { + return totalSize; + } + + @Override + public Iterator iterator() { + return new ConcatenatingIterator(); + } + + /** Custom iterator to provide a flat view of all concatenated collections */ + private class ConcatenatingIterator implements Iterator { + private int index = 0; + private Iterator iterator = null; + + @Override + public boolean hasNext() { + while (iterator == null || !iterator.hasNext()) { + if (index < sourceLists.size()) { + iterator = sourceLists.get(index).iterator(); + index++; + } else { + return false; + } + } + return true; + } + + @Override + public E next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + return iterator.next(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException("remove method not supported"); + } + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/DefaultDispatcher.java b/runtime/src/main/java/dev/cel/runtime/DefaultDispatcher.java index 09f70c52a..0a467db81 100644 --- a/runtime/src/main/java/dev/cel/runtime/DefaultDispatcher.java +++ b/runtime/src/main/java/dev/cel/runtime/DefaultDispatcher.java @@ -14,227 +14,221 @@ 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.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 javax.annotation.concurrent.ThreadSafe; -import com.google.errorprone.annotations.concurrent.GuardedBy; -import com.google.protobuf.MessageLite; import dev.cel.common.CelErrorCode; -import dev.cel.common.CelOptions; -import dev.cel.common.ExprFeatures; import dev.cel.common.annotations.Internal; -import dev.cel.common.internal.DefaultMessageFactory; -import dev.cel.common.internal.DynamicProto; +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; /** - * Default implementation of {@link Dispatcher}. - * - *

Should be final, do not mock; mocking {@link Dispatcher} instead. + * Default implementation of dispatcher. * *

CEL Library Internals. Do Not Use. */ -@ThreadSafe +@Immutable @Internal -public final class DefaultDispatcher implements Dispatcher, Registrar { - @SuppressWarnings("unused") - private final CelOptions celOptions; +public final class DefaultDispatcher implements CelFunctionResolver { - /** - * @deprecated use {@link DefaultDispatcher(CelOptions)} instead. - */ - @Deprecated - public DefaultDispatcher(ImmutableSet features) { - this(CelOptions.fromExprFeatures(features)); - } - - public DefaultDispatcher(CelOptions celOptions) { - this.celOptions = celOptions; - } - - /** - * Creates a new dispatcher with all standard functions. - * - * @deprecated use {@link #create(CelOptions)} instead. - */ - @Deprecated - public static DefaultDispatcher create(ImmutableSet features) { - return create(CelOptions.fromExprFeatures(features)); - } + private final ImmutableMap overloads; - public static DefaultDispatcher create(CelOptions celOptions) { - DynamicProto dynamicProto = DynamicProto.create(DefaultMessageFactory.INSTANCE); - return create(celOptions, dynamicProto); + public Optional findOverload(String functionName) { + return Optional.ofNullable(overloads.get(functionName)); } - public static DefaultDispatcher create(CelOptions celOptions, DynamicProto dynamicProto) { - DefaultDispatcher dispatcher = new DefaultDispatcher(celOptions); - StandardFunctions.add(dispatcher, dynamicProto, celOptions); - return dispatcher; + @Override + public Optional findOverloadMatchingArgs(String functionName, Object[] args) + throws CelEvaluationException { + return findOverloadMatchingArgs(functionName, overloads.keySet(), overloads, args); } - public static DefaultDispatcher create() { - return create(CelOptions.LEGACY); + @Override + public Optional findOverloadMatchingArgs( + String functionName, Collection overloadIds, Object[] args) + throws CelEvaluationException { + return findOverloadMatchingArgs(functionName, overloadIds, overloads, args); } - /** Internal representation of an overload. */ - @Immutable - private static final class Overload { - final ImmutableList> parameterTypes; - - /** See {@link Function}. */ - final Function function; - - private Overload(Class[] parameterTypes, Function function) { - this.parameterTypes = ImmutableList.copyOf(parameterTypes); - this.function = function; - } - - /** Determines whether this overload can handle the given arguments. */ - private boolean canHandle(Object[] arguments) { - if (parameterTypes.size() != arguments.length) { - return false; - } - 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. - if (paramType != Object.class - && !MessageLite.class.isAssignableFrom(paramType) - && !Map.class.isAssignableFrom(paramType)) { - return false; + /** Finds the overload that matches the given function name, overload IDs, and arguments. */ + static Optional findOverloadMatchingArgs( + String functionName, + Collection overloadIds, + Map overloads, + Object[] args) + throws CelEvaluationException { + int matchingOverloadCount = 0; + CelResolvedOverload match = null; + List candidates = null; + for (String overloadId : overloadIds) { + CelResolvedOverload overload = overloads.get(overloadId); + // If the overload is null, it means that the function was not registered; however, it is + // possible that the overload refers to a late-bound function. + if (overload != null && overload.canHandle(args)) { + if (++matchingOverloadCount > 1) { + if (candidates == null) { + candidates = new ArrayList<>(); + candidates.add(match.getOverloadId()); } - continue; - } - if (!paramType.isAssignableFrom(arg.getClass())) { - return false; + candidates.add(overloadId); } + match = overload; } - return true; } + + if (matchingOverloadCount > 1) { + throw CelEvaluationExceptionBuilder.newBuilder( + "Ambiguous overloads for function '%s'. Matching candidates: %s", + functionName, Joiner.on(", ").join(candidates)) + .setErrorCode(CelErrorCode.AMBIGUOUS_OVERLOAD) + .build(); + } + return Optional.ofNullable(match); } - @GuardedBy("this") - private final Map overloads = new HashMap<>(); + /** + * Finds the single registered overload iff it's marked as a non-strict function. + * + *

The intent behind this function is to provide an at-parity behavior with existing + * DefaultInterpreter, where it historically special-cased locating a single overload for certain + * non-strict functions, such as not_strictly_false. This method should not be used outside of + * this specific context. + * + * @throws IllegalStateException if there are multiple overloads that are marked non-strict. + */ + Optional findSingleNonStrictOverload(List overloadIds) { + for (String overloadId : overloadIds) { + CelResolvedOverload overload = overloads.get(overloadId); + if (overload != null && !overload.isStrict()) { + if (overloadIds.size() > 1) { + throw new IllegalStateException( + String.format( + "%d overloads found for a non-strict function. Expected 1.", overloadIds.size())); + } + return Optional.of(overload); + } + } - @Override - @SuppressWarnings("unchecked") - public synchronized void add( - String overloadId, Class argType, final UnaryFunction function) { - overloads.put( - overloadId, new Overload(new Class[] {argType}, args -> function.apply((T) args[0]))); + return Optional.empty(); } - @Override - @SuppressWarnings("unchecked") - public synchronized void add( - String overloadId, - Class argType1, - Class argType2, - final BinaryFunction function) { - overloads.put( - overloadId, - new Overload( - new Class[] {argType1, argType2}, - args -> function.apply((T1) args[0], (T2) args[1]))); + public static Builder newBuilder() { + return new Builder(); } - @Override - public synchronized void add(String overloadId, List> argTypes, Function function) { - overloads.put(overloadId, new Overload(argTypes.toArray(new Class[0]), function)); - } + /** Builder for {@link DefaultDispatcher}. */ + public static class Builder { - private static Object dispatch( - Metadata metadata, - long exprId, - String functionName, - List overloadIds, - Map overloads, - Object[] args) - throws InterpreterException { - List candidates = new ArrayList<>(); - for (String overloadId : overloadIds) { - Overload overload = overloads.get(overloadId); - if (overload == null) { - throw new InterpreterException.Builder( - "[internal] Unknown overload id '%s' for function '%s'", overloadId, functionName) - .setErrorCode(CelErrorCode.OVERLOAD_NOT_FOUND) - .setLocation(metadata, exprId) - .build(); - } - if (overload.canHandle(args)) { - candidates.add(overloadId); - } - } - if (candidates.size() == 1) { - String overloadId = candidates.get(0); - try { - return overloads.get(overloadId).function.apply(args); - } catch (RuntimeException e) { - throw new InterpreterException.Builder( - e, "Function '%s' failed with arg(s) '%s'", overloadId, Joiner.on(", ").join(args)) - .build(); + @AutoValue + @Immutable + abstract static class OverloadEntry { + abstract String functionName(); + + 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); } } - if (candidates.size() > 1) { - throw new InterpreterException.Builder( - "Ambiguous overloads for function '%s'. Matching candidates: %s", - functionName, Joiner.on(",").join(candidates)) - .setErrorCode(CelErrorCode.AMBIGUOUS_OVERLOAD) - .setLocation(metadata, exprId) - .build(); + + 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); + + OverloadEntry newEntry = OverloadEntry.of(functionName, argTypes, isStrict, overload); + + overloads.merge( + overloadId, + newEntry, + (existing, incoming) -> mergeDynamicDispatchesOrThrow(overloadId, existing, incoming)); + + return this; } - throw new InterpreterException.Builder( - "No matching overload for function '%s'. Overload candidates: %s", - functionName, Joiner.on(",").join(overloadIds)) - .setErrorCode(CelErrorCode.OVERLOAD_NOT_FOUND) - .setLocation(metadata, exprId) - .build(); - } + private OverloadEntry mergeDynamicDispatchesOrThrow( + String overloadId, OverloadEntry existing, OverloadEntry incoming) { + if (existing.overload() instanceof DynamicDispatchOverload + && incoming.overload() instanceof DynamicDispatchOverload) { - @Override - public synchronized Object dispatch( - Metadata metadata, long exprId, String functionName, List overloadIds, Object[] args) - throws InterpreterException { - return dispatch(metadata, exprId, functionName, overloadIds, overloads, args); - } + DynamicDispatchOverload existingOverload = (DynamicDispatchOverload) existing.overload(); + DynamicDispatchOverload incomingOverload = (DynamicDispatchOverload) incoming.overload(); - @Override - public synchronized Dispatcher.ImmutableCopy immutableCopy() { - return new ImmutableCopy(overloads); - } + DynamicDispatchOverload mergedOverload = + new DynamicDispatchOverload( + overloadId, + ImmutableSet.builder() + .addAll(existingOverload.getOverloadBindings()) + .addAll(incomingOverload.getOverloadBindings()) + .build()); - @Immutable - private static final class ImmutableCopy implements Dispatcher.ImmutableCopy { - private final ImmutableMap overloads; + boolean isStrict = + mergedOverload.getOverloadBindings().stream().allMatch(CelFunctionBinding::isStrict); - private ImmutableCopy(Map overloads) { - this.overloads = ImmutableMap.copyOf(overloads); + return OverloadEntry.of(overloadId, incoming.argTypes(), isStrict, mergedOverload); + } + + throw new IllegalArgumentException("Duplicate overload ID binding: " + overloadId); } - @Override - public Object dispatch( - Metadata metadata, - long exprId, - String functionName, - List overloadIds, - Object[] args) - throws InterpreterException { - return DefaultDispatcher.dispatch( - metadata, exprId, functionName, overloadIds, overloads, args); + 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()); } - @Override - public Dispatcher.ImmutableCopy immutableCopy() { - return this; + private Builder() { + this.overloads = new HashMap<>(); } } + + DefaultDispatcher(ImmutableMap overloads) { + this.overloads = overloads; + } } diff --git a/runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java b/runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java index 3c4c0003a..fdab71c3d 100644 --- a/runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java +++ b/runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java @@ -14,73 +14,58 @@ package dev.cel.runtime; -import dev.cel.expr.CheckedExpr; -import dev.cel.expr.Value; +import static com.google.common.base.Preconditions.checkNotNull; + import com.google.auto.value.AutoValue; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Joiner; import com.google.common.base.Preconditions; +import com.google.common.base.Strings; 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 javax.annotation.concurrent.ThreadSafe; +import com.google.protobuf.ByteString; +import com.google.protobuf.NullValue; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelErrorCode; import dev.cel.common.CelOptions; -import dev.cel.common.CelProtoAbstractSyntaxTree; -import dev.cel.common.annotations.Internal; import dev.cel.common.ast.CelConstant; import dev.cel.common.ast.CelExpr; import dev.cel.common.ast.CelExpr.CelCall; import dev.cel.common.ast.CelExpr.CelComprehension; -import dev.cel.common.ast.CelExpr.CelCreateList; -import dev.cel.common.ast.CelExpr.CelCreateMap; -import dev.cel.common.ast.CelExpr.CelCreateStruct; +import dev.cel.common.ast.CelExpr.CelList; +import dev.cel.common.ast.CelExpr.CelMap; import dev.cel.common.ast.CelExpr.CelSelect; +import dev.cel.common.ast.CelExpr.CelStruct; import dev.cel.common.ast.CelExpr.ExprKind; import dev.cel.common.ast.CelReference; +import dev.cel.common.exceptions.CelRuntimeException; import dev.cel.common.types.CelKind; import dev.cel.common.types.CelType; +import dev.cel.common.types.TypeType; +import dev.cel.common.values.CelByteString; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; -/** - * Default implementation of the CEL interpreter. - * - *

Use as in: - * - *

- *   MessageFactory messageFactory = new LinkedMessageFactory();
- *   RuntimeTypeProvider typeProvider = new DescriptorMessageProvider(messageFactory);
- *   Dispatcher dispatcher = DefaultDispatcher.create();
- *   Interpreter interpreter = new DefaultInterpreter(typeProvider, dispatcher);
- *   Interpretable interpretable = interpreter.createInterpretable(checkedExpr);
- *   Object result = interpretable.eval(Activation.of("name", value));
- * 
- * - *

Extensions functions can be added in addition to standard functions to the dispatcher as - * needed. - * - *

Note: {MessageFactory} instances may be combined using the {@link - * MessageFactory.CombinedMessageFactory}. - * - *

Note: On Android, the {@code DescriptorMessageProvider} is not supported as proto lite does - * not support descriptors. Instead, implement the {@code MessageProvider} interface directly. - * - *

CEL Library Internals. Do Not Use. - */ +/** Default implementation of the CEL interpreter. */ @ThreadSafe -@Internal -public final class DefaultInterpreter implements Interpreter { +final class DefaultInterpreter implements Interpreter { + private final TypeResolver typeResolver; private final RuntimeTypeProvider typeProvider; - private final Dispatcher dispatcher; + private final DefaultDispatcher dispatcher; private final CelOptions celOptions; /** @@ -108,10 +93,6 @@ static IntermediateResult create(Object value) { } } - public DefaultInterpreter(RuntimeTypeProvider typeProvider, Dispatcher dispatcher) { - this(typeProvider, dispatcher, CelOptions.LEGACY); - } - /** * Creates a new interpreter * @@ -120,71 +101,138 @@ public DefaultInterpreter(RuntimeTypeProvider typeProvider, Dispatcher dispatche * @param celOptions the configurable flags for adjusting evaluation behavior. */ public DefaultInterpreter( - RuntimeTypeProvider typeProvider, Dispatcher dispatcher, CelOptions celOptions) { - this.typeProvider = Preconditions.checkNotNull(typeProvider); - this.dispatcher = Preconditions.checkNotNull(dispatcher); - this.celOptions = celOptions; - } - - @Override - @Deprecated - public Interpretable createInterpretable(CheckedExpr checkedExpr) { - return createInterpretable(CelProtoAbstractSyntaxTree.fromCheckedExpr(checkedExpr).getAst()); + TypeResolver typeResolver, + RuntimeTypeProvider typeProvider, + DefaultDispatcher dispatcher, + CelOptions celOptions) { + this.typeResolver = checkNotNull(typeResolver); + this.typeProvider = checkNotNull(typeProvider); + this.dispatcher = checkNotNull(dispatcher); + this.celOptions = checkNotNull(celOptions); } @Override public Interpretable createInterpretable(CelAbstractSyntaxTree ast) { - return new DefaultInterpretable(typeProvider, dispatcher, ast, celOptions); + return new DefaultInterpretable(typeResolver, typeProvider, dispatcher, ast, celOptions); } @Immutable - private static final class DefaultInterpretable - implements Interpretable, UnknownTrackingInterpretable { + @VisibleForTesting + static final class DefaultInterpretable implements Interpretable, UnknownTrackingInterpretable { + private final TypeResolver typeResolver; private final RuntimeTypeProvider typeProvider; - private final Dispatcher.ImmutableCopy dispatcher; + private final DefaultDispatcher dispatcher; private final Metadata metadata; private final CelAbstractSyntaxTree ast; private final CelOptions celOptions; DefaultInterpretable( + TypeResolver typeResolver, RuntimeTypeProvider typeProvider, - Dispatcher dispatcher, + DefaultDispatcher dispatcher, CelAbstractSyntaxTree ast, CelOptions celOptions) { - this.typeProvider = Preconditions.checkNotNull(typeProvider); - this.dispatcher = Preconditions.checkNotNull(dispatcher).immutableCopy(); - this.ast = Preconditions.checkNotNull(ast); + this.typeResolver = checkNotNull(typeResolver); + this.typeProvider = checkNotNull(typeProvider); + this.dispatcher = checkNotNull(dispatcher); + this.ast = checkNotNull(ast); this.metadata = new DefaultMetadata(ast); - this.celOptions = Preconditions.checkNotNull(celOptions); + this.celOptions = checkNotNull(celOptions); } @Override - public Object eval(GlobalResolver resolver) throws InterpreterException { + public Object eval(GlobalResolver resolver) throws CelEvaluationException { // Result is already unwrapped from IntermediateResult. - return eval(resolver, CelEvaluationListener.noOpListener()); + return evalTrackingUnknowns( + RuntimeUnknownResolver.fromResolver(resolver), Optional.empty(), Optional.empty()); } @Override public Object eval(GlobalResolver resolver, CelEvaluationListener listener) - throws InterpreterException { - return evalTrackingUnknowns(RuntimeUnknownResolver.fromResolver(resolver), listener); + throws CelEvaluationException { + return evalTrackingUnknowns( + RuntimeUnknownResolver.fromResolver(resolver), Optional.empty(), Optional.of(listener)); + } + + @Override + public Object eval(GlobalResolver resolver, CelFunctionResolver lateBoundFunctionResolver) + throws CelEvaluationException { + return evalTrackingUnknowns( + RuntimeUnknownResolver.fromResolver(resolver), + Optional.of(lateBoundFunctionResolver), + Optional.empty()); + } + + @Override + public Object eval( + GlobalResolver resolver, + CelFunctionResolver lateBoundFunctionResolver, + CelEvaluationListener listener) + throws CelEvaluationException { + return evalTrackingUnknowns( + RuntimeUnknownResolver.fromResolver(resolver), + Optional.of(lateBoundFunctionResolver), + Optional.of(listener)); } @Override public Object evalTrackingUnknowns( - RuntimeUnknownResolver resolver, CelEvaluationListener listener) - throws InterpreterException { - ExecutionFrame frame = - new ExecutionFrame(listener, resolver, celOptions.comprehensionMaxIterations()); + RuntimeUnknownResolver resolver, + Optional functionResolver, + Optional listener) + throws CelEvaluationException { + ExecutionFrame frame = newExecutionFrame(resolver, functionResolver, listener); IntermediateResult internalResult = evalInternal(frame, ast.getExpr()); - Object result = internalResult.value(); - // TODO: remove support for IncompleteData. - return InterpreterUtil.completeDataOnly( - result, "Incomplete data cannot be returned as a result."); + + Object underlyingValue = internalResult.value(); + + return maybeAdaptToCelUnknownSet(underlyingValue); + } + + private 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())); + } + + /** + * Evaluates this interpretable and returns the resulting execution frame populated with + * evaluation state. This method is specifically designed for testing the interpreter's internal + * invariants. + * + *

Do not expose to public. This method is strictly for internal testing purposes + * only. + */ + @VisibleForTesting + @CanIgnoreReturnValue + ExecutionFrame populateExecutionFrame(ExecutionFrame frame) throws CelEvaluationException { + evalInternal(frame, ast.getExpr()); + + return frame; + } + + @VisibleForTesting + ExecutionFrame newTestExecutionFrame(GlobalResolver resolver) { + return newExecutionFrame( + RuntimeUnknownResolver.fromResolver(resolver), Optional.empty(), Optional.empty()); + } + + private ExecutionFrame newExecutionFrame( + RuntimeUnknownResolver resolver, + Optional functionResolver, + Optional listener) { + int comprehensionMaxIterations = + celOptions.enableComprehension() ? celOptions.comprehensionMaxIterations() : 0; + return new ExecutionFrame(listener, resolver, functionResolver, comprehensionMaxIterations); } private IntermediateResult evalInternal(ExecutionFrame frame, CelExpr expr) - throws InterpreterException { + throws CelEvaluationException { try { ExprKind.Kind exprKind = expr.exprKind().getKind(); IntermediateResult result; @@ -201,14 +249,14 @@ private IntermediateResult evalInternal(ExecutionFrame frame, CelExpr expr) case CALL: result = evalCall(frame, expr, expr.call()); break; - case CREATE_LIST: - result = evalList(frame, expr, expr.createList()); + case LIST: + result = evalList(frame, expr, expr.list()); break; - case CREATE_STRUCT: - result = evalStruct(frame, expr, expr.createStruct()); + case STRUCT: + result = evalStruct(frame, expr, expr.struct()); break; - case CREATE_MAP: - result = evalMap(frame, expr.createMap()); + case MAP: + result = evalMap(frame, expr.map()); break; case COMPREHENSION: result = evalComprehension(frame, expr, expr.comprehension()); @@ -217,24 +265,37 @@ private IntermediateResult evalInternal(ExecutionFrame frame, CelExpr expr) throw new IllegalStateException( "unexpected expression kind: " + expr.exprKind().getKind()); } - frame.getEvaluationListener().callback(expr, result.value()); + + frame + .getEvaluationListener() + .ifPresent( + listener -> listener.callback(expr, maybeAdaptToCelUnknownSet(result.value()))); return result; + } catch (CelRuntimeException e) { + throw CelEvaluationExceptionBuilder.newBuilder(e).setMetadata(metadata, expr.id()).build(); } catch (RuntimeException e) { - throw new InterpreterException.Builder(e, e.getMessage()) - .setLocation(metadata, expr.id()) + throw CelEvaluationExceptionBuilder.newBuilder(e.getMessage()) + .setCause(e) + .setMetadata(metadata, expr.id()) .build(); } } - private boolean isUnknownValue(Object value) { - return value instanceof CelUnknownSet || InterpreterUtil.isUnknown(value); + private static boolean isUnknownValue(Object value) { + return InterpreterUtil.isAccumulatedUnknowns(value); + } + + private static boolean isUnknownOrError(Object value) { + return isUnknownValue(value) || value instanceof Exception; } private Object evalConstant( ExecutionFrame unusedFrame, CelExpr unusedExpr, CelConstant constExpr) { switch (constExpr.getKind()) { case NULL_VALUE: - return constExpr.nullValue(); + return celOptions.evaluateCanonicalTypesToNativeValues() + ? constExpr.nullValue() + : NullValue.NULL_VALUE; case BOOLEAN_VALUE: return constExpr.booleanValue(); case INT64_VALUE: @@ -250,14 +311,19 @@ private Object evalConstant( case STRING_VALUE: return constExpr.stringValue(); case BYTES_VALUE: - return constExpr.bytesValue(); + CelByteString celByteString = constExpr.bytesValue(); + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return celByteString; + } + + return ByteString.copyFrom(celByteString.toByteArray()); default: throw new IllegalStateException("unsupported constant case: " + constExpr.getKind()); } } private IntermediateResult evalIdent(ExecutionFrame frame, CelExpr expr) - throws InterpreterException { + throws CelEvaluationException { CelReference reference = ast.getReferenceOrThrow(expr.id()); if (reference.value().isPresent()) { return IntermediateResult.create(evalConstant(frame, expr, reference.value().get())); @@ -266,11 +332,11 @@ private IntermediateResult evalIdent(ExecutionFrame frame, CelExpr expr) } private IntermediateResult resolveIdent(ExecutionFrame frame, CelExpr expr, String name) - throws InterpreterException { + throws CelEvaluationException { // Check whether the type exists in the type check map as a 'type'. - Optional checkedType = ast.getType(expr.id()); - if (checkedType.isPresent() && checkedType.get().kind() == CelKind.TYPE) { - Object typeValue = typeProvider.adaptType(checkedType.get()); + CelType checkedType = getCheckedTypeOrThrow(expr); + if (checkedType.kind() == CelKind.TYPE) { + TypeType typeValue = typeResolver.adaptType(checkedType); return IntermediateResult.create(typeValue); } @@ -278,11 +344,17 @@ private IntermediateResult resolveIdent(ExecutionFrame frame, CelExpr expr, Stri Object value = rawResult.value(); boolean isLazyExpression = value instanceof LazyExpression; if (isLazyExpression) { - value = evalInternal(frame, ((LazyExpression) value).celExpr).value(); + frame.markLazyEvaluationOrThrow(name); + + try { + value = evalInternal(frame, ((LazyExpression) value).celExpr).value(); + } finally { + frame.endLazyEvaluation(name); + } } // Value resolved from Binding, it could be Message, PartialMessage or unbound(null) - value = InterpreterUtil.strict(typeProvider.adapt(value)); + value = InterpreterUtil.strict(typeProvider.adapt(checkedType.name(), value)); IntermediateResult result = IntermediateResult.create(rawResult.attribute(), value); if (isLazyExpression) { @@ -293,7 +365,7 @@ private IntermediateResult resolveIdent(ExecutionFrame frame, CelExpr expr, Stri } private IntermediateResult evalSelect(ExecutionFrame frame, CelExpr expr, CelSelect selectExpr) - throws InterpreterException { + throws CelEvaluationException { Optional referenceOptional = ast.getReference(expr.id()); if (referenceOptional.isPresent()) { CelReference reference = referenceOptional.get(); @@ -311,7 +383,7 @@ private IntermediateResult evalSelect(ExecutionFrame frame, CelExpr expr, CelSel private IntermediateResult evalFieldSelect( ExecutionFrame frame, CelExpr expr, CelExpr operandExpr, String field, boolean isTestOnly) - throws InterpreterException { + throws CelEvaluationException { // This indicates this is a field selection on the operand. IntermediateResult operandResult = evalInternal(frame, operandExpr); Object operand = operandResult.value(); @@ -340,7 +412,7 @@ private IntermediateResult evalFieldSelect( } private IntermediateResult evalCall(ExecutionFrame frame, CelExpr expr, CelCall callExpr) - throws InterpreterException { + throws CelEvaluationException { CelReference reference = ast.getReferenceOrThrow(expr.id()); Preconditions.checkState(!reference.overloadIds().isEmpty()); @@ -356,14 +428,12 @@ private IntermediateResult evalCall(ExecutionFrame frame, CelExpr expr, CelCall return evalLogicalAnd(frame, callExpr); case "logical_or": return evalLogicalOr(frame, callExpr); - case "not_strictly_false": - return evalNotStrictlyFalse(frame, callExpr); 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()) { @@ -376,8 +446,18 @@ private IntermediateResult evalCall(ExecutionFrame frame, CelExpr expr, CelCall break; } - // Delegate handling of call to dispatcher. + boolean isNonStrictCall = + dispatcher.findSingleNonStrictOverload(reference.overloadIds()).isPresent(); + return dispatchCall(frame, expr, callExpr, reference, isNonStrictCall); + } + private IntermediateResult dispatchCall( + ExecutionFrame frame, + CelExpr expr, + CelCall callExpr, + CelReference reference, + boolean isNonStrict) + throws CelEvaluationException { List callArgs = new ArrayList<>(); callExpr.target().ifPresent(callArgs::add); @@ -385,13 +465,16 @@ private IntermediateResult evalCall(ExecutionFrame frame, CelExpr expr, CelCall IntermediateResult[] argResults = new IntermediateResult[callArgs.size()]; for (int i = 0; i < argResults.length; i++) { - // Default evaluation is strict so errors will propagate (via thrown Java exception) before - // unknowns. - argResults[i] = evalInternal(frame, callArgs.get(i)); - // TODO: remove support for IncompleteData after migrating users to attribute - // tracking unknowns. - InterpreterUtil.completeDataOnly( - argResults[i].value(), "Incomplete data does not support function calls."); + IntermediateResult result; + try { + result = evalInternal(frame, callArgs.get(i)); + } catch (Exception e) { + if (!isNonStrict) { + throw e; + } + result = IntermediateResult.create(e); + } + argResults[i] = result; } Optional indexAttr = @@ -408,7 +491,7 @@ private IntermediateResult evalCall(ExecutionFrame frame, CelExpr expr, CelCall indexAttr.isPresent() ? CallArgumentChecker.createAcceptingPartial(frame.getResolver()) : CallArgumentChecker.create(frame.getResolver()); - for (DefaultInterpreter.IntermediateResult element : argResults) { + for (IntermediateResult element : argResults) { argChecker.checkArg(element); } Optional unknowns = argChecker.maybeUnknowns(); @@ -417,11 +500,56 @@ private IntermediateResult evalCall(ExecutionFrame frame, CelExpr expr, CelCall } Object[] argArray = Arrays.stream(argResults).map(IntermediateResult::value).toArray(); + ImmutableList overloadIds = reference.overloadIds(); + CelResolvedOverload overload = + findOverloadOrThrow(frame, expr, callExpr.function(), overloadIds, argArray); + try { + Object dispatchResult = overload.getDefinition().apply(argArray); + // CustomFunctions themselves can return a CelUnknownSet directly. + dispatchResult = InterpreterUtil.maybeAdaptToAccumulatedUnknowns(dispatchResult); + if (celOptions.unwrapWellKnownTypesOnFunctionDispatch()) { + CelType checkedType = getCheckedTypeOrThrow(expr); + dispatchResult = typeProvider.adapt(checkedType.name(), dispatchResult); + } + return IntermediateResult.create(attr, dispatchResult); + } catch (CelRuntimeException ce) { + throw CelEvaluationExceptionBuilder.newBuilder(ce).setMetadata(metadata, expr.id()).build(); + } catch (RuntimeException e) { + throw CelEvaluationExceptionBuilder.newBuilder( + "Function '%s' failed with arg(s) '%s'", + overload.getOverloadId(), Joiner.on(", ").join(argArray)) + .setMetadata(metadata, expr.id()) + .setCause(e) + .build(); + } + } - return IntermediateResult.create( - attr, - dispatcher.dispatch( - metadata, expr.id(), callExpr.function(), reference.overloadIds(), argArray)); + private CelResolvedOverload findOverloadOrThrow( + ExecutionFrame frame, + CelExpr expr, + String functionName, + List overloadIds, + Object[] args) + throws CelEvaluationException { + try { + Optional funcImpl = + dispatcher.findOverloadMatchingArgs(functionName, overloadIds, args); + if (funcImpl.isPresent()) { + return funcImpl.get(); + } + return frame + .findOverload(functionName, overloadIds, args) + .orElseThrow( + () -> + CelEvaluationExceptionBuilder.newBuilder( + "No matching overload for function '%s'. Overload candidates: %s", + functionName, Joiner.on(",").join(overloadIds)) + .setErrorCode(CelErrorCode.OVERLOAD_NOT_FOUND) + .setMetadata(metadata, expr.id()) + .build()); + } catch (CelRuntimeException e) { + throw CelEvaluationExceptionBuilder.newBuilder(e).setMetadata(metadata, expr.id()).build(); + } } private Optional maybeContainerIndexAttribute( @@ -453,142 +581,176 @@ private Optional maybeContainerIndexAttribute( } private IntermediateResult evalConditional(ExecutionFrame frame, CelCall callExpr) - throws InterpreterException { + throws CelEvaluationException { IntermediateResult condition = evalBooleanStrict(frame, callExpr.args().get(0)); - if (isUnknownValue(condition.value())) { - return condition; - } - if ((boolean) condition.value()) { - return evalInternal(frame, callExpr.args().get(1)); + if (celOptions.enableShortCircuiting()) { + if (isUnknownValue(condition.value())) { + return condition; + } + if ((boolean) condition.value()) { + return evalInternal(frame, callExpr.args().get(1)); + } + return evalInternal(frame, callExpr.args().get(2)); + } else { + IntermediateResult lhs = evalNonstrictly(frame, callExpr.args().get(1)); + IntermediateResult rhs = evalNonstrictly(frame, callExpr.args().get(2)); + if (isUnknownValue(condition.value())) { + return condition; + } + Object result = + InterpreterUtil.strict((boolean) condition.value() ? lhs.value() : rhs.value()); + return IntermediateResult.create(result); } - return evalInternal(frame, callExpr.args().get(2)); } private IntermediateResult mergeBooleanUnknowns(IntermediateResult lhs, IntermediateResult rhs) - throws InterpreterException { + throws CelEvaluationException { // TODO: migrate clients to a common type that reports both expr-id unknowns // and attribute sets. - if (lhs.value() instanceof CelUnknownSet && rhs.value() instanceof CelUnknownSet) { + Object lhsVal = lhs.value(); + Object rhsVal = rhs.value(); + if (lhsVal instanceof AccumulatedUnknowns && rhsVal instanceof AccumulatedUnknowns) { return IntermediateResult.create( - ((CelUnknownSet) lhs.value()).merge((CelUnknownSet) rhs.value())); - } else if (lhs.value() instanceof CelUnknownSet) { + ((AccumulatedUnknowns) lhsVal).merge((AccumulatedUnknowns) rhsVal)); + } else if (lhsVal instanceof AccumulatedUnknowns) { return lhs; - } else if (rhs.value() instanceof CelUnknownSet) { + } else if (rhsVal instanceof AccumulatedUnknowns) { return rhs; } - // Otherwise fallback to normal impl - return IntermediateResult.create( - InterpreterUtil.shortcircuitUnknownOrThrowable(lhs.value(), rhs.value())); + // Otherwise, enforce strictness on both args + return IntermediateResult.create(InterpreterUtil.enforceStrictness(lhsVal, rhsVal)); } - private IntermediateResult evalLogicalOr(ExecutionFrame frame, CelCall callExpr) - throws InterpreterException { - IntermediateResult left = evalBooleanNonstrict(frame, callExpr.args().get(0)); - if (left.value() instanceof Boolean && (Boolean) left.value()) { - return left; - } + private enum ShortCircuitableOperators { + LOGICAL_OR, + LOGICAL_AND + } - IntermediateResult right = evalBooleanNonstrict(frame, callExpr.args().get(1)); - if (right.value() instanceof Boolean && (Boolean) right.value()) { - return right; + private boolean canShortCircuit(IntermediateResult result, ShortCircuitableOperators operator) { + if (!(result.value() instanceof Boolean)) { + return false; } - // both false. - if (right.value() instanceof Boolean && left.value() instanceof Boolean) { - return left; + Boolean value = (Boolean) result.value(); + if (value && operator.equals(ShortCircuitableOperators.LOGICAL_OR)) { + return true; } - return mergeBooleanUnknowns(left, right); + return !value && operator.equals(ShortCircuitableOperators.LOGICAL_AND); } - private IntermediateResult evalLogicalAnd(ExecutionFrame frame, CelCall callExpr) - throws InterpreterException { - IntermediateResult left = evalBooleanNonstrict(frame, callExpr.args().get(0)); - if (left.value() instanceof Boolean && !((Boolean) left.value())) { - return left; - } + private IntermediateResult evalLogicalOr(ExecutionFrame frame, CelCall callExpr) + throws CelEvaluationException { + IntermediateResult left; + IntermediateResult right; + if (celOptions.enableShortCircuiting()) { + left = evalBooleanNonstrict(frame, callExpr.args().get(0)); + if (canShortCircuit(left, ShortCircuitableOperators.LOGICAL_OR)) { + return left; + } - IntermediateResult right = evalBooleanNonstrict(frame, callExpr.args().get(1)); - if (right.value() instanceof Boolean && !((Boolean) right.value())) { - return right; + right = evalBooleanNonstrict(frame, callExpr.args().get(1)); + if (canShortCircuit(right, ShortCircuitableOperators.LOGICAL_OR)) { + return right; + } + } else { + left = evalBooleanNonstrict(frame, callExpr.args().get(0)); + right = evalBooleanNonstrict(frame, callExpr.args().get(1)); + if (canShortCircuit(left, ShortCircuitableOperators.LOGICAL_OR)) { + return left; + } + if (canShortCircuit(right, ShortCircuitableOperators.LOGICAL_OR)) { + return right; + } } - // both true. + // both are booleans. if (right.value() instanceof Boolean && left.value() instanceof Boolean) { - return left; + return IntermediateResult.create((Boolean) right.value() || (Boolean) left.value()); } return mergeBooleanUnknowns(left, right); } - // Returns true unless the expression evaluates to false, in which case it returns false. - // True is also returned if evaluation yields an error or an unknown set. - private IntermediateResult evalNotStrictlyFalse(ExecutionFrame frame, CelCall callExpr) { - try { - IntermediateResult value = evalBooleanStrict(frame, callExpr.args().get(0)); - if (value.value() instanceof Boolean) { - return value; + private IntermediateResult evalLogicalAnd(ExecutionFrame frame, CelCall callExpr) + throws CelEvaluationException { + IntermediateResult left; + IntermediateResult right; + if (celOptions.enableShortCircuiting()) { + left = evalBooleanNonstrict(frame, callExpr.args().get(0)); + if (canShortCircuit(left, ShortCircuitableOperators.LOGICAL_AND)) { + return left; + } + + right = evalBooleanNonstrict(frame, callExpr.args().get(1)); + if (canShortCircuit(right, ShortCircuitableOperators.LOGICAL_AND)) { + return right; + } + } else { + left = evalBooleanNonstrict(frame, callExpr.args().get(0)); + right = evalBooleanNonstrict(frame, callExpr.args().get(1)); + if (canShortCircuit(left, ShortCircuitableOperators.LOGICAL_AND)) { + return left; + } + if (canShortCircuit(right, ShortCircuitableOperators.LOGICAL_AND)) { + return right; } - } catch (Exception e) { - /*nothing to do*/ } - return IntermediateResult.create(true); + + // both are booleans. + if (right.value() instanceof Boolean && left.value() instanceof Boolean) { + return IntermediateResult.create((Boolean) right.value() && (Boolean) left.value()); + } + + return mergeBooleanUnknowns(left, right); } private IntermediateResult evalType(ExecutionFrame frame, CelCall callExpr) - throws InterpreterException { + throws CelEvaluationException { CelExpr typeExprArg = callExpr.args().get(0); IntermediateResult argResult = evalInternal(frame, typeExprArg); - - CelType checkedType = - ast.getType(typeExprArg.id()) - .orElseThrow( - () -> - new InterpreterException.Builder( - "expected a runtime type for '%s' from checked expression, but found" - + " none.", - argResult.getClass().getSimpleName()) - .setErrorCode(CelErrorCode.TYPE_NOT_FOUND) - .setLocation(metadata, typeExprArg.id()) - .build()); - - Value checkedTypeValue = typeProvider.adaptType(checkedType); - Object typeValue = typeProvider.resolveObjectType(argResult.value(), checkedTypeValue); - return IntermediateResult.create(typeValue); - } - - private IntermediateResult evalOptionalOr(ExecutionFrame frame, CelCall callExpr) - throws InterpreterException { - CelExpr lhsExpr = callExpr.target().get(); - IntermediateResult lhsResult = evalInternal(frame, lhsExpr); - if (!(lhsResult.value() instanceof Optional)) { - throw new InterpreterException.Builder( - "expected optional value, found: %s", lhsResult.value()) - .setErrorCode(CelErrorCode.INVALID_ARGUMENT) - .setLocation(metadata, lhsExpr.id()) - .build(); + // Type is a strict function. Early return if the argument is an error or an unknown. + if (isUnknownOrError(argResult.value())) { + return argResult; } - Optional lhsOptionalValue = (Optional) lhsResult.value(); + CelType checkedType = getCheckedTypeOrThrow(typeExprArg); + CelType checkedTypeValue = typeResolver.adaptType(checkedType); + return IntermediateResult.create( + typeResolver.resolveObjectType(argResult.value(), checkedTypeValue)); + } - if (lhsOptionalValue.isPresent()) { - // Short-circuit lhs if a value exists - return lhsResult; - } + private IntermediateResult evalOptionalOr(ExecutionFrame frame, CelExpr expr) + throws CelEvaluationException { + return evalOptionalOrInternal(frame, expr, /* unwrapOptional= */ false); + } - return evalInternal(frame, callExpr.args().get(0)); + private IntermediateResult evalOptionalOrValue(ExecutionFrame frame, CelExpr expr) + throws CelEvaluationException { + return evalOptionalOrInternal(frame, expr, /* unwrapOptional= */ true); } - private IntermediateResult evalOptionalOrValue(ExecutionFrame frame, CelCall callExpr) - throws InterpreterException { - CelExpr lhsExpr = callExpr.target().get(); + private IntermediateResult evalOptionalOrInternal( + ExecutionFrame frame, CelExpr expr, boolean unwrapOptional) throws CelEvaluationException { + CelCall callExpr = expr.call(); + CelExpr lhsExpr = + callExpr + .target() + .orElseThrow( + () -> new IllegalStateException("Missing target for chained optional function")); IntermediateResult lhsResult = evalInternal(frame, lhsExpr); + + if (isUnknownValue(lhsResult.value())) { + return lhsResult; + } + if (!(lhsResult.value() instanceof Optional)) { - throw new InterpreterException.Builder( - "expected optional value, found: %s", lhsResult.value()) + String functionName = unwrapOptional ? "orValue" : "or"; + throw CelEvaluationExceptionBuilder.newBuilder( + "No matching overload for function '%s'.", functionName) .setErrorCode(CelErrorCode.INVALID_ARGUMENT) - .setLocation(metadata, lhsExpr.id()) + .setMetadata(metadata, expr.id()) .build(); } @@ -596,14 +758,14 @@ private IntermediateResult evalOptionalOrValue(ExecutionFrame frame, CelCall cal if (lhsOptionalValue.isPresent()) { // Short-circuit lhs if a value exists - return IntermediateResult.create(lhsOptionalValue.get()); + return unwrapOptional ? IntermediateResult.create(lhsOptionalValue.get()) : lhsResult; } return evalInternal(frame, callExpr.args().get(0)); } private Optional maybeEvalOptionalSelectField( - ExecutionFrame frame, CelExpr expr, CelCall callExpr) throws InterpreterException { + ExecutionFrame frame, CelExpr expr, CelCall callExpr) throws CelEvaluationException { CelExpr operand = callExpr.args().get(0); IntermediateResult lhsResult = evalInternal(frame, operand); if ((lhsResult.value() instanceof Map)) { @@ -621,20 +783,23 @@ 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) - throws InterpreterException { + throws CelEvaluationException { IntermediateResult value = strict ? evalInternal(frame, expr) : evalNonstrictly(frame, expr); - if (!(value.value() instanceof Boolean) - && !isUnknownValue(value.value()) - && !(value.value() instanceof Exception)) { - throw new InterpreterException.Builder("expected boolean value, found: %s", value.value()) + if (!(value.value() instanceof Boolean) && !isUnknownOrError(value.value())) { + throw CelEvaluationExceptionBuilder.newBuilder( + "expected boolean value, found: %s", value.value()) .setErrorCode(CelErrorCode.INVALID_ARGUMENT) - .setLocation(metadata, expr.id()) + .setMetadata(metadata, expr.id()) .build(); } @@ -642,22 +807,20 @@ private IntermediateResult evalBoolean(ExecutionFrame frame, CelExpr expr, boole } private IntermediateResult evalBooleanStrict(ExecutionFrame frame, CelExpr expr) - throws InterpreterException { + throws CelEvaluationException { return evalBoolean(frame, expr, /* strict= */ true); } // Evaluate a non-strict boolean sub expression. - // Behaves the same as non-strict eval, but throws an InterpreterException if the result + // Behaves the same as non-strict eval, but throws a CelEvaluationException if the result // doesn't support CELs short-circuiting behavior (not an error, unknown or boolean). private IntermediateResult evalBooleanNonstrict(ExecutionFrame frame, CelExpr expr) - throws InterpreterException { + throws CelEvaluationException { return evalBoolean(frame, expr, /* strict= */ false); } - private IntermediateResult evalList( - ExecutionFrame frame, CelExpr unusedExpr, CelCreateList listExpr) - throws InterpreterException { - + private IntermediateResult evalList(ExecutionFrame frame, CelExpr unusedExpr, CelList listExpr) + throws CelEvaluationException { CallArgumentChecker argChecker = CallArgumentChecker.create(frame.getResolver()); List result = new ArrayList<>(listExpr.elements().size()); @@ -666,13 +829,19 @@ private IntermediateResult evalList( for (int i = 0; i < elements.size(); i++) { CelExpr element = elements.get(i); IntermediateResult evaluatedElement = evalInternal(frame, element); - // TODO: remove support for IncompleteData. - InterpreterUtil.completeDataOnly( - evaluatedElement.value(), "Incomplete data cannot be an elem of a list."); argChecker.checkArg(evaluatedElement); Object value = evaluatedElement.value(); - if (optionalIndicesSet.contains(i)) { + if (!optionalIndicesSet + .isEmpty() // Performance optimization to prevent autoboxing when there's no + // 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; @@ -687,32 +856,36 @@ private IntermediateResult evalList( return IntermediateResult.create(argChecker.maybeUnknowns().orElse(result)); } - private IntermediateResult evalMap(ExecutionFrame frame, CelCreateMap mapExpr) - throws InterpreterException { + private IntermediateResult evalMap(ExecutionFrame frame, CelMap mapExpr) + throws CelEvaluationException { CallArgumentChecker argChecker = CallArgumentChecker.create(frame.getResolver()); Map result = new LinkedHashMap<>(); - for (CelCreateMap.Entry entry : mapExpr.entries()) { + for (CelMap.Entry entry : mapExpr.entries()) { IntermediateResult keyResult = evalInternal(frame, entry.key()); argChecker.checkArg(keyResult); IntermediateResult valueResult = evalInternal(frame, entry.value()); - // TODO: remove support for IncompleteData. - InterpreterUtil.completeDataOnly( - valueResult.value(), "Incomplete data cannot be a value of a map."); argChecker.checkArg(valueResult); if (celOptions.errorOnDuplicateMapKeys() && result.containsKey(keyResult.value())) { - throw new InterpreterException.Builder("duplicate map key [%s]", keyResult.value()) + throw CelEvaluationExceptionBuilder.newBuilder( + "duplicate map key [%s]", keyResult.value()) .setErrorCode(CelErrorCode.DUPLICATE_ATTRIBUTE) - .setLocation(metadata, entry.id()) + .setMetadata(metadata, entry.key().id()) .build(); } Object value = valueResult.value(); - if (entry.optionalEntry()) { + 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 @@ -729,29 +902,32 @@ private IntermediateResult evalMap(ExecutionFrame frame, CelCreateMap mapExpr) return IntermediateResult.create(argChecker.maybeUnknowns().orElse(result)); } - private IntermediateResult evalStruct( - ExecutionFrame frame, CelExpr expr, CelCreateStruct structExpr) - throws InterpreterException { + private IntermediateResult evalStruct(ExecutionFrame frame, CelExpr expr, CelStruct structExpr) + throws CelEvaluationException { CelReference reference = ast.getReference(expr.id()) .orElseThrow( () -> new IllegalStateException( - "Could not find a reference for CelCreateStruct expresison at ID: " + "Could not find a reference for CelStruct expression at ID: " + expr.id())); // Message creation. CallArgumentChecker argChecker = CallArgumentChecker.create(frame.getResolver()); Map fields = new HashMap<>(); - for (CelCreateStruct.Entry entry : structExpr.entries()) { + for (CelStruct.Entry entry : structExpr.entries()) { IntermediateResult fieldResult = evalInternal(frame, entry.value()); - // TODO: remove support for IncompleteData - InterpreterUtil.completeDataOnly( - fieldResult.value(), "Incomplete data cannot be a field of a message."); argChecker.checkArg(fieldResult); 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 @@ -786,10 +962,36 @@ private IntermediateResult evalNonstrictly(ExecutionFrame frame, CelExpr expr) { } } + @SuppressWarnings("unchecked") // All type-erased elements are object compatible + private IntermediateResult maybeAdaptToListView(IntermediateResult accuValue) { + // ListView uses a mutable reference internally. Macros such as `filter` uses conditionals + // under the hood. In situations where short circuiting is disabled, we don't want to evaluate + // both LHS and RHS, as evaluating LHS can mutate the accu value, which also affects RHS. + if (!(accuValue.value() instanceof List) || !celOptions.enableShortCircuiting()) { + return accuValue; + } + + ConcatenatedListView lv = + new ConcatenatedListView<>((List) accuValue.value()); + return IntermediateResult.create(lv); + } + + @SuppressWarnings("unchecked") // All type-erased elements are object compatible + private IntermediateResult maybeAdaptViewToList(IntermediateResult accuValue) { + if ((accuValue.value() instanceof ConcatenatedListView)) { + // Materialize view back into a list to facilitate O(1) lookups. + List copiedList = new ArrayList<>((List) accuValue.value()); + + accuValue = IntermediateResult.create(copiedList); + } + + return accuValue; + } + @SuppressWarnings("unchecked") private IntermediateResult evalComprehension( - ExecutionFrame frame, CelExpr unusedExpr, CelComprehension compre) - throws InterpreterException { + ExecutionFrame frame, CelExpr compreExpr, CelComprehension compre) + throws CelEvaluationException { String accuVar = compre.accuVar(); String iterVar = compre.iterVar(); IntermediateResult iterRangeRaw = evalInternal(frame, compre.iterRange()); @@ -802,37 +1004,57 @@ private IntermediateResult evalComprehension( } else if (iterRangeRaw.value() instanceof Map) { iterRange = ((Map) iterRangeRaw.value()).keySet(); } else { - throw new InterpreterException.Builder( + throw CelEvaluationExceptionBuilder.newBuilder( "expected a list or a map for iteration range but got '%s'", iterRangeRaw.value().getClass().getSimpleName()) .setErrorCode(CelErrorCode.INVALID_ARGUMENT) - .setLocation(metadata, compre.iterRange().id()) + .setMetadata(metadata, compre.iterRange().id()) .build(); } IntermediateResult accuValue; - if (LazyExpression.isLazilyEvaluable(compre)) { + boolean isLazilyEvaluable = LazyExpression.isLazilyEvaluable(compre); + if (isLazilyEvaluable) { accuValue = IntermediateResult.create(new LazyExpression(compre.accuInit())); } else { accuValue = evalNonstrictly(frame, compre.accuInit()); + // This ensures macros such as filter/map that uses "add_list" functions under the hood + // remain linear in time complexity + accuValue = maybeAdaptToListView(accuValue); } int i = 0; for (Object elem : iterRange) { - frame.incrementIterations(); + frame.incrementIterations(metadata, compreExpr.id()); CelAttribute iterAttr = CelAttribute.EMPTY; if (iterRange instanceof List) { iterAttr = iterRangeRaw.attribute().qualify(CelAttribute.Qualifier.ofInt(i)); } - i++; - ImmutableMap loopVars = - ImmutableMap.of( - iterVar, - IntermediateResult.create(iterAttr, RuntimeHelpers.maybeAdaptPrimitive(elem)), - accuVar, - accuValue); + Map loopVars = new HashMap<>(); + if (!Strings.isNullOrEmpty(compre.iterVar2())) { + String iterVar2 = compre.iterVar2(); + if (iterRangeRaw.value() instanceof List) { + loopVars.put(iterVar, IntermediateResult.create((long) i)); + loopVars.put( + iterVar2, + IntermediateResult.create(iterAttr, RuntimeHelpers.maybeAdaptPrimitive(elem))); + } else if (iterRangeRaw.value() instanceof Map) { + Object key = elem; + Object value = ((Map) iterRangeRaw.value()).get(key); + loopVars.put( + iterVar, IntermediateResult.create(RuntimeHelpers.maybeAdaptPrimitive(key))); + loopVars.put( + iterVar2, IntermediateResult.create(RuntimeHelpers.maybeAdaptPrimitive(value))); + } + } else { + loopVars.put( + iterVar, + IntermediateResult.create(iterAttr, RuntimeHelpers.maybeAdaptPrimitive(elem))); + } + loopVars.put(accuVar, accuValue); + i++; - frame.pushScope(loopVars); + frame.pushScope(Collections.unmodifiableMap(loopVars)); IntermediateResult evalObject = evalBooleanStrict(frame, compre.loopCondition()); if (!isUnknownValue(evalObject.value()) && !(boolean) evalObject.value()) { frame.popScope(); @@ -842,25 +1064,56 @@ private IntermediateResult evalComprehension( frame.popScope(); } - frame.pushScope(ImmutableMap.of(accuVar, accuValue)); - IntermediateResult result = evalInternal(frame, compre.result()); - frame.popScope(); + accuValue = maybeAdaptViewToList(accuValue); + + Map scopedAttributes = + Collections.singletonMap(accuVar, accuValue); + if (isLazilyEvaluable) { + frame.pushLazyScope(scopedAttributes); + } else { + frame.pushScope(scopedAttributes); + } + IntermediateResult result; + try { + result = evalInternal(frame, compre.result()); + } finally { + frame.popScope(); + } return result; } private IntermediateResult evalCelBlock( - ExecutionFrame frame, CelExpr unusedExpr, CelCall blockCall) throws InterpreterException { - CelCreateList exprList = blockCall.args().get(0).createList(); - ImmutableMap.Builder blockList = ImmutableMap.builder(); + ExecutionFrame frame, CelExpr unusedExpr, CelCall blockCall) throws CelEvaluationException { + CelList exprList = blockCall.args().get(0).list(); + Map blockList = new HashMap<>(); for (int index = 0; index < exprList.elements().size(); index++) { // Register the block indices as lazily evaluated expressions stored as unique identifiers. + String indexKey = "@index" + index; blockList.put( - "@index" + index, + indexKey, IntermediateResult.create(new LazyExpression(exprList.elements().get(index)))); } - frame.pushScope(blockList.buildOrThrow()); + frame.setRequireCycleCheck(true); + frame.pushLazyScope(Collections.unmodifiableMap(blockList)); + + try { + return evalInternal(frame, blockCall.args().get(1)); + } finally { + frame.popScope(); + } + } - return evalInternal(frame, blockCall.args().get(1)); + private CelType getCheckedTypeOrThrow(CelExpr expr) throws CelEvaluationException { + return ast.getType(expr.id()) + .orElseThrow( + () -> + CelEvaluationExceptionBuilder.newBuilder( + "expected a runtime type for expression ID '%d' from checked expression," + + " but found none.", + expr.id()) + .setErrorCode(CelErrorCode.TYPE_NOT_FOUND) + .setMetadata(metadata, expr.id()) + .build()); } } @@ -882,8 +1135,8 @@ private static boolean isLazilyEvaluable(CelComprehension comprehension) { .equals(CelConstant.Kind.BOOLEAN_VALUE) && !comprehension.loopCondition().constant().booleanValue() && comprehension.iterVar().equals("#unused") - && comprehension.iterRange().exprKind().getKind().equals(ExprKind.Kind.CREATE_LIST) - && comprehension.iterRange().createList().elements().isEmpty(); + && comprehension.iterRange().exprKind().getKind().equals(ExprKind.Kind.LIST) + && comprehension.iterRange().list().elements().isEmpty(); } private LazyExpression(CelExpr celExpr) { @@ -892,25 +1145,50 @@ private LazyExpression(CelExpr celExpr) { } /** This class tracks the state meaningful to a single evaluation pass. */ - private static class ExecutionFrame { - private final CelEvaluationListener evaluationListener; + static class ExecutionFrame { + private final Optional evaluationListener; private final int maxIterations; private final ArrayDeque resolvers; + private final Optional lateBoundFunctionResolver; + private final Set activeLazyAttributes = new HashSet<>(); private RuntimeUnknownResolver currentResolver; private int iterations; + private boolean requireCycleCheck; + @VisibleForTesting int scopeLevel; private ExecutionFrame( - CelEvaluationListener evaluationListener, + Optional evaluationListener, RuntimeUnknownResolver resolver, + Optional lateBoundFunctionResolver, int maxIterations) { this.evaluationListener = evaluationListener; this.resolvers = new ArrayDeque<>(); this.resolvers.add(resolver); + this.lateBoundFunctionResolver = lateBoundFunctionResolver; this.currentResolver = resolver; this.maxIterations = maxIterations; } - private CelEvaluationListener getEvaluationListener() { + private void markLazyEvaluationOrThrow(String name) { + if (!requireCycleCheck) { + return; + } + + boolean added = activeLazyAttributes.add(name); + if (!added) { + throw new IllegalStateException(String.format("Cycle detected: %s", name)); + } + } + + private void endLazyEvaluation(String name) { + if (!requireCycleCheck) { + return; + } + + activeLazyAttributes.remove(name); + } + + private Optional getEvaluationListener() { return evaluationListener; } @@ -918,13 +1196,24 @@ private RuntimeUnknownResolver getResolver() { return currentResolver; } - private void incrementIterations() throws InterpreterException { + private Optional findOverload( + String function, List overloadIds, Object[] args) throws CelEvaluationException { + if (lateBoundFunctionResolver.isPresent()) { + return lateBoundFunctionResolver + .get() + .findOverloadMatchingArgs(function, overloadIds, args); + } + return Optional.empty(); + } + + private void incrementIterations(Metadata metadata, long exprId) throws CelEvaluationException { if (maxIterations < 0) { return; } if (++iterations > maxIterations) { - throw new InterpreterException.Builder( + throw CelEvaluationExceptionBuilder.newBuilder( String.format("Iteration budget exceeded: %d", maxIterations)) + .setMetadata(metadata, exprId) .setErrorCode(CelErrorCode.ITERATION_BUDGET_EXCEEDED) .build(); } @@ -943,13 +1232,31 @@ private void cacheLazilyEvaluatedResult( currentResolver.cacheLazilyEvaluatedResult(name, result); } - private void pushScope(ImmutableMap scope) { + /** + * If set, interpreter will check for potential cycles for lazily evaluable attributes. This + * only applies for cel.@block indices. + */ + private void setRequireCycleCheck(boolean requireCycleCheck) { + this.requireCycleCheck = requireCycleCheck; + } + + private void pushLazyScope(Map scope) { + pushScope(scope); + for (String lazyAttribute : scope.keySet()) { + currentResolver.declareLazyAttribute(lazyAttribute); + } + } + + /** Note: we utilize a HashMap instead of ImmutableMap to make lookups faster on string keys. */ + private void pushScope(Map scope) { + scopeLevel++; RuntimeUnknownResolver scopedResolver = currentResolver.withScope(scope); currentResolver = scopedResolver; resolvers.addLast(scopedResolver); } private void popScope() { + scopeLevel--; if (resolvers.isEmpty()) { throw new IllegalStateException("Execution frame error: more scopes popped than pushed"); } diff --git a/runtime/src/main/java/dev/cel/runtime/DefaultMetadata.java b/runtime/src/main/java/dev/cel/runtime/DefaultMetadata.java index 65f719817..98aeb9c2a 100644 --- a/runtime/src/main/java/dev/cel/runtime/DefaultMetadata.java +++ b/runtime/src/main/java/dev/cel/runtime/DefaultMetadata.java @@ -14,16 +14,14 @@ package dev.cel.runtime; -import dev.cel.expr.CheckedExpr; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; import com.google.errorprone.annotations.Immutable; import dev.cel.common.CelAbstractSyntaxTree; -import dev.cel.common.CelProtoAbstractSyntaxTree; import dev.cel.common.annotations.Internal; /** - * Metadata implementation based on {@link CheckedExpr}. + * Metadata implementation based on {@link CelAbstractSyntaxTree}. * *

CEL Library Internals. Do Not Use. */ @@ -37,11 +35,6 @@ public DefaultMetadata(CelAbstractSyntaxTree ast) { this.ast = Preconditions.checkNotNull(ast); } - @Deprecated - public DefaultMetadata(CheckedExpr checkedExpr) { - this(CelProtoAbstractSyntaxTree.fromCheckedExpr(checkedExpr).getAst()); - } - @Override public String getLocation() { return ast.getSource().getDescription(); @@ -50,6 +43,11 @@ public String getLocation() { @Override public int getPosition(long exprId) { ImmutableMap positions = ast.getSource().getPositionsMap(); - return positions.containsKey(exprId) ? positions.get(exprId) : 0; + return positions.getOrDefault(exprId, 0); + } + + @Override + public boolean hasPosition(long exprId) { + return ast.getSource().getPositionsMap().containsKey(exprId); } } diff --git a/runtime/src/main/java/dev/cel/runtime/DescriptorMessageProvider.java b/runtime/src/main/java/dev/cel/runtime/DescriptorMessageProvider.java index e6782a206..ecbba5e7e 100644 --- a/runtime/src/main/java/dev/cel/runtime/DescriptorMessageProvider.java +++ b/runtime/src/main/java/dev/cel/runtime/DescriptorMessageProvider.java @@ -14,28 +14,24 @@ package dev.cel.runtime; -import dev.cel.expr.Type; -import dev.cel.expr.Value; -import com.google.common.collect.ImmutableSet; import com.google.errorprone.annotations.Immutable; +import com.google.protobuf.ByteString; import com.google.protobuf.Descriptors.Descriptor; import com.google.protobuf.Descriptors.FieldDescriptor; import com.google.protobuf.Message; import com.google.protobuf.MessageOrBuilder; import com.google.protobuf.NullValue; -import dev.cel.common.CelErrorCode; import dev.cel.common.CelOptions; -import dev.cel.common.CelRuntimeException; -import dev.cel.common.ExprFeatures; import dev.cel.common.annotations.Internal; +import dev.cel.common.exceptions.CelAttributeNotFoundException; import dev.cel.common.internal.DynamicProto; import dev.cel.common.internal.ProtoAdapter; import dev.cel.common.internal.ProtoMessageFactory; -import dev.cel.common.types.CelType; import dev.cel.common.types.CelTypes; +import dev.cel.common.values.CelByteString; import java.util.Map; import java.util.Optional; -import org.jspecify.nullness.Nullable; +import org.jspecify.annotations.Nullable; /** * An implementation of {@link RuntimeTypeProvider} which relies on proto descriptors. @@ -50,7 +46,7 @@ @Internal public final class DescriptorMessageProvider implements RuntimeTypeProvider { private final ProtoMessageFactory protoMessageFactory; - private final TypeResolver typeResolver; + private final CelOptions celOptions; @SuppressWarnings("Immutable") private final ProtoAdapter protoAdapter; @@ -65,62 +61,25 @@ public DescriptorMessageProvider(MessageFactory messageFactory) { this(messageFactory.toProtoMessageFactory(), CelOptions.LEGACY); } - /** - * Creates a new message provider with the given message factory and a set of customized {@code - * features}. - * - * @deprecated Migrate to the CEL-Java fluent APIs. See {@code CelRuntimeFactory}. - */ - @Deprecated - public DescriptorMessageProvider( - MessageFactory messageFactory, ImmutableSet features) { - this(messageFactory.toProtoMessageFactory(), CelOptions.fromExprFeatures(features)); - } - /** * Create a new message provider with a given message factory and custom descriptor set to use * when adapting from proto to CEL and vice versa. */ public DescriptorMessageProvider(ProtoMessageFactory protoMessageFactory, CelOptions celOptions) { - this.typeResolver = StandardTypeResolver.getInstance(celOptions); this.protoMessageFactory = protoMessageFactory; - this.protoAdapter = - new ProtoAdapter( - DynamicProto.create(protoMessageFactory), celOptions.enableUnsignedLongs()); - } - - @Override - @Nullable - public Value resolveObjectType(Object obj, @Nullable Value checkedTypeValue) { - return typeResolver.resolveObjectType(obj, checkedTypeValue); - } - - /** {@inheritDoc} */ - @Override - public Value adaptType(CelType type) { - return typeResolver.adaptType(type); - } - - @Nullable - @Override - @Deprecated - /** {@inheritDoc} */ - public Value adaptType(@Nullable Type type) { - return typeResolver.adaptType(type); + this.celOptions = celOptions; + this.protoAdapter = new ProtoAdapter(DynamicProto.create(protoMessageFactory), celOptions); } - @Nullable @Override - public Object createMessage(String messageName, Map values) { + public @Nullable Object createMessage(String messageName, Map values) { Message.Builder builder = protoMessageFactory .newBuilder(messageName) .orElseThrow( () -> - new CelRuntimeException( - new IllegalArgumentException( - String.format("cannot resolve '%s' as a message", messageName)), - CelErrorCode.ATTRIBUTE_NOT_FOUND)); + CelAttributeNotFoundException.of( + String.format("cannot resolve '%s' as a message", messageName))); try { Descriptor descriptor = builder.getDescriptorForType(); @@ -137,37 +96,39 @@ public Object createMessage(String messageName, Map values) { } @Override - @Nullable @SuppressWarnings("unchecked") - public Object selectField(Object message, String fieldName) { + public @Nullable Object selectField(Object message, String fieldName) { + boolean isOptionalMessage = false; if (message instanceof Optional) { - Optional> optionalMap = (Optional>) message; - if (!optionalMap.isPresent()) { - return Optional.empty(); - } - - Map unwrappedMap = optionalMap.get(); - if (!unwrappedMap.containsKey(fieldName)) { + isOptionalMessage = true; + Optional optionalMessage = (Optional) message; + if (!optionalMessage.isPresent()) { return Optional.empty(); } - return Optional.of(unwrappedMap.get(fieldName)); + message = optionalMessage.get(); } if (message instanceof Map) { Map map = (Map) message; if (map.containsKey(fieldName)) { - return map.get(fieldName); + Object mapValue = map.get(fieldName); + return isOptionalMessage ? Optional.of(mapValue) : mapValue; + } + + if (isOptionalMessage) { + return Optional.empty(); + } else { + throw CelAttributeNotFoundException.forMissingMapKey(fieldName); } - throw new CelRuntimeException( - new IllegalArgumentException(String.format("key '%s' is not present in map.", fieldName)), - CelErrorCode.ATTRIBUTE_NOT_FOUND); } - MessageOrBuilder typedMessage = assertFullProtoMessage(message); + MessageOrBuilder typedMessage = assertFullProtoMessage(message, fieldName); FieldDescriptor fieldDescriptor = findField(typedMessage.getDescriptorForType(), fieldName); // check whether the field is a wrapper type, then test has and return null - if (isWrapperType(fieldDescriptor) && !typedMessage.hasField(fieldDescriptor)) { + if (isWrapperType(fieldDescriptor) + && !fieldDescriptor.isRepeated() + && !typedMessage.hasField(fieldDescriptor)) { return NullValue.NULL_VALUE; } Object value = typedMessage.getField(fieldDescriptor); @@ -176,10 +137,15 @@ public Object selectField(Object message, String fieldName) { /** Adapt object to its message value. */ @Override - public Object adapt(Object message) { + public Object adapt(String messageName, Object message) { if (message instanceof Message) { return protoAdapter.adaptProtoToValue((Message) message); } + + if (celOptions.evaluateCanonicalTypesToNativeValues() && message instanceof ByteString) { + return CelByteString.of(((ByteString) message).toByteArray()); + } + return message; } @@ -198,7 +164,7 @@ public Object hasField(Object message, String fieldName) { return map.containsKey(fieldName); } - MessageOrBuilder typedMessage = assertFullProtoMessage(message); + MessageOrBuilder typedMessage = assertFullProtoMessage(message, fieldName); FieldDescriptor fieldDescriptor = findField(typedMessage.getDescriptorForType(), fieldName); if (fieldDescriptor.isRepeated()) { return typedMessage.getRepeatedFieldCount(fieldDescriptor) > 0; @@ -207,33 +173,34 @@ 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 (celOptions.enableJsonFieldNames()) { + for (FieldDescriptor fd : descriptor.getFields()) { + if (fd.getJsonName().equals(fieldName)) { + 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) { + private static MessageOrBuilder assertFullProtoMessage(Object candidate, String fieldName) { if (!(candidate instanceof MessageOrBuilder)) { - // This is an internal error. It should not happen for type checked expressions. - throw new CelRuntimeException( - new IllegalStateException( - String.format( - "[internal] expected an instance of 'com.google.protobuf.MessageOrBuilder' " - + "but found '%s'", - candidate.getClass().getName())), - CelErrorCode.INTERNAL_ERROR); + // This can happen when the field selection is done on dyn, and it is not a message. + throw CelAttributeNotFoundException.forFieldResolution(fieldName); } return (MessageOrBuilder) candidate; } diff --git a/runtime/src/main/java/dev/cel/runtime/DescriptorTypeResolver.java b/runtime/src/main/java/dev/cel/runtime/DescriptorTypeResolver.java new file mode 100644 index 000000000..3d5208e2e --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/DescriptorTypeResolver.java @@ -0,0 +1,95 @@ +// 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 static com.google.common.base.Preconditions.checkNotNull; + +import com.google.errorprone.annotations.Immutable; +import com.google.protobuf.MessageOrBuilder; +import dev.cel.common.annotations.Internal; +import dev.cel.common.types.CelType; +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; + +/** + * {@code DescriptorTypeResolver} extends {@link TypeResolver} and additionally resolves incoming + * protobuf message types using descriptors. + * + *

CEL Library Internals. Do Not Use. + */ +@Immutable +@Internal +public final class DescriptorTypeResolver extends TypeResolver { + + private final @Nullable CelTypeProvider typeProvider; + + /** + * 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. + */ + @Deprecated + static DescriptorTypeResolver create(CelValueConverter celValueConverter) { + return new DescriptorTypeResolver(null, celValueConverter); + } + + /** + * Creates a {@code DescriptorTypeResolver}. If the protobuf message to be resolved can be found + * 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, CelValueConverter celValueConverter) { + return new DescriptorTypeResolver(typeProvider, celValueConverter); + } + + @Override + public TypeType resolveObjectType(Object obj, CelType typeCheckedType) { + checkNotNull(obj); + + Optional wellKnownTypeType = resolveWellKnownObjectType(obj); + if (wellKnownTypeType.isPresent()) { + return wellKnownTypeType.get(); + } + + if (obj instanceof MessageOrBuilder) { + MessageOrBuilder msg = (MessageOrBuilder) obj; + String typeName = msg.getDescriptorForType().getFullName(); + if (typeProvider != null) { + return typeProvider + .findType(typeName) + .map(TypeType::create) + .orElseThrow(() -> new NoSuchElementException("Could not find type: " + typeName)); + } else { + return TypeType.create(StructTypeReference.create(typeName)); + } + } + + return super.resolveObjectType(obj, typeCheckedType); + } + + private DescriptorTypeResolver( + @Nullable CelTypeProvider typeProvider, CelValueConverter celValueConverter) { + super(celValueConverter); + this.typeProvider = typeProvider; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/Dispatcher.java b/runtime/src/main/java/dev/cel/runtime/Dispatcher.java deleted file mode 100644 index fcaa087aa..000000000 --- a/runtime/src/main/java/dev/cel/runtime/Dispatcher.java +++ /dev/null @@ -1,66 +0,0 @@ -// 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.runtime; - -import com.google.errorprone.annotations.Immutable; -import javax.annotation.concurrent.ThreadSafe; -import dev.cel.common.annotations.Internal; -import java.util.List; - -/** - * An object which implements dispatching of function calls. - * - *

CEL Library Internals. Do Not Use. - */ -@ThreadSafe -@Internal -public interface Dispatcher { - - /** - * Invokes a function based on given parameters. - * - * @param metadata Metadata used for error reporting. - * @param exprId Expression identifier which can be used together with {@code metadata} to get - * information about the dispatch target for error reporting. - * @param functionName the logical name of the function being invoked. - * @param overloadIds A list of function overload ids. The dispatcher selects the unique overload - * from this list with matching arguments. - * @param args The arguments to pass to the function. - * @return The result of the function call. - * @throws InterpreterException if something goes wrong. - */ - Object dispatch( - Metadata metadata, long exprId, String functionName, List overloadIds, Object[] args) - throws InterpreterException; - - /** - * Returns an {@link ImmutableCopy} from current instance. - * - * @see ImmutableCopy - */ - ImmutableCopy immutableCopy(); - - /** - * An {@link Immutable} copy of a {@link Dispatcher}. Currently {@link DefaultDispatcher} - * implementation implements both {@link Dispatcher} and {@link Registrar} and cannot be annotated - * as {@link Immutable}. - * - *

Should consider to provide Registrar.dumpAsDispatcher and Registrar.dumpAsAsyncDispatcher - * instead of letting DefaultDispatcher or AsyncDispatcher to implement both Registrar and - * Dispatcher. But it requires a global refactoring. - */ - @Immutable - interface ImmutableCopy extends Dispatcher {} -} diff --git a/runtime/src/main/java/dev/cel/runtime/DynamicMessageFactory.java b/runtime/src/main/java/dev/cel/runtime/DynamicMessageFactory.java index 60bfb6cd8..41f78b078 100644 --- a/runtime/src/main/java/dev/cel/runtime/DynamicMessageFactory.java +++ b/runtime/src/main/java/dev/cel/runtime/DynamicMessageFactory.java @@ -26,7 +26,7 @@ import dev.cel.common.internal.DefaultMessageFactory; import dev.cel.common.internal.ProtoMessageFactory; import java.util.Collection; -import org.jspecify.nullness.Nullable; +import org.jspecify.annotations.Nullable; /** * The {@code DynamicMessageFactory} creates {@link DynamicMessage} instances by protobuf name. diff --git a/runtime/src/main/java/dev/cel/runtime/FunctionBindingImpl.java b/runtime/src/main/java/dev/cel/runtime/FunctionBindingImpl.java new file mode 100644 index 000000000..7b8efe8fd --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/FunctionBindingImpl.java @@ -0,0 +1,216 @@ +// 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 static com.google.common.collect.ImmutableList.toImmutableList; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.exceptions.CelOverloadNotFoundException; + +@Immutable +final class FunctionBindingImpl implements InternalCelFunctionBinding { + + private final String functionName; + + private final String overloadId; + + private final ImmutableList> argTypes; + + private final CelFunctionOverload definition; + + private final boolean isStrict; + + @Override + public String getFunctionName() { + return functionName; + } + + @Override + public String getOverloadId() { + return overloadId; + } + + @Override + public ImmutableList> getArgTypes() { + return argTypes; + } + + @Override + public CelFunctionOverload getDefinition() { + return definition; + } + + @Override + public boolean isStrict() { + return 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(); + 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. + // (Ex: "matches" function) + boolean hasSingletonBinding = + overloadBindings.stream().anyMatch(b -> b.getOverloadId().equals(functionName)); + + if (!hasSingletonBinding) { + if (overloadBindings.size() == 1) { + CelFunctionBinding singleBinding = Iterables.getOnlyElement(overloadBindings); + builder.add( + new FunctionBindingImpl( + functionName, + functionName, + singleBinding.getArgTypes(), + singleBinding.getDefinition(), + singleBinding.isStrict())); + } else if (overloadBindings.size() > 1) { + builder.add(new DynamicDispatchBinding(functionName, overloadBindings)); + } + } + + return builder.build(); + } + + @Immutable + static final class DynamicDispatchBinding implements InternalCelFunctionBinding { + + private final boolean isStrict; + private final DynamicDispatchOverload dynamicDispatchOverload; + + @Override + public String getOverloadId() { + return dynamicDispatchOverload.functionName; + } + + @Override + public String getFunctionName() { + return dynamicDispatchOverload.functionName; + } + + @Override + public ImmutableList> getArgTypes() { + return ImmutableList.of(); + } + + @Override + public CelFunctionOverload getDefinition() { + return dynamicDispatchOverload; + } + + @Override + public boolean isStrict() { + return isStrict; + } + + private DynamicDispatchBinding( + String functionName, ImmutableSet overloadBindings) { + this.isStrict = overloadBindings.stream().allMatch(CelFunctionBinding::isStrict); + this.dynamicDispatchOverload = new DynamicDispatchOverload(functionName, overloadBindings); + } + } + + @Immutable + static final class DynamicDispatchOverload implements OptimizedFunctionOverload { + private final String functionName; + private final ImmutableSet overloadBindings; + + @Override + public Object apply(Object[] args) throws CelEvaluationException { + for (CelFunctionBinding overload : overloadBindings) { + if (CelFunctionOverload.canHandle(args, overload.getArgTypes(), overload.isStrict())) { + return overload.getDefinition().apply(args); + } + } + + throw new CelOverloadNotFoundException( + functionName, + overloadBindings.stream() + .map(CelFunctionBinding::getOverloadId) + .collect(toImmutableList())); + } + + @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/GlobalResolver.java b/runtime/src/main/java/dev/cel/runtime/GlobalResolver.java index 2cc1a7d31..ffc1933b6 100644 --- a/runtime/src/main/java/dev/cel/runtime/GlobalResolver.java +++ b/runtime/src/main/java/dev/cel/runtime/GlobalResolver.java @@ -15,7 +15,7 @@ package dev.cel.runtime; import dev.cel.common.annotations.Internal; -import org.jspecify.nullness.Nullable; +import org.jspecify.annotations.Nullable; /** * An interface describing an object that can perform a lookup on a given name, returning the value @@ -27,6 +27,20 @@ @Internal public interface GlobalResolver { + /** An empty binder which resolves everything to null. */ + GlobalResolver EMPTY = + new GlobalResolver() { + @Override + public @Nullable Object resolve(String name) { + return null; + } + + @Override + public String toString() { + return "{}"; + } + }; + /** Resolves the given name to its value. Returns null if resolution fails. */ @Nullable Object resolve(String name); } diff --git a/runtime/src/main/java/dev/cel/runtime/HierarchicalVariableResolver.java b/runtime/src/main/java/dev/cel/runtime/HierarchicalVariableResolver.java new file mode 100644 index 000000000..5efc4fea4 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/HierarchicalVariableResolver.java @@ -0,0 +1,44 @@ +// 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. + +package dev.cel.runtime; + +import java.util.Optional; + +final class HierarchicalVariableResolver implements CelVariableResolver { + + private final CelVariableResolver primary; + private final CelVariableResolver secondary; + + @Override + public Optional find(String name) { + Optional value = primary.find(name); + return value.isPresent() ? value : secondary.find(name); + } + + @Override + public String toString() { + return secondary + " +> " + primary; + } + + static HierarchicalVariableResolver newInstance( + CelVariableResolver primary, CelVariableResolver secondary) { + return new HierarchicalVariableResolver(primary, secondary); + } + + private HierarchicalVariableResolver(CelVariableResolver primary, CelVariableResolver secondary) { + this.primary = primary; + this.secondary = secondary; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/IncompleteData.java b/runtime/src/main/java/dev/cel/runtime/InternalCelFunctionBinding.java similarity index 67% rename from runtime/src/main/java/dev/cel/runtime/IncompleteData.java rename to runtime/src/main/java/dev/cel/runtime/InternalCelFunctionBinding.java index 4859f63c6..48a0f36d1 100644 --- a/runtime/src/main/java/dev/cel/runtime/IncompleteData.java +++ b/runtime/src/main/java/dev/cel/runtime/InternalCelFunctionBinding.java @@ -1,4 +1,4 @@ -// Copyright 2022 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. @@ -14,13 +14,16 @@ package dev.cel.runtime; +import com.google.errorprone.annotations.Immutable; import dev.cel.common.annotations.Internal; /** - * Add an interface for interpreter to check if an object is an instance of {@link PartialMessage}. + * Internal interface to expose the function name associated with a binding. * - *

Deprecated. New clients should use {@link CelAttribute} based unknowns. + *

CEL Library Internals. Do Not Use. */ -@Deprecated @Internal -public interface IncompleteData {} +@Immutable +public interface InternalCelFunctionBinding extends CelFunctionBinding { + String getFunctionName(); +} diff --git a/runtime/src/main/java/dev/cel/runtime/InternalFunctionBinder.java b/runtime/src/main/java/dev/cel/runtime/InternalFunctionBinder.java new file mode 100644 index 000000000..5a063ee6c --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/InternalFunctionBinder.java @@ -0,0 +1,49 @@ +// 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.common.collect.ImmutableList; +import dev.cel.common.annotations.Internal; + +/** + * A helper to create CelFunctionBinding instances with sensitive controls, such as to toggle the + * strictness of the function. + * + *

CEL Library Internals. Do Not Use. + */ +@Internal +public final class InternalFunctionBinder { + + /** + * Create a unary function binding from the {@code overloadId}, {@code arg}, {@code impl}, and + * {@code} isStrict. + */ + @SuppressWarnings("unchecked") + public static CelFunctionBinding from( + String overloadId, Class arg, CelFunctionOverload.Unary impl, boolean isStrict) { + return from(overloadId, ImmutableList.of(arg), (args) -> impl.apply((T) args[0]), isStrict); + } + + /** + * Create a function binding from the {@code overloadId}, {@code argTypes}, {@code impl} and + * {@code isStrict}. + */ + public static CelFunctionBinding from( + String overloadId, Iterable> argTypes, CelFunctionOverload impl, boolean isStrict) { + return new FunctionBindingImpl(overloadId, ImmutableList.copyOf(argTypes), impl, isStrict); + } + + private InternalFunctionBinder() {} +} diff --git a/runtime/src/main/java/dev/cel/runtime/Interpretable.java b/runtime/src/main/java/dev/cel/runtime/Interpretable.java index 0c967416a..ece90cb4b 100644 --- a/runtime/src/main/java/dev/cel/runtime/Interpretable.java +++ b/runtime/src/main/java/dev/cel/runtime/Interpretable.java @@ -27,7 +27,38 @@ public interface Interpretable { /** Runs interpretation with the given activation which supplies name/value bindings. */ - Object eval(GlobalResolver resolver) throws InterpreterException; + Object eval(GlobalResolver resolver) throws CelEvaluationException; - Object eval(GlobalResolver resolver, CelEvaluationListener listener) throws InterpreterException; + /** + * Runs interpretation with the given activation which supplies name/value bindings. + * + *

This method allows for evaluation listeners to be provided per-evaluation. + */ + Object eval(GlobalResolver resolver, CelEvaluationListener listener) + throws CelEvaluationException; + + /** + * Runs interpretation with the given activation which supplies name/value bindings. + * + *

This method allows for late-binding functions to be provided per-evaluation, which can be + * useful for binding functions which might have side-effects that are not observable to CEL + * directly such as recording telemetry or evaluation state in a more granular fashion than a more + * general evaluation listener might permit. + */ + Object eval(GlobalResolver resolver, CelFunctionResolver lateBoundFunctionResolver) + throws CelEvaluationException; + + /** + * Runs interpretation with the given activation which supplies name/value bindings. + * + *

This method allows for late-binding functions to be provided per-evaluation, which can be + * useful for binding functions which might have side-effects that are not observable to CEL + * directly such as recording telemetry or evaluation state in a more granular fashion than a more + * general evaluation listener might permit. + */ + Object eval( + GlobalResolver resolver, + CelFunctionResolver lateBoundFunctionResolver, + CelEvaluationListener listener) + throws CelEvaluationException; } diff --git a/runtime/src/main/java/dev/cel/runtime/Interpreter.java b/runtime/src/main/java/dev/cel/runtime/Interpreter.java index c6fe08077..5c316da1a 100644 --- a/runtime/src/main/java/dev/cel/runtime/Interpreter.java +++ b/runtime/src/main/java/dev/cel/runtime/Interpreter.java @@ -14,7 +14,6 @@ package dev.cel.runtime; -import dev.cel.expr.CheckedExpr; import javax.annotation.concurrent.ThreadSafe; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.annotations.Internal; @@ -28,16 +27,6 @@ @Internal public interface Interpreter { - /** - * Creates an interpretable for the given expression. - * - *

This method may run pre-processing and partial evaluation of the expression it gets passed. - * - * @deprecated Use {@link #createInterpretable(CelAbstractSyntaxTree)} instead. - */ - @Deprecated - Interpretable createInterpretable(CheckedExpr checkedExpr) throws InterpreterException; - /** * Creates an interpretable for the given expression. * diff --git a/runtime/src/main/java/dev/cel/runtime/InterpreterException.java b/runtime/src/main/java/dev/cel/runtime/InterpreterException.java deleted file mode 100644 index 98595ee88..000000000 --- a/runtime/src/main/java/dev/cel/runtime/InterpreterException.java +++ /dev/null @@ -1,107 +0,0 @@ -// 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.runtime; - -import com.google.errorprone.annotations.CanIgnoreReturnValue; -import com.google.errorprone.annotations.CheckReturnValue; -import dev.cel.common.CelErrorCode; -import dev.cel.common.CelRuntimeException; -import dev.cel.common.annotations.Internal; -import dev.cel.common.internal.SafeStringFormatter; -import org.jspecify.nullness.Nullable; - -/** - * An exception produced during interpretation of expressions. - * - *

TODO: Remove in favor of creating exception types that corresponds to the error - * code. - * - *

CEL Library Internals. Do Not Use. - */ -@Internal -public class InterpreterException extends Exception { - private final CelErrorCode errorCode; - - public CelErrorCode getErrorCode() { - return errorCode; - } - - /** Builder for InterpreterException. */ - public static class Builder { - private final String message; - @Nullable private String location; - private int position; - private Throwable cause; - private CelErrorCode errorCode = CelErrorCode.INTERNAL_ERROR; - - @SuppressWarnings({"AnnotateFormatMethod"}) // Format strings are optional. - public Builder(String message, Object... args) { - this.message = SafeStringFormatter.format(message, args); - } - - @SuppressWarnings({"AnnotateFormatMethod"}) // Format strings are optional. - public Builder(RuntimeException e, String message, Object... args) { - if (e instanceof CelRuntimeException) { - CelRuntimeException celRuntimeException = (CelRuntimeException) e; - this.errorCode = celRuntimeException.getErrorCode(); - // CelRuntimeException is just a wrapper for the specific RuntimeException (typically - // IllegalArgumentException). The underlying cause and its message is what we are actually - // interested in. - this.cause = e.getCause(); - message = e.getCause().getMessage(); - } else { - this.cause = e; - } - - this.message = SafeStringFormatter.format(message, args); - } - - @CanIgnoreReturnValue - public Builder setLocation(@Nullable Metadata metadata, long exprId) { - if (metadata != null) { - this.location = metadata.getLocation(); - this.position = metadata.getPosition(exprId); - } - return this; - } - - @CanIgnoreReturnValue - public Builder setCause(Throwable cause) { - this.cause = cause; - return this; - } - - @CanIgnoreReturnValue - public Builder setErrorCode(CelErrorCode errorCode) { - this.errorCode = errorCode; - return this; - } - - @CheckReturnValue - public InterpreterException build() { - return new InterpreterException( - String.format( - "evaluation error%s: %s", - location != null ? " at " + location + ":" + position : "", message), - cause, - errorCode); - } - } - - private InterpreterException(String message, Throwable cause, CelErrorCode errorCode) { - super(message, cause); - this.errorCode = errorCode; - } -} diff --git a/runtime/src/main/java/dev/cel/runtime/InterpreterUtil.java b/runtime/src/main/java/dev/cel/runtime/InterpreterUtil.java index 05a764632..73607cefd 100644 --- a/runtime/src/main/java/dev/cel/runtime/InterpreterUtil.java +++ b/runtime/src/main/java/dev/cel/runtime/InterpreterUtil.java @@ -14,16 +14,10 @@ package dev.cel.runtime; -import dev.cel.expr.ExprValue; -import dev.cel.expr.UnknownSet; -import com.google.errorprone.annotations.CanIgnoreReturnValue; -import dev.cel.common.CelErrorCode; +import com.google.common.collect.ImmutableSet; +import com.google.errorprone.annotations.CheckReturnValue; import dev.cel.common.annotations.Internal; -import java.util.Arrays; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; -import org.jspecify.nullness.Nullable; +import org.jspecify.annotations.Nullable; /** * Util class for CEL interpreter. @@ -38,12 +32,13 @@ public final class InterpreterUtil { * {@link Throwable}. Applying {@code strict()} to such a value-or-throwable will re-throw the * proper exception. */ - public static Object strict(Object valueOrThrowable) throws InterpreterException { + @CheckReturnValue + public static Object strict(Object valueOrThrowable) throws CelEvaluationException { if (!(valueOrThrowable instanceof Throwable)) { return valueOrThrowable; } - if (valueOrThrowable instanceof InterpreterException) { - throw (InterpreterException) valueOrThrowable; + if (valueOrThrowable instanceof CelEvaluationException) { + throw (CelEvaluationException) valueOrThrowable; } if (valueOrThrowable instanceof RuntimeException) { throw (RuntimeException) valueOrThrowable; @@ -52,97 +47,47 @@ public static Object strict(Object valueOrThrowable) throws InterpreterException } /** - * Check if raw object is ExprValue object and has UnknownSet + * Check if raw object is {@link CelUnknownSet}. * * @param obj Object to check. * @return boolean value if object is unknown. */ public static boolean isUnknown(Object obj) { - return obj instanceof ExprValue - && ((ExprValue) obj).getKindCase() == ExprValue.KindCase.UNKNOWN; + return obj instanceof CelUnknownSet; } - /** - * Throws an InterpreterException with {@code exceptionMessage} if the {@code obj} is an instance - * of {@link IncompleteData}. {@link IncompleteData} does not support some operators. - * - *

Returns the obj argument otherwise. - * - *

Deprecated. TODO: Can be removed once clients have stopped using - * IncompleteData. - */ - @CanIgnoreReturnValue - @Deprecated - public static Object completeDataOnly(Object obj, String exceptionMessage) - throws InterpreterException { - if (obj instanceof IncompleteData) { - throw new InterpreterException.Builder(exceptionMessage) - .setErrorCode(CelErrorCode.INVALID_ARGUMENT) - .build(); - } - return obj; + public static boolean isAccumulatedUnknowns(Object obj) { + return obj instanceof AccumulatedUnknowns; } - /** - * Combine multiple ExprValue objects which has UnknownSet into one ExprValue - * - * @param objs ExprValue objects which has UnknownSet - * @return A new ExprValue object which has all unknown expr ids from input objects, without - * duplication. - */ - public static ExprValue combineUnknownExprValue(Object... objs) { - UnknownSet.Builder unknownsetBuilder = UnknownSet.newBuilder(); - Set ids = new LinkedHashSet<>(); - for (Object object : objs) { - if (isUnknown(object)) { - ids.addAll(((ExprValue) object).getUnknown().getExprsList()); - } + /** If the argument is {@link CelUnknownSet}, adapts it into {@link AccumulatedUnknowns} */ + public static Object maybeAdaptToAccumulatedUnknowns(Object val) { + if (!(val instanceof CelUnknownSet)) { + return val; } - unknownsetBuilder.addAllExprs(ids); - return ExprValue.newBuilder().setUnknown(unknownsetBuilder).build(); + + return adaptToAccumulatedUnknowns((CelUnknownSet) val); } - /** Create a {@code ExprValue} for one or more {@code ids} representing an unknown set. */ - public static ExprValue createUnknownExprValue(Long... ids) { - return createUnknownExprValue(Arrays.asList(ids)); + public static AccumulatedUnknowns adaptToAccumulatedUnknowns(CelUnknownSet unknowns) { + return AccumulatedUnknowns.create(unknowns.unknownExprIds(), unknowns.attributes()); } - /** - * Create an ExprValue object has UnknownSet, from a list of unknown expr ids - * - * @param ids List of unknown expr ids - * @return A new ExprValue object which has all unknown expr ids from input list - */ - public static ExprValue createUnknownExprValue(List ids) { - ExprValue.Builder exprValueBuilder = ExprValue.newBuilder(); - exprValueBuilder.setUnknown(UnknownSet.newBuilder().addAllExprs(ids)); - return exprValueBuilder.build(); + 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())); } /** - * Short circuit unknown or error arguments to logical operators. - * - *

Given two arguments, one of which must be throwable (error) or unknown, returns the result - * from the && or || operators for these arguments, assuming that the result cannot be determined - * from any boolean arguments alone. This allows us to consolidate the error/unknown handling for - * both of these operators. + * 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. */ - public static Object shortcircuitUnknownOrThrowable(Object left, Object right) - throws InterpreterException { - // unknown unknown ==> unknown combined - if (InterpreterUtil.isUnknown(left) && InterpreterUtil.isUnknown(right)) { - return InterpreterUtil.combineUnknownExprValue(left, right); - } - // unknown ==> unknown - // unknown t|f ==> unknown - if (InterpreterUtil.isUnknown(left)) { - return left; - } - // unknown ==> unknown - // t|f unknown ==> unknown - if (InterpreterUtil.isUnknown(right)) { - return right; - } + public static Object enforceStrictness(Object left, Object right) throws CelEvaluationException { // Throw left or right side exception for now, should combine them into ErrorSet. // ==> if (left instanceof Throwable) { @@ -157,16 +102,12 @@ public static Object shortcircuitUnknownOrThrowable(Object left, Object right) public static Object valueOrUnknown(@Nullable Object valueOrThrowable, Long id) { // Handle the unknown value case. - if (isUnknown(valueOrThrowable)) { - ExprValue value = (ExprValue) valueOrThrowable; - if (value.getUnknown().getExprsCount() != 0) { - return valueOrThrowable; - } - return createUnknownExprValue(id); + if (isAccumulatedUnknowns(valueOrThrowable)) { + return AccumulatedUnknowns.create(id); } // Handle the null value case. if (valueOrThrowable == null) { - return createUnknownExprValue(id); + return AccumulatedUnknowns.create(id); } return valueOrThrowable; } diff --git a/runtime/src/main/java/dev/cel/runtime/LiteProgramImpl.java b/runtime/src/main/java/dev/cel/runtime/LiteProgramImpl.java new file mode 100644 index 000000000..af8c1a6d0 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/LiteProgramImpl.java @@ -0,0 +1,64 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime; + +import com.google.auto.value.AutoValue; +import com.google.errorprone.annotations.Immutable; +import java.util.Map; + +@Immutable +@AutoValue +abstract class LiteProgramImpl implements Program { + + abstract Interpretable interpretable(); + + @Override + public Object eval() throws CelEvaluationException { + return interpretable().eval(GlobalResolver.EMPTY); + } + + @Override + public Object eval(Map mapValue) throws CelEvaluationException { + return interpretable().eval(Activation.copyOf(mapValue)); + } + + @Override + public Object eval(Map mapValue, CelFunctionResolver lateBoundFunctionResolver) + throws CelEvaluationException { + return interpretable().eval(Activation.copyOf(mapValue), lateBoundFunctionResolver); + } + + @Override + public Object eval(CelVariableResolver resolver, CelFunctionResolver lateBoundFunctionResolver) { + // TODO: Wire in program planner + throw new UnsupportedOperationException("To be implemented"); + } + + @Override + public Object eval(CelVariableResolver resolver) throws CelEvaluationException { + // TODO: Wire in program planner + throw new UnsupportedOperationException("To be implemented"); + } + + @Override + public Object eval(PartialVars partialVars) throws CelEvaluationException { + // TODO: Wire in program planner + throw new UnsupportedOperationException("To be implemented"); + } + + static Program plan(Interpretable interpretable) { + return new AutoValue_LiteProgramImpl(interpretable); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/LiteRuntimeImpl.java b/runtime/src/main/java/dev/cel/runtime/LiteRuntimeImpl.java new file mode 100644 index 000000000..8ce2d7733 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/LiteRuntimeImpl.java @@ -0,0 +1,221 @@ +// 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 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.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.CelOptions; +import dev.cel.common.values.CelValueProvider; +import dev.cel.runtime.standard.CelStandardFunction; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Optional; + +@ThreadSafe +final class LiteRuntimeImpl implements CelLiteRuntime { + private final Interpreter interpreter; + private final CelOptions celOptions; + private final ImmutableList customFunctionBindings; + private final ImmutableSet celStandardFunctions; + private final CelValueProvider celValueProvider; + + // 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)); + } + + @Override + public CelLiteRuntimeBuilder toRuntimeBuilder() { + CelLiteRuntimeBuilder builder = + new Builder() + .setOptions(celOptions) + .setStandardFunctions(celStandardFunctions) + .addFunctionBindings(customFunctionBindings) + .addLibraries(runtimeLibraries); + + if (celValueProvider != null) { + builder.setValueProvider(celValueProvider); + } + + return builder; + } + + static final class Builder implements CelLiteRuntimeBuilder { + + // Following is visible to test `toBuilder`. + @VisibleForTesting CelOptions celOptions; + @VisibleForTesting final HashMap customFunctionBindings; + @VisibleForTesting final ImmutableSet.Builder runtimeLibrariesBuilder; + @VisibleForTesting final ImmutableSet.Builder standardFunctionBuilder; + @VisibleForTesting CelValueProvider celValueProvider; + + @Override + public CelLiteRuntimeBuilder setOptions(CelOptions celOptions) { + this.celOptions = celOptions; + return this; + } + + @Override + public CelLiteRuntimeBuilder setStandardFunctions(CelStandardFunction... standardFunctions) { + return setStandardFunctions(Arrays.asList(standardFunctions)); + } + + @Override + public CelLiteRuntimeBuilder setStandardFunctions( + Iterable standardFunctions) { + standardFunctionBuilder.addAll(standardFunctions); + return this; + } + + @Override + public CelLiteRuntimeBuilder addFunctionBindings(CelFunctionBinding... bindings) { + return addFunctionBindings(Arrays.asList(bindings)); + } + + @Override + public CelLiteRuntimeBuilder addFunctionBindings(Iterable bindings) { + bindings.forEach(o -> customFunctionBindings.putIfAbsent(o.getOverloadId(), o)); + return this; + } + + @Override + public CelLiteRuntimeBuilder setValueProvider(CelValueProvider celValueProvider) { + this.celValueProvider = celValueProvider; + return this; + } + + @Override + public CelLiteRuntimeBuilder addLibraries(CelLiteRuntimeLibrary... libraries) { + return addLibraries(Arrays.asList(checkNotNull(libraries))); + } + + @Override + public CelLiteRuntimeBuilder addLibraries(Iterable libraries) { + this.runtimeLibrariesBuilder.addAll(checkNotNull(libraries)); + 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."); + } + if (!celOptions.unwrapWellKnownTypesOnFunctionDispatch()) { + throw new IllegalArgumentException( + prefix + "unwrapWellKnownTypesOnFunctionDispatch cannot be disabled."); + } + } + + @Override + public CelLiteRuntime build() { + assertAllowedCelOptions(celOptions); + ImmutableSet runtimeLibs = runtimeLibrariesBuilder.build(); + runtimeLibs.forEach(lib -> lib.setRuntimeOptions(this)); + + ImmutableMap.Builder functionBindingsBuilder = + ImmutableMap.builder(); + + ImmutableSet standardFunctions = standardFunctionBuilder.build(); + if (!standardFunctions.isEmpty()) { + RuntimeHelpers runtimeHelpers = RuntimeHelpers.create(); + RuntimeEquality runtimeEquality = RuntimeEquality.create(runtimeHelpers, celOptions); + for (CelStandardFunction standardFunction : standardFunctions) { + ImmutableSet standardFunctionBinding = + standardFunction.newFunctionBindings(celOptions, runtimeEquality); + for (CelFunctionBinding func : standardFunctionBinding) { + functionBindingsBuilder.put(func.getOverloadId(), func); + } + } + } + + functionBindingsBuilder.putAll(customFunctionBindings); + + DefaultDispatcher.Builder dispatcherBuilder = DefaultDispatcher.newBuilder(); + functionBindingsBuilder + .buildOrThrow() + .forEach( + (String overloadId, CelFunctionBinding func) -> { + String functionName = func.getOverloadId(); + if (func instanceof InternalCelFunctionBinding) { + functionName = ((InternalCelFunctionBinding) func).getFunctionName(); + } + dispatcherBuilder.addOverload( + functionName, + overloadId, + func.getArgTypes(), + func.isStrict(), + func.getDefinition()); + }); + + Interpreter interpreter = + new DefaultInterpreter( + TypeResolver.create(celValueProvider.celValueConverter()), + CelValueRuntimeTypeProvider.newInstance(celValueProvider), + dispatcherBuilder.build(), + celOptions); + + return new LiteRuntimeImpl( + interpreter, + celOptions, + customFunctionBindings.values(), + standardFunctions, + runtimeLibs, + celValueProvider); + } + + private Builder() { + this.celOptions = CelOptions.current().enableCelValue(true).build(); + this.celValueProvider = (structType, fields) -> Optional.empty(); + this.customFunctionBindings = new HashMap<>(); + this.standardFunctionBuilder = ImmutableSet.builder(); + this.runtimeLibrariesBuilder = ImmutableSet.builder(); + } + } + + static CelLiteRuntimeBuilder newBuilder() { + return new Builder(); + } + + private LiteRuntimeImpl( + Interpreter interpreter, + CelOptions celOptions, + Iterable customFunctionBindings, + ImmutableSet celStandardFunctions, + ImmutableSet runtimeLibraries, + CelValueProvider celValueProvider) { + this.interpreter = interpreter; + this.celOptions = celOptions; + this.customFunctionBindings = ImmutableList.copyOf(customFunctionBindings); + this.celStandardFunctions = celStandardFunctions; + this.runtimeLibraries = runtimeLibraries; + this.celValueProvider = celValueProvider; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/MessageFactory.java b/runtime/src/main/java/dev/cel/runtime/MessageFactory.java index e56825b4a..a7ec8de23 100644 --- a/runtime/src/main/java/dev/cel/runtime/MessageFactory.java +++ b/runtime/src/main/java/dev/cel/runtime/MessageFactory.java @@ -20,7 +20,7 @@ import dev.cel.common.internal.DefaultMessageFactory; import dev.cel.common.internal.ProtoMessageFactory; import java.util.Optional; -import org.jspecify.nullness.Nullable; +import org.jspecify.annotations.Nullable; /** * The {@code MessageFactory} provides a method to create a protobuf builder objects by name. diff --git a/runtime/src/main/java/dev/cel/runtime/MessageProvider.java b/runtime/src/main/java/dev/cel/runtime/MessageProvider.java index 96a803fdb..b92f2a668 100644 --- a/runtime/src/main/java/dev/cel/runtime/MessageProvider.java +++ b/runtime/src/main/java/dev/cel/runtime/MessageProvider.java @@ -34,6 +34,6 @@ public interface MessageProvider { /** Check whether a field is set on message. */ Object hasField(Object message, String fieldName); - /** Adapt object to its message value with source location metadata on failure . */ - Object adapt(Object message); + /** Adapt object to its message value with source location metadata on failure. */ + Object adapt(String messageName, Object message); } diff --git a/runtime/src/main/java/dev/cel/runtime/Metadata.java b/runtime/src/main/java/dev/cel/runtime/Metadata.java index 6bbda654e..05d60768c 100644 --- a/runtime/src/main/java/dev/cel/runtime/Metadata.java +++ b/runtime/src/main/java/dev/cel/runtime/Metadata.java @@ -33,4 +33,7 @@ public interface Metadata { * Returns the character position of the node in the source. This is a 0-based character offset. */ int getPosition(long exprId); + + /** Checks if a source position recorded for the provided expression id. */ + boolean hasPosition(long exprId); } diff --git a/common/src/main/java/dev/cel/common/values/BytesValue.java b/runtime/src/main/java/dev/cel/runtime/OptimizedFunctionOverload.java similarity index 50% rename from common/src/main/java/dev/cel/common/values/BytesValue.java rename to runtime/src/main/java/dev/cel/runtime/OptimizedFunctionOverload.java index d4c309364..fde8bcc15 100644 --- a/common/src/main/java/dev/cel/common/values/BytesValue.java +++ b/runtime/src/main/java/dev/cel/runtime/OptimizedFunctionOverload.java @@ -1,4 +1,4 @@ -// Copyright 2023 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,32 +12,24 @@ // See the License for the specific language governing permissions and // limitations under the License. -package dev.cel.common.values; +package dev.cel.runtime; -import com.google.auto.value.AutoValue; import com.google.errorprone.annotations.Immutable; -import dev.cel.common.types.CelType; -import dev.cel.common.types.SimpleType; -/** BytesValue is a simple CelValue wrapper around CelByteString (immutable byte string). */ -@AutoValue +/** + * Internal interface to support fast-path Unary and Binary evaluations, avoiding Object[] + * allocation. + */ @Immutable -public abstract class BytesValue extends CelValue { +interface OptimizedFunctionOverload extends CelFunctionOverload { - @Override - public abstract CelByteString value(); - - @Override - public boolean isZeroValue() { - return value().isEmpty(); - } - - @Override - public CelType celType() { - return SimpleType.BYTES; + /** Fast-path for unary function execution to avoid Object[] allocation. */ + default Object apply(Object arg) throws CelEvaluationException { + return apply(new Object[] {arg}); } - public static BytesValue create(CelByteString value) { - return new AutoValue_BytesValue(value); + /** 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/PartialMessage.java b/runtime/src/main/java/dev/cel/runtime/PartialMessage.java deleted file mode 100644 index 0d9ad5dca..000000000 --- a/runtime/src/main/java/dev/cel/runtime/PartialMessage.java +++ /dev/null @@ -1,181 +0,0 @@ -// 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.runtime; - -import com.google.protobuf.Descriptors; -import com.google.protobuf.Descriptors.Descriptor; -import com.google.protobuf.Descriptors.FieldDescriptor; -import com.google.protobuf.Descriptors.OneofDescriptor; -import com.google.protobuf.FieldMask; -import com.google.protobuf.Message; -import com.google.protobuf.UnknownFieldSet; -import com.google.protobuf.util.FieldMaskUtil; -import dev.cel.common.annotations.Internal; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - -/** - * Wrap a Message to throw an error on access to certain fields or sub-fields, as described by a - * FieldMask. - * - *

Deprecated. New clients should use {@link CelAttribute} based unknowns. - */ -@Deprecated -@Internal -public class PartialMessage implements PartialMessageOrBuilder, IncompleteData { - - private final Message message; - private final FieldMask fieldMask; - - @Override - public Message getDefaultInstanceForType() { - return message.getDefaultInstanceForType(); - } - - @Override - public boolean isInitialized() { - return message.isInitialized(); - } - - @Override - public List findInitializationErrors() { - return message.findInitializationErrors(); - } - - @Override - public String getInitializationErrorString() { - return message.getInitializationErrorString(); - } - - @Override - public Descriptor getDescriptorForType() { - return message.getDescriptorForType(); - } - - @Override - public Map getAllFields() { - return message.getAllFields(); - } - - @Override - public boolean hasOneof(OneofDescriptor oneof) { - return message.hasOneof(oneof); - } - - @Override - public FieldDescriptor getOneofFieldDescriptor(OneofDescriptor oneof) { - return message.getOneofFieldDescriptor(oneof); - } - - @Override - public boolean hasField(FieldDescriptor field) { - return message.hasField(field); - } - - /** Create relative field masks to the field specified by the name. */ - private FieldMask subpathMask(String name) { - FieldMask.Builder builder = FieldMask.newBuilder(); - for (String p : fieldMask.getPathsList()) { - if (!p.startsWith(name)) { - continue; - } - String tmp = p.substring(name.length() + 1); - if (tmp.length() > 0) { - builder.addPaths(tmp); - } - } - return builder.build(); - } - - @Override - public Object getField(Descriptors.FieldDescriptor field) { - String path = field.getName(); - - if (fieldMask.getPathsList().contains(path)) { - return InterpreterUtil.createUnknownExprValue(new ArrayList()); - } - - Object obj = message.getField(field); - FieldMask subFieldMask = subpathMask(path); - if (obj instanceof Message && !subFieldMask.getPathsList().isEmpty()) { - // Partial message means at least one of its field has been marked as unknown - return new PartialMessage((Message) obj, subFieldMask); - } else { - return obj; - } - } - - @Override - public int getRepeatedFieldCount(FieldDescriptor field) { - return message.getRepeatedFieldCount(field); - } - - @Override - public Object getRepeatedField(FieldDescriptor field, int index) { - return message.getRepeatedField(field, index); - } - - @Override - public UnknownFieldSet getUnknownFields() { - return message.getUnknownFields(); - } - - public PartialMessage(Message m) { - this.message = m; - this.fieldMask = FieldMask.getDefaultInstance(); - } - - public PartialMessage(Message m, FieldMask mask) { - this.message = m; - this.fieldMask = mask; - - if (m == null) { - throw new NullPointerException("The message in PartialMessage is null."); - } - if (!FieldMaskUtil.isValid(m.getDescriptorForType(), fieldMask)) { - throw new RuntimeException( - new InterpreterException.Builder("Invalid field mask for message:" + message).build()); - } - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append(this.getClass()); - sb.append("{\nmessage: {\n"); - sb.append(this.message); - sb.append("},\nfieldMask: {\n"); - for (Iterator it = fieldMask.getPathsList().iterator(); it.hasNext(); ) { - sb.append(" paths: ").append(it.next()); - if (it.hasNext()) { - sb.append(",\n"); - } - } - sb.append("\n}\n"); - return sb.toString(); - } - - @Override - public Message getMessage() { - return message; - } - - @Override - public FieldMask getFieldMask() { - return fieldMask; - } -} diff --git a/runtime/src/main/java/dev/cel/runtime/PartialMessageOrBuilder.java b/runtime/src/main/java/dev/cel/runtime/PartialMessageOrBuilder.java deleted file mode 100644 index 9009ea37e..000000000 --- a/runtime/src/main/java/dev/cel/runtime/PartialMessageOrBuilder.java +++ /dev/null @@ -1,46 +0,0 @@ -// 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.runtime; - -import com.google.protobuf.Descriptors.FieldDescriptor; -import com.google.protobuf.FieldMask; -import com.google.protobuf.Message; -import com.google.protobuf.MessageOrBuilder; -import dev.cel.common.annotations.Internal; - -/** - * Wrap Message to support Unknown value. - * - *

Deprecated. New clients should use {@link CelAttribute} based unknowns. - */ -@Deprecated -@Internal -public interface PartialMessageOrBuilder extends MessageOrBuilder { - /** Return original message. */ - public Message getMessage(); - - /* - * Return field mask. - */ - public FieldMask getFieldMask(); - - /** - * This method is similar to {@link MessageOrBuilder#getField(FieldDescriptor)}, with the - * following differences: This method may throw an InterpreterException wrapped with a - * RuntimeException, if the field path is set in the Field mask. - */ - @Override - public Object getField(FieldDescriptor field); -} 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 new file mode 100644 index 000000000..e808a373c --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/Program.java @@ -0,0 +1,49 @@ +// 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.errorprone.annotations.Immutable; +import java.util.Map; + +/** Creates an evaluable {@code Program} instance which is thread-safe and immutable. */ +@Immutable +public interface Program { + + /** Evaluate the expression without any variables. */ + Object eval() throws CelEvaluationException; + + /** Evaluate the expression using a {@code mapValue} as the source of input variables. */ + Object eval(Map mapValue) throws CelEvaluationException; + + /** + * Evaluate a compiled program with {@code mapValue} and late-bound functions {@code + * lateBoundFunctionResolver}. + */ + Object eval(Map mapValue, CelFunctionResolver lateBoundFunctionResolver) + throws CelEvaluationException; + + /** Evaluate a compiled program with a custom variable {@code resolver}. */ + Object eval(CelVariableResolver resolver) throws CelEvaluationException; + + /** + * Evaluate a compiled program with a custom variable {@code resolver} and late-bound functions + * {@code lateBoundFunctionResolver}. + */ + Object eval(CelVariableResolver resolver, CelFunctionResolver lateBoundFunctionResolver) + throws CelEvaluationException; + + /** Evaluate a compiled program with unknown attribute patterns {@code partialVars}. */ + Object eval(PartialVars partialVars) throws CelEvaluationException; +} diff --git a/runtime/src/main/java/dev/cel/runtime/ProgramImpl.java b/runtime/src/main/java/dev/cel/runtime/ProgramImpl.java new file mode 100644 index 000000000..2543a9525 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/ProgramImpl.java @@ -0,0 +1,201 @@ +// 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.common.base.Preconditions; +import com.google.errorprone.annotations.Immutable; +import com.google.protobuf.Message; +import dev.cel.common.CelOptions; +import dev.cel.runtime.CelRuntime.Program; +import java.util.Map; +import java.util.Optional; + +/** Internal implementation of a {@link CelRuntime.Program} */ +@AutoValue +@Immutable +abstract class ProgramImpl implements CelRuntime.Program { + + @Override + public Object eval() throws CelEvaluationException { + return evalInternal(Activation.EMPTY); + } + + @Override + public Object eval(Map mapValue) throws CelEvaluationException { + return evalInternal(Activation.copyOf(mapValue)); + } + + @Override + public Object eval(Message message) throws CelEvaluationException { + return evalInternal(ProtoMessageActivationFactory.fromProto(message, getOptions())); + } + + @Override + public Object eval(CelVariableResolver resolver) throws CelEvaluationException { + return evalInternal((name) -> resolver.find(name).orElse(null)); + } + + @Override + public Object eval(CelVariableResolver resolver, CelFunctionResolver lateBoundFunctionResolver) + throws CelEvaluationException { + return evalInternal((name) -> resolver.find(name).orElse(null), lateBoundFunctionResolver); + } + + @Override + public Object eval(Map mapValue, CelFunctionResolver lateBoundFunctionResolver) + throws CelEvaluationException { + return evalInternal(Activation.copyOf(mapValue), lateBoundFunctionResolver); + } + + @Override + public Object eval(PartialVars partialVars) throws CelEvaluationException { + return evalInternal( + UnknownContext.create(partialVars.resolver(), partialVars.unknowns()), + /* lateBoundFunctionResolver= */ Optional.empty(), + /* listener= */ Optional.empty()); + } + + @Override + public Object trace(CelEvaluationListener listener) throws CelEvaluationException { + return evalInternal(Activation.EMPTY, listener); + } + + @Override + public Object trace(Map mapValue, CelEvaluationListener listener) + throws CelEvaluationException { + return evalInternal(Activation.copyOf(mapValue), listener); + } + + @Override + public Object trace(Message message, CelEvaluationListener listener) + throws CelEvaluationException { + return evalInternal(ProtoMessageActivationFactory.fromProto(message, getOptions()), listener); + } + + @Override + public Object trace(CelVariableResolver resolver, CelEvaluationListener listener) + throws CelEvaluationException { + return evalInternal((name) -> resolver.find(name).orElse(null), listener); + } + + @Override + public Object trace( + CelVariableResolver resolver, + CelFunctionResolver lateBoundFunctionResolver, + CelEvaluationListener listener) + throws CelEvaluationException { + return evalInternal( + (name) -> resolver.find(name).orElse(null), lateBoundFunctionResolver, listener); + } + + @Override + public Object trace( + Map mapValue, + CelFunctionResolver lateBoundFunctionResolver, + CelEvaluationListener listener) + throws CelEvaluationException { + 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()); + } + + private Object evalInternal(GlobalResolver resolver) throws CelEvaluationException { + return evalInternal(UnknownContext.create(resolver), Optional.empty(), Optional.empty()); + } + + private Object evalInternal(GlobalResolver resolver, CelEvaluationListener listener) + throws CelEvaluationException { + return evalInternal(UnknownContext.create(resolver), Optional.empty(), Optional.of(listener)); + } + + private Object evalInternal(GlobalResolver resolver, CelFunctionResolver functionResolver) + throws CelEvaluationException { + return evalInternal( + UnknownContext.create(resolver), Optional.of(functionResolver), Optional.empty()); + } + + private Object evalInternal( + GlobalResolver resolver, + CelFunctionResolver lateBoundFunctionResolver, + CelEvaluationListener listener) + throws CelEvaluationException { + return evalInternal( + UnknownContext.create(resolver), + Optional.of(lateBoundFunctionResolver), + Optional.of(listener)); + } + + /** + * Evaluate an expr node with an UnknownContext (an activation annotated with which attributes are + * unknown). + */ + private Object evalInternal( + UnknownContext context, + Optional lateBoundFunctionResolver, + Optional listener) + throws CelEvaluationException { + Interpretable impl = getInterpretable(); + if (getOptions().enableUnknownTracking()) { + Preconditions.checkState( + impl instanceof UnknownTrackingInterpretable, + "Environment misconfigured. Requested unknown tracking without a compatible" + + " implementation."); + + UnknownTrackingInterpretable interpreter = (UnknownTrackingInterpretable) impl; + return interpreter.evalTrackingUnknowns( + RuntimeUnknownResolver.builder() + .setResolver(context.variableResolver()) + .setAttributeResolver(context.createAttributeResolver()) + .build(), + lateBoundFunctionResolver, + listener); + } else { + if (lateBoundFunctionResolver.isPresent() && listener.isPresent()) { + return impl.eval( + context.variableResolver(), lateBoundFunctionResolver.get(), listener.get()); + } else if (lateBoundFunctionResolver.isPresent()) { + return impl.eval(context.variableResolver(), lateBoundFunctionResolver.get()); + } else if (listener.isPresent()) { + return impl.eval(context.variableResolver(), listener.get()); + } + + return impl.eval(context.variableResolver()); + } + } + + /** Get the underlying {@link Interpretable} for the {@code Program}. */ + abstract Interpretable getInterpretable(); + + /** Get the {@code CelOptions} configured for this program. */ + abstract CelOptions getOptions(); + + /** Instantiate a new {@code Program} from the input {@code interpretable}. */ + static Program from(Interpretable interpretable, CelOptions options) { + return new AutoValue_ProgramImpl(interpretable, options); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/ProtoMessageActivationFactory.java b/runtime/src/main/java/dev/cel/runtime/ProtoMessageActivationFactory.java new file mode 100644 index 000000000..fb42a1964 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/ProtoMessageActivationFactory.java @@ -0,0 +1,74 @@ +// 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.protobuf.Descriptors.FieldDescriptor; +import com.google.protobuf.Message; +import dev.cel.common.CelOptions; +import dev.cel.common.internal.DefaultMessageFactory; +import dev.cel.common.internal.DynamicProto; +import dev.cel.common.internal.ProtoAdapter; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** Package-private factory to facilitate binding a full protobuf message into an activation. */ +final class ProtoMessageActivationFactory { + + /** + * Creates an {@code Activation} from a {@code Message} where each field in the message is exposed + * as a top-level variable in the {@code Activation}. + * + *

Unset message fields are published with the default value for the field type. However, an + * unset {@code google.protobuf.Any} value is not a valid CEL value, and will be published as an + * {@code Exception} value on the {@code Activation} just as though an unset {@code Any} would if + * it were accessed during a CEL evaluation. + */ + public static Activation fromProto(Message message, CelOptions celOptions) { + Map variables = new HashMap<>(); + Map msgFieldValues = message.getAllFields(); + + ProtoAdapter protoAdapter = + new ProtoAdapter(DynamicProto.create(DefaultMessageFactory.INSTANCE), celOptions); + + boolean skipUnsetFields = + celOptions.fromProtoUnsetFieldOption().equals(CelOptions.ProtoUnsetFieldOptions.SKIP); + + for (FieldDescriptor field : message.getDescriptorForType().getFields()) { + // If skipping unset fields and the field is not repeated, then continue. + if (skipUnsetFields && !field.isRepeated() && !msgFieldValues.containsKey(field)) { + continue; + } + + // Get the value of the field set on the message, if present, otherwise use reflection to + // get the default value for the field using the FieldDescriptor. + Object fieldValue = msgFieldValues.getOrDefault(field, message.getField(field)); + try { + Optional adapted = protoAdapter.adaptFieldToValue(field, fieldValue); + variables.put(field.getName(), adapted.orElse(null)); + } catch (IllegalArgumentException e) { + variables.put( + field.getName(), + CelEvaluationExceptionBuilder.newBuilder( + "illegal field value. field=%s, value=%s", field.getName(), fieldValue) + .setCause(e) + .build()); + } + } + return Activation.copyOf(variables); + } + + private ProtoMessageActivationFactory() {} +} diff --git a/runtime/src/main/java/dev/cel/runtime/ProtoMessageRuntimeEquality.java b/runtime/src/main/java/dev/cel/runtime/ProtoMessageRuntimeEquality.java new file mode 100644 index 000000000..25a090081 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/ProtoMessageRuntimeEquality.java @@ -0,0 +1,70 @@ +// 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.errorprone.annotations.Immutable; +import com.google.protobuf.Message; +import dev.cel.common.CelOptions; +import dev.cel.common.annotations.Internal; +import dev.cel.common.internal.DynamicProto; +import dev.cel.common.internal.ProtoEquality; +import java.util.Objects; + +/** + * ProtoMessageRuntimeEquality contains methods for performing CEL related equality checks, + * including full protobuf messages by leveraging descriptors. + * + *

CEL Library Internals. Do Not Use. + */ +@Internal +@Immutable +public final class ProtoMessageRuntimeEquality extends RuntimeEquality { + + private final ProtoEquality protoEquality; + + @Internal + public static ProtoMessageRuntimeEquality create( + DynamicProto dynamicProto, CelOptions celOptions) { + return new ProtoMessageRuntimeEquality(dynamicProto, celOptions); + } + + @Override + public boolean objectEquals(Object x, Object y) { + if (celOptions.disableCelStandardEquality()) { + return Objects.equals(x, y); + } + if (x == y) { + return true; + } + + if (celOptions.enableProtoDifferencerEquality()) { + x = runtimeHelpers.adaptValue(x); + y = runtimeHelpers.adaptValue(y); + if (x instanceof Message) { + if (!(y instanceof Message)) { + return false; + } + return protoEquality.equals((Message) x, (Message) y); + } + } + + return super.objectEquals(x, y); + } + + private ProtoMessageRuntimeEquality(DynamicProto dynamicProto, CelOptions celOptions) { + super(ProtoMessageRuntimeHelpers.create(dynamicProto, celOptions), celOptions); + this.protoEquality = new ProtoEquality(dynamicProto); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/ProtoMessageRuntimeHelpers.java b/runtime/src/main/java/dev/cel/runtime/ProtoMessageRuntimeHelpers.java new file mode 100644 index 000000000..85acd373c --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/ProtoMessageRuntimeHelpers.java @@ -0,0 +1,65 @@ +// 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.protobuf.Message; +import com.google.protobuf.MessageLiteOrBuilder; +import com.google.protobuf.MessageOrBuilder; +import dev.cel.common.CelOptions; +import dev.cel.common.annotations.Internal; +import dev.cel.common.internal.DynamicProto; +import dev.cel.common.internal.ProtoAdapter; + +/** + * Helper methods for common CEL related routines that require a full protobuf dependency. + * + *

CEL Library Internals. Do Not Use. + */ +@Internal +public final class ProtoMessageRuntimeHelpers extends RuntimeHelpers { + + private final ProtoAdapter protoAdapter; + + @Internal + public static ProtoMessageRuntimeHelpers create( + DynamicProto dynamicProto, CelOptions celOptions) { + return new ProtoMessageRuntimeHelpers(new ProtoAdapter(dynamicProto, celOptions)); + } + + /** + * Adapts a {@code protobuf.Message} to a plain old Java object. + * + *

Well-known protobuf types (wrappers, JSON types) are unwrapped to Java native object + * representations. + * + *

If the incoming {@code obj} is of type {@code google.protobuf.Any} the object is unpacked + * and the proto within is passed to the {@code adaptProtoToValue} method again to ensure the + * message contained within the Any is properly unwrapped if it is a well-known protobuf type. + */ + @Override + Object adaptProtoToValue(MessageLiteOrBuilder obj) { + if (obj instanceof Message) { + return protoAdapter.adaptProtoToValue((MessageOrBuilder) obj); + } + if (obj instanceof Message.Builder) { + return protoAdapter.adaptProtoToValue(((Message.Builder) obj).build()); + } + return obj; + } + + private ProtoMessageRuntimeHelpers(ProtoAdapter protoAdapter) { + this.protoAdapter = protoAdapter; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/Registrar.java b/runtime/src/main/java/dev/cel/runtime/Registrar.java index 92eb8b31c..467a7c426 100644 --- a/runtime/src/main/java/dev/cel/runtime/Registrar.java +++ b/runtime/src/main/java/dev/cel/runtime/Registrar.java @@ -14,46 +14,35 @@ package dev.cel.runtime; -import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.Immutable; -import dev.cel.common.annotations.Internal; import java.util.List; /** * An object which registers the functions that a {@link Dispatcher} calls. * - *

CEL Library Internals. Do Not Use. + * @deprecated Do not use. This interface exists solely for legacy async stack compatibility + * reasons. */ -@Internal +@Deprecated public interface Registrar { - /** Interface to a general function. */ + /** Interface describing the general signature of all CEL custom function implementations. */ @Immutable - @FunctionalInterface - interface Function { - @CanIgnoreReturnValue - Object apply(Object[] args) throws InterpreterException; - } + interface Function extends CelFunctionOverload {} /** - * Interface to a typed unary function without activation argument. Convenience for the {@code - * add} methods. + * Helper interface for describing unary functions where the type-parameter is used to improve + * compile-time correctness of function bindings. */ @Immutable - @FunctionalInterface - interface UnaryFunction { - Object apply(T arg) throws InterpreterException; - } + interface UnaryFunction extends CelFunctionOverload.Unary {} /** - * Interface to a typed binary function without activation argument. Convenience for the {@code - * add} methods. + * Helper interface for describing binary functions where the type parameters are used to improve + * compile-time correctness of function bindings. */ @Immutable - @FunctionalInterface - interface BinaryFunction { - Object apply(T1 arg1, T2 arg2) throws InterpreterException; - } + interface BinaryFunction extends CelFunctionOverload.Binary {} /** Adds a unary function to the dispatcher. */ void add(String overloadId, Class argType, UnaryFunction function); diff --git a/runtime/src/main/java/dev/cel/runtime/RuntimeEquality.java b/runtime/src/main/java/dev/cel/runtime/RuntimeEquality.java index 517fb1729..56a8761cd 100644 --- a/runtime/src/main/java/dev/cel/runtime/RuntimeEquality.java +++ b/runtime/src/main/java/dev/cel/runtime/RuntimeEquality.java @@ -16,74 +16,72 @@ import com.google.common.primitives.UnsignedLong; import com.google.errorprone.annotations.Immutable; -import com.google.protobuf.Message; -import com.google.protobuf.MessageOrBuilder; -import dev.cel.common.CelErrorCode; +import com.google.protobuf.MessageLiteOrBuilder; import dev.cel.common.CelOptions; -import dev.cel.common.CelRuntimeException; import dev.cel.common.annotations.Internal; +import dev.cel.common.exceptions.CelAttributeNotFoundException; import dev.cel.common.internal.ComparisonFunctions; -import dev.cel.common.internal.DynamicProto; -import dev.cel.common.internal.ProtoEquality; 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 java.util.Set; -/** CEL Library Internals. Do Not Use. */ -@Internal +/** RuntimeEquality contains methods for performing CEL related equality checks. */ @Immutable -public final class RuntimeEquality { - - private final DynamicProto dynamicProto; - private final ProtoEquality protoEquality; +@Internal +public class RuntimeEquality { + protected final RuntimeHelpers runtimeHelpers; + protected final CelOptions celOptions; - public RuntimeEquality(DynamicProto dynamicProto) { - this.dynamicProto = dynamicProto; - this.protoEquality = new ProtoEquality(dynamicProto); + public static RuntimeEquality create(RuntimeHelpers runtimeHelper, CelOptions celOptions) { + return new RuntimeEquality(runtimeHelper, celOptions); } // Functions // ========= /** Determine whether the {@code list} contains the given {@code value}. */ - public boolean inList(List list, A value, CelOptions celOptions) { + public boolean inList(List list, A value) { if (list.contains(value)) { return true; } if (value instanceof Number) { - // Ensure that list elements are properly unwrapped and compared for equality. - return list.stream().anyMatch(elem -> objectEquals(elem, value, celOptions)); + for (A elem : list) { + if (objectEquals(elem, value)) { + return true; + } + } } return false; } /** Bound-checked indexing of maps. */ @SuppressWarnings("unchecked") - public B indexMap(Map map, A index, CelOptions celOptions) { - Optional value = findInMap(map, index, celOptions); + public B indexMap(Map map, A index) { + Optional value = findInMap(map, index); // Use this method rather than the standard 'orElseThrow' method because of the unchecked cast. if (value.isPresent()) { return (B) value.get(); } - throw new CelRuntimeException( - new IndexOutOfBoundsException(index.toString()), CelErrorCode.ATTRIBUTE_NOT_FOUND); + + throw CelAttributeNotFoundException.of(index.toString()); } /** Determine whether the {@code map} contains the given {@code key}. */ - public boolean inMap(Map map, A key, CelOptions celOptions) { - return findInMap(map, key, celOptions).isPresent(); + public boolean inMap(Map map, A key) { + return findInMap(map, key).isPresent(); } - public Optional findInMap(Map map, Object index, CelOptions celOptions) { + public Optional findInMap(Map map, Object index) { if (celOptions.disableCelStandardEquality()) { return Optional.ofNullable(map.get(index)); } - if (index instanceof MessageOrBuilder) { - index = RuntimeHelpers.adaptProtoToValue(dynamicProto, (MessageOrBuilder) index, celOptions); + if (index instanceof MessageLiteOrBuilder) { + index = runtimeHelpers.adaptProtoToValue((MessageLiteOrBuilder) index); } Object v = map.get(index); if (v != null) { @@ -135,23 +133,18 @@ public Optional findInMap(Map map, Object index, CelOptions celOpt * *

Heterogeneous equality differs from homogeneous equality in that two objects may be * comparable even if they are not of the same type, where type differences are usually trivially - * false. Heterogeneous runtime equality is under consideration in b/71516544. - * - *

Note, uint values are problematic in that they cannot be properly type-tested for equality - * in comparisons with 64-int signed integer values, see b/159183198. This problem only affects - * Java and is typically inconsequential due to the requirement for type-checking expressions - * before they are evaluated. + * false. */ @SuppressWarnings({"rawtypes", "unchecked"}) - public boolean objectEquals(Object x, Object y, CelOptions celOptions) { + public boolean objectEquals(Object x, Object y) { if (celOptions.disableCelStandardEquality()) { return Objects.equals(x, y); } if (x == y) { return true; } - x = RuntimeHelpers.adaptValue(dynamicProto, x, celOptions); - y = RuntimeHelpers.adaptValue(dynamicProto, y, celOptions); + x = runtimeHelpers.adaptValue(x); + y = runtimeHelpers.adaptValue(y); if (x instanceof Number) { if (!(y instanceof Number)) { return false; @@ -159,11 +152,13 @@ public boolean objectEquals(Object x, Object y, CelOptions celOptions) { return ComparisonFunctions.numericEquals((Number) x, (Number) y); } if (celOptions.enableProtoDifferencerEquality()) { - if (x instanceof Message) { - if (!(y instanceof Message)) { + if (x instanceof MessageLiteOrBuilder) { + if (!(y instanceof MessageLiteOrBuilder)) { return false; } - return protoEquality.equals((Message) x, (Message) y); + // TODO: Implement when CelLiteDescriptor is available + throw new UnsupportedOperationException( + "Proto Differencer equality is not supported for MessageLite."); } } if (x instanceof Iterable) { @@ -179,7 +174,7 @@ public boolean objectEquals(Object x, Object y, CelOptions celOptions) { return false; } try { - if (!objectEquals(xElem, yElems.next(), celOptions)) { + if (!objectEquals(xElem, yElems.next())) { return false; } } catch (IllegalArgumentException iae) { @@ -204,15 +199,15 @@ public boolean objectEquals(Object x, Object y, CelOptions celOptions) { return false; } IllegalArgumentException e = null; - Set entrySet = xMap.entrySet(); + Set entrySet = xMap.entrySet(); for (Map.Entry xEntry : entrySet) { - Optional yVal = findInMap(yMap, xEntry.getKey(), celOptions); + Optional yVal = findInMap(yMap, xEntry.getKey()); // Use isPresent() rather than isEmpty() to stay backwards compatible with Java 8. if (!yVal.isPresent()) { return false; } try { - if (!objectEquals(xEntry.getValue(), yVal.get(), celOptions)) { + if (!objectEquals(xEntry.getValue(), yVal.get())) { return false; } } catch (IllegalArgumentException iae) { @@ -227,6 +222,41 @@ public boolean objectEquals(Object x, Object y, CelOptions celOptions) { return Objects.equals(x, y); } + /** + * Returns the hash code consistent with the {@link #objectEquals(Object, Object)} method. For + * example, {@code hashCode(1) == hashCode(1.0)} since {@code objectEquals(1, 1.0)} is true. + */ + public int hashCode(Object object) { + if (object == null) { + return 0; + } + + if (celOptions.disableCelStandardEquality()) { + return Objects.hashCode(object); + } + + object = runtimeHelpers.adaptValue(object); + if (object instanceof Number) { + return Double.hashCode(((Number) object).doubleValue()); + } + if (object instanceof Iterable) { + int h = 1; + Iterable iter = (Iterable) object; + for (Object elem : iter) { + h = h * 31 + hashCode(elem); + } + return h; + } + if (object instanceof Map) { + int h = 0; + for (Map.Entry entry : ((Map) object).entrySet()) { + h += hashCode(entry.getKey()) ^ hashCode(entry.getValue()); + } + return h; + } + return Objects.hashCode(object); + } + private static Optional doubleToUnsignedLossless(Number v) { Optional conv = RuntimeHelpers.doubleToUnsignedChecked(v.doubleValue()); return conv.map(ul -> ul.longValue() == v.doubleValue() ? ul : null); @@ -245,4 +275,9 @@ private static Optional unsignedToLongLossless(UnsignedLong v) { } return Optional.empty(); } + + RuntimeEquality(RuntimeHelpers runtimeHelpers, CelOptions celOptions) { + this.runtimeHelpers = runtimeHelpers; + this.celOptions = celOptions; + } } diff --git a/runtime/src/main/java/dev/cel/runtime/RuntimeHelpers.java b/runtime/src/main/java/dev/cel/runtime/RuntimeHelpers.java index ffb979842..0ee7824b7 100644 --- a/runtime/src/main/java/dev/cel/runtime/RuntimeHelpers.java +++ b/runtime/src/main/java/dev/cel/runtime/RuntimeHelpers.java @@ -17,21 +17,19 @@ import static com.google.common.base.Preconditions.checkArgument; import com.google.common.primitives.Ints; -import com.google.common.primitives.UnsignedInts; import com.google.common.primitives.UnsignedLong; import com.google.common.primitives.UnsignedLongs; +import com.google.errorprone.annotations.Immutable; import com.google.protobuf.Duration; -import com.google.protobuf.Message; -import com.google.protobuf.MessageOrBuilder; -import com.google.protobuf.NullValue; +import com.google.protobuf.MessageLiteOrBuilder; import com.google.re2j.Pattern; -import dev.cel.common.CelErrorCode; import dev.cel.common.CelOptions; -import dev.cel.common.CelRuntimeException; import dev.cel.common.annotations.Internal; +import dev.cel.common.exceptions.CelDivideByZeroException; +import dev.cel.common.exceptions.CelIndexOutOfBoundsException; +import dev.cel.common.exceptions.CelNumericOverflowException; import dev.cel.common.internal.Converter; -import dev.cel.common.internal.DynamicProto; -import dev.cel.common.internal.ProtoAdapter; +import dev.cel.common.values.NullValue; import java.time.format.DateTimeParseException; import java.util.ArrayList; import java.util.List; @@ -43,53 +41,68 @@ * *

CEL Library Internals. Do Not Use. */ +@Immutable @Internal -public final class RuntimeHelpers { +public class RuntimeHelpers { // Maximum and minimum range supported by protobuf Duration values. private static final java.time.Duration DURATION_MAX = java.time.Duration.ofDays(3652500); private static final java.time.Duration DURATION_MIN = DURATION_MAX.negated(); + public static RuntimeHelpers create() { + return new RuntimeHelpers(); + } + // Functions // ========= - /** Convert a string to a Duration. */ + /** Convert a string to a Protobuf Duration. */ public static Duration createDurationFromString(String d) { + java.time.Duration dv = createJavaDurationFromString(d); + return Duration.newBuilder().setSeconds(dv.getSeconds()).setNanos(dv.getNano()).build(); + } + + /** Convert a string to a native Java Duration. */ + public static java.time.Duration createJavaDurationFromString(String d) { try { java.time.Duration dv = AmountFormats.parseUnitBasedDuration(d); // Ensure that the duration value can be adequately represented within a protobuf.Duration. checkArgument( dv.compareTo(DURATION_MAX) <= 0 && dv.compareTo(DURATION_MIN) >= 0, "invalid duration range"); - return Duration.newBuilder().setSeconds(dv.getSeconds()).setNanos(dv.getNano()).build(); + return dv; } catch (DateTimeParseException e) { throw new IllegalArgumentException("invalid duration format", e); } } - /** Match a string against a regular expression. */ - public static boolean matches(String string, String regexp) { - return matches( - string, regexp, CelOptions.newBuilder().disableCelStandardEquality(false).build()); - } - public static boolean matches(String string, String regexp, CelOptions celOptions) { + Pattern pattern = Pattern.compile(regexp); + int maxProgramSize = celOptions.maxRegexProgramSize(); + if (maxProgramSize >= 0 && pattern.programSize() > maxProgramSize) { + throw new IllegalArgumentException( + String.format( + "Regex pattern exceeds allowed program size. Allowed: %d, Provided: %d", + maxProgramSize, pattern.programSize())); + } + if (!celOptions.enableRegexPartialMatch()) { // Uses re2 for consistency across languages. - return Pattern.matches(regexp, string); + return pattern.matcher(string).matches(); } - // Return an unanchored match for the presence of the regexp anywher in the string. - return Pattern.compile(regexp).matcher(string).find(); - } - /** Create a compiled pattern for the given regular expression. */ - public static Pattern compilePattern(String regexp) { - return Pattern.compile(regexp); + // Return an unanchored match for the presence of the regexp anywhere in the string. + return pattern.matcher(string).find(); } /** Concatenates two lists into a new list. */ public static List concat(List first, List second) { - // TODO: return a view instead of an actual copy. + if (first instanceof ConcatenatedListView) { + // Comprehensions use a more efficient list view for performing O(1) concatenation + first.addAll(second); + return first; + } + List result = new ArrayList<>(first.size() + second.size()); result.addAll(first); result.addAll(second); @@ -104,17 +117,11 @@ public static A indexList(List list, Number index) { if (index instanceof Double) { return doubleToLongLossless(index.doubleValue()) .map(v -> indexList(list, v)) - .orElseThrow( - () -> - new CelRuntimeException( - new IndexOutOfBoundsException("Index out of bounds: " + index.doubleValue()), - CelErrorCode.INDEX_OUT_OF_BOUNDS)); + .orElseThrow(() -> new CelIndexOutOfBoundsException(index.doubleValue())); } int castIndex = Ints.checkedCast(index.longValue()); if (castIndex < 0 || castIndex >= list.size()) { - throw new CelRuntimeException( - new IndexOutOfBoundsException("Index out of bounds: " + castIndex), - CelErrorCode.INDEX_OUT_OF_BOUNDS); + throw new CelIndexOutOfBoundsException(castIndex); } return list.get(castIndex); } @@ -133,7 +140,7 @@ public static long int64Add(long x, long y, CelOptions celOptions) { public static long int64Divide(long x, long y, CelOptions celOptions) { if (celOptions.errorOnIntWrap() && x == Long.MIN_VALUE && y == -1) { - throw new ArithmeticException("most negative number wraps"); + throw new CelNumericOverflowException("most negative number wraps"); } return x / y; } @@ -174,13 +181,13 @@ public static long uint64Add(long x, long y, CelOptions celOptions) { if (celOptions.errorOnIntWrap()) { if (x < 0 && y < 0) { // Both numbers are in the upper half of the range, so it must overflow. - throw new ArithmeticException("range overflow on unsigned addition"); + throw new CelNumericOverflowException("range overflow on unsigned addition"); } long z = x + y; if ((x < 0 || y < 0) && z >= 0) { // Only one number is in the upper half of the range. It overflows if the result // is not in the upper half. - throw new ArithmeticException("range overflow on unsigned addition"); + throw new CelNumericOverflowException("range overflow on unsigned addition"); } return z; } @@ -189,7 +196,7 @@ public static long uint64Add(long x, long y, CelOptions celOptions) { public static UnsignedLong uint64Add(UnsignedLong x, UnsignedLong y) { if (x.compareTo(UnsignedLong.MAX_VALUE.minus(y)) > 0) { - throw new ArithmeticException("range overflow on unsigned addition"); + throw new CelNumericOverflowException("range overflow on unsigned addition"); } return x.plus(y); } @@ -200,7 +207,7 @@ public static int uint64CompareTo(long x, long y, CelOptions celOptions) { : UnsignedLong.valueOf(x).compareTo(UnsignedLong.valueOf(y)); } - public static int uint64CompareTo(long x, long y) { + static int uint64CompareTo(long x, long y) { // Features is set to empty, as this class is public and the build visibility is public. // Existing callers expect legacy behavior. return uint64CompareTo(x, y, CelOptions.LEGACY); @@ -216,11 +223,11 @@ public static long uint64Divide(long x, long y, CelOptions celOptions) { ? UnsignedLongs.divide(x, y) : UnsignedLong.valueOf(x).dividedBy(UnsignedLong.valueOf(y)).longValue(); } catch (ArithmeticException e) { - throw new CelRuntimeException(e, CelErrorCode.DIVIDE_BY_ZERO); + throw new CelDivideByZeroException(e); } } - public static long uint64Divide(long x, long y) { + static long uint64Divide(long x, long y) { // Features is set to empty, as this class is public and the build visibility is public. // Existing callers expect legacy behavior. return uint64Divide(x, y, CelOptions.LEGACY); @@ -228,8 +235,7 @@ public static long uint64Divide(long x, long y) { public static UnsignedLong uint64Divide(UnsignedLong x, UnsignedLong y) { if (y.equals(UnsignedLong.ZERO)) { - throw new CelRuntimeException( - new ArithmeticException("/ by zero"), CelErrorCode.DIVIDE_BY_ZERO); + throw new CelDivideByZeroException(); } return x.dividedBy(y); } @@ -240,19 +246,18 @@ public static long uint64Mod(long x, long y, CelOptions celOptions) { ? UnsignedLongs.remainder(x, y) : UnsignedLong.valueOf(x).mod(UnsignedLong.valueOf(y)).longValue(); } catch (ArithmeticException e) { - throw new CelRuntimeException(e, CelErrorCode.DIVIDE_BY_ZERO); + throw new CelDivideByZeroException(e); } } public static UnsignedLong uint64Mod(UnsignedLong x, UnsignedLong y) { if (y.equals(UnsignedLong.ZERO)) { - throw new CelRuntimeException( - new ArithmeticException("/ by zero"), CelErrorCode.DIVIDE_BY_ZERO); + throw new CelDivideByZeroException(); } return x.mod(y); } - public static long uint64Mod(long x, long y) { + static long uint64Mod(long x, long y) { // Features is set to empty, as this class is public and the build visibility is public. // Existing callers expect legacy behavior. return uint64Mod(x, y, CelOptions.LEGACY); @@ -264,12 +269,12 @@ public static long uint64Multiply(long x, long y, CelOptions celOptions) { ? x * y : UnsignedLong.valueOf(x).times(UnsignedLong.valueOf(y)).longValue(); if (celOptions.errorOnIntWrap() && y != 0 && Long.divideUnsigned(z, y) != x) { - throw new ArithmeticException("multiply out of unsigned integer range"); + throw new CelNumericOverflowException("multiply out of unsigned integer range"); } return z; } - public static long uint64Multiply(long x, long y) { + static long uint64Multiply(long x, long y) { // Features is set to empty, as this class is public and the build visibility is public. // Existing callers expect legacy behavior. return uint64Multiply(x, y, CelOptions.LEGACY); @@ -277,7 +282,7 @@ public static long uint64Multiply(long x, long y) { public static UnsignedLong uint64Multiply(UnsignedLong x, UnsignedLong y) { if (!y.equals(UnsignedLong.ZERO) && x.compareTo(UnsignedLong.MAX_VALUE.dividedBy(y)) > 0) { - throw new ArithmeticException("multiply out of unsigned integer range"); + throw new CelNumericOverflowException("multiply out of unsigned integer range"); } return x.times(y); } @@ -287,7 +292,7 @@ public static long uint64Subtract(long x, long y, CelOptions celOptions) { // Throw an overflow error if x < y, as unsigned longs. This happens if y has its high // bit set and x does not, or if they have the same high bit and x < y as signed longs. if ((x < 0 && y < 0 && x < y) || (x >= 0 && y >= 0 && x < y) || (x >= 0 && y < 0)) { - throw new ArithmeticException("unsigned subtraction underflow"); + throw new CelNumericOverflowException("unsigned subtraction underflow"); } // fallthrough } @@ -298,14 +303,11 @@ public static UnsignedLong uint64Subtract(UnsignedLong x, UnsignedLong y) { // Throw an overflow error if x < y, as unsigned longs. This happens if y has its high // bit set and x does not, or if they have the same high bit and x < y as signed longs. if (x.compareTo(y) < 0) { - throw new ArithmeticException("unsigned subtraction underflow"); + throw new CelNumericOverflowException("unsigned subtraction underflow"); } return x.minus(y); } - // Object equality - // =================== - // Proto Type Adaption // =================== @@ -314,36 +316,34 @@ public static UnsignedLong uint64Subtract(UnsignedLong x, UnsignedLong y) { // want to avoid to do this conversion eagerly, so we create views on the underlying data. // The below code is the extensive boilerplate to do so. - public static Converter identity() { + static Converter identity() { return (A value) -> value; } - public static final Converter INT32_TO_INT64 = Integer::longValue; - - public static final Converter UINT32_TO_UINT64 = UnsignedInts::toLong; + static final Converter INT32_TO_INT64 = Integer::longValue; - public static final Converter FLOAT_TO_DOUBLE = Float::doubleValue; + static final Converter FLOAT_TO_DOUBLE = Float::doubleValue; - public static final Converter INT64_TO_INT32 = Ints::checkedCast; + static final Converter INT64_TO_INT32 = Ints::checkedCast; - public static final Converter DOUBLE_TO_FLOAT = Double::floatValue; + static final Converter DOUBLE_TO_FLOAT = Double::floatValue; /** Adapts a plain old Java object into a CEL value. */ - public static Object adaptValue(DynamicProto dynamicProto, Object value, CelOptions celOptions) { - if (value == null) { + public Object adaptValue(Object value) { + if (value == null || value.equals(com.google.protobuf.NullValue.NULL_VALUE)) { return NullValue.NULL_VALUE; } if (value instanceof Number) { return maybeAdaptPrimitive(value); } - if (value instanceof MessageOrBuilder) { - return adaptProtoToValue(dynamicProto, (MessageOrBuilder) value, celOptions); + if (value instanceof MessageLiteOrBuilder) { + return adaptProtoToValue((MessageLiteOrBuilder) value); } return value; } /** Adapts a {@code Number} value to its appropriate CEL type. */ - public static Object maybeAdaptPrimitive(Object value) { + static Object maybeAdaptPrimitive(Object value) { if (value instanceof Optional) { Optional optionalVal = (Optional) value; if (!optionalVal.isPresent()) { @@ -370,16 +370,8 @@ public static Object maybeAdaptPrimitive(Object value) { * and the proto within is passed to the {@code adaptProtoToValue} method again to ensure the * message contained within the Any is properly unwrapped if it is a well-known protobuf type. */ - public static Object adaptProtoToValue( - DynamicProto dynamicProto, MessageOrBuilder obj, CelOptions celOptions) { - ProtoAdapter protoAdapter = new ProtoAdapter(dynamicProto, celOptions.enableUnsignedLongs()); - if (obj instanceof Message) { - return protoAdapter.adaptProtoToValue(obj); - } - if (obj instanceof Message.Builder) { - return protoAdapter.adaptProtoToValue(((Message.Builder) obj).build()); - } - return obj; + Object adaptProtoToValue(MessageLiteOrBuilder obj) { + throw new UnsupportedOperationException("Not implemented yet"); } public static Optional doubleToUnsignedChecked(double v) { @@ -405,10 +397,10 @@ public static Optional doubleToLongChecked(double v) { return Optional.of((long) v); } - public static Optional doubleToLongLossless(Number v) { + static Optional doubleToLongLossless(Number v) { Optional conv = doubleToLongChecked(v.doubleValue()); return conv.map(l -> l.doubleValue() == v.doubleValue() ? l : null); } - private RuntimeHelpers() {} + RuntimeHelpers() {} } diff --git a/runtime/src/main/java/dev/cel/runtime/RuntimeTypeProvider.java b/runtime/src/main/java/dev/cel/runtime/RuntimeTypeProvider.java index 7df4e26ad..97e4f3af1 100644 --- a/runtime/src/main/java/dev/cel/runtime/RuntimeTypeProvider.java +++ b/runtime/src/main/java/dev/cel/runtime/RuntimeTypeProvider.java @@ -25,4 +25,4 @@ */ @Immutable @Internal -public interface RuntimeTypeProvider extends MessageProvider, TypeResolver {} +public interface RuntimeTypeProvider extends MessageProvider {} diff --git a/runtime/src/main/java/dev/cel/runtime/RuntimeTypeProviderLegacyImpl.java b/runtime/src/main/java/dev/cel/runtime/RuntimeTypeProviderLegacyImpl.java deleted file mode 100644 index 841d702a2..000000000 --- a/runtime/src/main/java/dev/cel/runtime/RuntimeTypeProviderLegacyImpl.java +++ /dev/null @@ -1,130 +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 dev.cel.expr.Type; -import dev.cel.expr.Value; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Preconditions; -import com.google.errorprone.annotations.Immutable; -import dev.cel.common.CelOptions; -import dev.cel.common.annotations.Internal; -import dev.cel.common.internal.CelDescriptorPool; -import dev.cel.common.internal.DynamicProto; -import dev.cel.common.types.CelType; -import dev.cel.common.types.TypeType; -import dev.cel.common.values.CelValue; -import dev.cel.common.values.CelValueProvider; -import dev.cel.common.values.ProtoCelValueConverter; -import dev.cel.common.values.SelectableValue; -import dev.cel.common.values.StringValue; -import java.util.Map; -import java.util.NoSuchElementException; -import org.jspecify.nullness.Nullable; - -/** Bridge between the old RuntimeTypeProvider and CelValueProvider APIs. */ -@Internal -@Immutable -public final class RuntimeTypeProviderLegacyImpl implements RuntimeTypeProvider { - - private final CelValueProvider valueProvider; - private final ProtoCelValueConverter protoCelValueConverter; - private final TypeResolver standardTypeResolver; - - @VisibleForTesting - public RuntimeTypeProviderLegacyImpl( - CelOptions celOptions, - CelValueProvider valueProvider, - CelDescriptorPool celDescriptorPool, - DynamicProto dynamicProto) { - this.valueProvider = valueProvider; - this.protoCelValueConverter = - ProtoCelValueConverter.newInstance(celOptions, celDescriptorPool, dynamicProto); - this.standardTypeResolver = StandardTypeResolver.getInstance(celOptions); - } - - @Override - public Object createMessage(String messageName, Map values) { - return unwrapCelValue( - valueProvider - .newValue(messageName, values) - .orElseThrow( - () -> - new NoSuchElementException( - "Could not generate a new value for message name: " + messageName))); - } - - @Override - @SuppressWarnings("unchecked") - public Object selectField(Object message, String fieldName) { - SelectableValue selectableValue = - (SelectableValue) protoCelValueConverter.fromJavaObjectToCelValue(message); - - return unwrapCelValue(selectableValue.select(StringValue.create(fieldName))); - } - - @Override - @SuppressWarnings("unchecked") - public Object hasField(Object message, String fieldName) { - SelectableValue selectableValue = - (SelectableValue) protoCelValueConverter.fromJavaObjectToCelValue(message); - - return selectableValue.find(StringValue.create(fieldName)).isPresent(); - } - - @Override - public Object adapt(Object message) { - if (message instanceof CelUnknownSet) { - return message; // CelUnknownSet is handled specially for iterative evaluation. No need to - // adapt to CelValue. - } - return unwrapCelValue(protoCelValueConverter.fromJavaObjectToCelValue(message)); - } - - @Override - public Value resolveObjectType(Object obj, Value checkedTypeValue) { - // Presently, Java only supports evaluation of checked-only expressions. - Preconditions.checkNotNull(checkedTypeValue); - return standardTypeResolver.resolveObjectType(obj, checkedTypeValue); - } - - @Override - public Value adaptType(CelType type) { - Preconditions.checkNotNull(type); - if (type instanceof TypeType) { - return createTypeValue(((TypeType) type).containingTypeName()); - } - - return createTypeValue(type.name()); - } - - @Override - public @Nullable Value adaptType(@Nullable Type type) { - throw new UnsupportedOperationException("This should only be called with native CelType."); - } - - /** - * 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 unwrapCelValue(CelValue object) { - return protoCelValueConverter.fromCelValueToJavaObject(object); - } - - private static Value createTypeValue(String name) { - return Value.newBuilder().setTypeValue(name).build(); - } -} diff --git a/runtime/src/main/java/dev/cel/runtime/RuntimeUnknownResolver.java b/runtime/src/main/java/dev/cel/runtime/RuntimeUnknownResolver.java index 1c1be9b94..f14e75dd7 100644 --- a/runtime/src/main/java/dev/cel/runtime/RuntimeUnknownResolver.java +++ b/runtime/src/main/java/dev/cel/runtime/RuntimeUnknownResolver.java @@ -53,7 +53,7 @@ public static RuntimeUnknownResolver fromResolver(GlobalResolver resolver) { // This prevents calculating the attribute trail if it will never be used for // efficiency, but doesn't change observable behavior. return new RuntimeUnknownResolver( - resolver, DEFAULT_RESOLVER, /* attributeTrackingEnabled= */ false) {}; + resolver, DEFAULT_RESOLVER, /* attributeTrackingEnabled= */ false); } public static Builder builder() { @@ -66,7 +66,7 @@ public static class Builder { private GlobalResolver resolver; private Builder() { - resolver = Activation.EMPTY; + resolver = GlobalResolver.EMPTY; attributeResolver = DEFAULT_RESOLVER; } @@ -91,18 +91,24 @@ public RuntimeUnknownResolver build() { * Return a single element unknown set if the attribute is partially unknown based on the defined * patterns. */ - Optional maybePartialUnknown(CelAttribute attribute) { - return attributeResolver.maybePartialUnknown(attribute); + Optional maybePartialUnknown(CelAttribute attribute) { + CelUnknownSet unknownSet = attributeResolver.maybePartialUnknown(attribute).orElse(null); + return Optional.ofNullable(unknownSet).map(InterpreterUtil::adaptToAccumulatedUnknowns); } /** Resolve a simple name to a value. */ DefaultInterpreter.IntermediateResult resolveSimpleName(String name, Long exprId) { + // Strip leading dot if present (for global disambiguation). + if (name.startsWith(".")) { + name = name.substring(1); + } + CelAttribute attr = CelAttribute.EMPTY; if (attributeTrackingEnabled) { attr = CelAttribute.fromQualifiedIdentifier(name); - Optional result = attributeResolver.resolve(attr); + Optional result = resolveAttribute(attr); if (result.isPresent()) { return DefaultInterpreter.IntermediateResult.create(attr, result.get()); } @@ -115,7 +121,13 @@ DefaultInterpreter.IntermediateResult resolveSimpleName(String name, Long exprId } void cacheLazilyEvaluatedResult(String name, DefaultInterpreter.IntermediateResult result) { - // no-op. Caching is handled in ScopedResolver. + throw new IllegalStateException( + "Internal error: Lazy attributes can only be cached in ScopedResolver."); + } + + void declareLazyAttribute(String attrName) { + throw new IllegalStateException( + "Internal error: Lazy attributes can only be declared in ScopedResolver."); } /** @@ -123,7 +135,8 @@ void cacheLazilyEvaluatedResult(String name, DefaultInterpreter.IntermediateResu * resolved values behind field accesses and index operations. */ Optional resolveAttribute(CelAttribute attr) { - return attributeResolver.resolve(attr); + Object resolved = attributeResolver.resolve(attr).orElse(null); + return Optional.ofNullable(resolved).map(InterpreterUtil::maybeAdaptToAccumulatedUnknowns); } ScopedResolver withScope(Map vars) { @@ -146,9 +159,13 @@ private ScopedResolver( @Override DefaultInterpreter.IntermediateResult resolveSimpleName(String name, Long exprId) { + // A name with a leading '.' always resolves in the root scope + if (name.startsWith(".")) { + return parent.resolveSimpleName(name, exprId); + } DefaultInterpreter.IntermediateResult result = lazyEvalResultCache.get(name); if (result != null) { - return result; + return copyIfMutable(result); } result = shadowedVars.get(name); if (result != null) { @@ -159,7 +176,46 @@ DefaultInterpreter.IntermediateResult resolveSimpleName(String name, Long exprId @Override void cacheLazilyEvaluatedResult(String name, DefaultInterpreter.IntermediateResult result) { - lazyEvalResultCache.put(name, result); + // Ensure that lazily evaluated result is stored at the proper scope. + // A lazily attribute is first declared when a new cel.bind/cel.block expr is encountered. + // + // If this attribute isn't found in the current scope, we need to walk up the parent scopes + // until we find this declaration. + // + // For example: cel.bind(x, get_true(), ['foo','bar'].map(unused, x && x)) + // + // Here, `x` would be evaluated in map macro's scope, but the result should be stored in + // cel.bind's scope. + if (!lazyEvalResultCache.containsKey(name)) { + parent.cacheLazilyEvaluatedResult(name, result); + } else { + lazyEvalResultCache.put(name, copyIfMutable(result)); + } + } + + @Override + void declareLazyAttribute(String attrName) { + lazyEvalResultCache.put(attrName, null); + } + + /** + * Perform a defensive copy of the intermediate result if it is mutable. + * + *

Some internal types are mutable to optimize performance, but this can cause issues when + * the result can be reused in multiple subexpressions due to caching. + * + *

Note: this is necessary on both the cache put and get path since the interpreter may use + * the same instance that was cached as a return value. + */ + private static DefaultInterpreter.IntermediateResult copyIfMutable( + DefaultInterpreter.IntermediateResult result) { + if (result.value() instanceof AccumulatedUnknowns) { + AccumulatedUnknowns unknowns = (AccumulatedUnknowns) result.value(); + return DefaultInterpreter.IntermediateResult.create( + result.attribute(), + AccumulatedUnknowns.create(unknowns.exprIds(), unknowns.attributes())); + } + return result; } } diff --git a/runtime/src/main/java/dev/cel/runtime/StandardFunctions.java b/runtime/src/main/java/dev/cel/runtime/StandardFunctions.java deleted file mode 100644 index d7d00fdaa..000000000 --- a/runtime/src/main/java/dev/cel/runtime/StandardFunctions.java +++ /dev/null @@ -1,1221 +0,0 @@ -// 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.runtime; - -import static java.time.Duration.ofSeconds; - -import com.google.common.primitives.Ints; -import com.google.common.primitives.UnsignedLong; -import com.google.common.primitives.UnsignedLongs; -import com.google.protobuf.ByteString; -import com.google.protobuf.Duration; -import com.google.protobuf.Timestamp; -import com.google.protobuf.util.Durations; -import com.google.protobuf.util.Timestamps; -import com.google.re2j.PatternSyntaxException; -import dev.cel.common.CelErrorCode; -import dev.cel.common.CelOptions; -import dev.cel.common.annotations.Internal; -import dev.cel.common.internal.ComparisonFunctions; -import dev.cel.common.internal.DefaultMessageFactory; -import dev.cel.common.internal.DynamicProto; -import java.math.BigDecimal; -import java.text.ParseException; -import java.time.DateTimeException; -import java.time.DayOfWeek; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -/** Adds standard functions to a {@link Registrar}. */ -@Internal -public class StandardFunctions { - private static final String UTC = "UTC"; - - /** - * Adds CEL standard functions to the given registrar. - * - *

Note this does not add functions which do not use strict argument evaluation order, as - * 'conditional', 'logical_and', and 'logical_or'. Those functions need to be dealt with ad-hoc. - */ - @SuppressWarnings({"rawtypes", "unchecked"}) - public static void add(Registrar registrar, DynamicProto dynamicProto, CelOptions celOptions) { - RuntimeEquality runtimeEquality = new RuntimeEquality(dynamicProto); - addNonInlined(registrar, runtimeEquality, celOptions); - - // String functions - registrar.add( - "matches", - String.class, - String.class, - (String string, String regexp) -> { - try { - return RuntimeHelpers.matches(string, regexp, celOptions); - } catch (PatternSyntaxException e) { - throw new InterpreterException.Builder(e.getMessage()) - .setCause(e) - .setErrorCode(CelErrorCode.INVALID_ARGUMENT) - .build(); - } - }); - // Duplicate receiver-style matches overload. - registrar.add( - "matches_string", - String.class, - String.class, - (String string, String regexp) -> { - try { - return RuntimeHelpers.matches(string, regexp, celOptions); - } catch (PatternSyntaxException e) { - throw new InterpreterException.Builder(e.getMessage()) - .setCause(e) - .setErrorCode(CelErrorCode.INVALID_ARGUMENT) - .build(); - } - }); - // In operator: b in a - registrar.add( - "in_list", - Object.class, - List.class, - (Object value, List list) -> runtimeEquality.inList(list, value, celOptions)); - registrar.add( - "in_map", - Object.class, - Map.class, - (Object key, Map map) -> runtimeEquality.inMap(map, key, celOptions)); - } - - /** - * Adds CEL standard functions to the given registrar, omitting those that can be inlined by - * {@code FuturesInterpreter}. - */ - public static void addNonInlined(Registrar registrar, CelOptions celOptions) { - addNonInlined( - registrar, - new RuntimeEquality(DynamicProto.create(DefaultMessageFactory.INSTANCE)), - celOptions); - } - - /** - * Adds CEL standard functions to the given registrar, omitting those that can be inlined by - * {@code FuturesInterpreter}. - */ - public static void addNonInlined( - Registrar registrar, RuntimeEquality runtimeEquality, CelOptions celOptions) { - addBoolFunctions(registrar); - addBytesFunctions(registrar); - addDoubleFunctions(registrar, celOptions); - addDurationFunctions(registrar); - addIntFunctions(registrar, celOptions); - addListFunctions(registrar, runtimeEquality, celOptions); - addMapFunctions(registrar, runtimeEquality, celOptions); - addStringFunctions(registrar, celOptions); - addTimestampFunctions(registrar); - if (celOptions.enableUnsignedLongs()) { - addUintFunctions(registrar, celOptions); - } else { - addSignedUintFunctions(registrar, celOptions); - } - if (celOptions.enableHeterogeneousNumericComparisons()) { - addCrossTypeNumericFunctions(registrar); - } - addOptionalValueFunctions(registrar, runtimeEquality, celOptions); - - // Common operators. - registrar.add( - "equals", - Object.class, - Object.class, - (Object x, Object y) -> runtimeEquality.objectEquals(x, y, celOptions)); - registrar.add( - "not_equals", - Object.class, - Object.class, - (Object x, Object y) -> !runtimeEquality.objectEquals(x, y, celOptions)); - - // Conversion to dyn. - registrar.add("to_dyn", Object.class, (Object arg) -> arg); - } - - private static void addBoolFunctions(Registrar registrar) { - // The conditional, logical_or, logical_and, and not_strictly_false functions are special-cased. - registrar.add("logical_not", Boolean.class, (Boolean x) -> !x); - - // Boolean ordering functions: <, <=, >=, > - registrar.add("less_bool", Boolean.class, Boolean.class, (Boolean x, Boolean y) -> !x && y); - registrar.add( - "less_equals_bool", Boolean.class, Boolean.class, (Boolean x, Boolean y) -> !x || y); - registrar.add("greater_bool", Boolean.class, Boolean.class, (Boolean x, Boolean y) -> x && !y); - registrar.add( - "greater_equals_bool", Boolean.class, Boolean.class, (Boolean x, Boolean y) -> x || !y); - } - - private static void addBytesFunctions(Registrar registrar) { - // Bytes ordering functions: <, <=, >=, > - registrar.add( - "less_bytes", - ByteString.class, - ByteString.class, - (ByteString x, ByteString y) -> - ByteString.unsignedLexicographicalComparator().compare(x, y) < 0); - registrar.add( - "less_equals_bytes", - ByteString.class, - ByteString.class, - (ByteString x, ByteString y) -> - ByteString.unsignedLexicographicalComparator().compare(x, y) <= 0); - registrar.add( - "greater_bytes", - ByteString.class, - ByteString.class, - (ByteString x, ByteString y) -> - ByteString.unsignedLexicographicalComparator().compare(x, y) > 0); - registrar.add( - "greater_equals_bytes", - ByteString.class, - ByteString.class, - (ByteString x, ByteString y) -> - ByteString.unsignedLexicographicalComparator().compare(x, y) >= 0); - - // Concatenation. - registrar.add("add_bytes", ByteString.class, ByteString.class, ByteString::concat); - - // Global and receiver functions for size(bytes) and bytes.size() respectively. - registrar.add("size_bytes", ByteString.class, (ByteString bytes) -> (long) bytes.size()); - registrar.add("bytes_size", ByteString.class, (ByteString bytes) -> (long) bytes.size()); - - // Conversion functions. - registrar.add("string_to_bytes", String.class, ByteString::copyFromUtf8); - } - - private static void addDoubleFunctions(Registrar registrar, CelOptions celOptions) { - // Double ordering functions. - registrar.add("less_double", Double.class, Double.class, (Double x, Double y) -> x < y); - registrar.add("less_equals_double", Double.class, Double.class, (Double x, Double y) -> x <= y); - registrar.add("greater_double", Double.class, Double.class, (Double x, Double y) -> x > y); - registrar.add( - "greater_equals_double", Double.class, Double.class, (Double x, Double y) -> x >= y); - - // Double arithmetic operations. - registrar.add("add_double", Double.class, Double.class, (Double x, Double y) -> x + y); - registrar.add("subtract_double", Double.class, Double.class, (Double x, Double y) -> x - y); - registrar.add("multiply_double", Double.class, Double.class, (Double x, Double y) -> x * y); - registrar.add("divide_double", Double.class, Double.class, (Double x, Double y) -> x / y); - registrar.add("negate_double", Double.class, (Double x) -> -x); - - // Conversions to double. - registrar.add("int64_to_double", Long.class, Long::doubleValue); - if (celOptions.enableUnsignedLongs()) { - registrar.add("uint64_to_double", UnsignedLong.class, UnsignedLong::doubleValue); - } else { - registrar.add( - "uint64_to_double", - Long.class, - (Long arg) -> UnsignedLong.fromLongBits(arg).doubleValue()); - } - registrar.add( - "string_to_double", - String.class, - (String arg) -> { - try { - return Double.parseDouble(arg); - } catch (NumberFormatException e) { - throw new InterpreterException.Builder(e.getMessage()) - .setCause(e) - .setErrorCode(CelErrorCode.BAD_FORMAT) - .build(); - } - }); - } - - private static void addDurationFunctions(Registrar registrar) { - // Duration ordering functions: <, <=, >=, > - registrar.add( - "less_duration", - Duration.class, - Duration.class, - (Duration x, Duration y) -> Durations.compare(x, y) < 0); - registrar.add( - "less_equals_duration", - Duration.class, - Duration.class, - (Duration x, Duration y) -> Durations.compare(x, y) <= 0); - registrar.add( - "greater_duration", - Duration.class, - Duration.class, - (Duration x, Duration y) -> Durations.compare(x, y) > 0); - registrar.add( - "greater_equals_duration", - Duration.class, - Duration.class, - (Duration x, Duration y) -> Durations.compare(x, y) >= 0); - - // Duration arithmetic functions. Some functions which involve a timestamp and duration - // can be found in the `addTimestampFunctions`. - registrar.add("add_duration_duration", Duration.class, Duration.class, Durations::add); - registrar.add( - "subtract_duration_duration", Duration.class, Duration.class, Durations::subtract); - - // Type conversion functions. - registrar.add( - "string_to_duration", - String.class, - (String d) -> { - try { - return RuntimeHelpers.createDurationFromString(d); - } catch (IllegalArgumentException e) { - throw new InterpreterException.Builder(e.getMessage()) - .setErrorCode(CelErrorCode.BAD_FORMAT) - .build(); - } - }); - - // Functions for extracting different time components and units from a duration. - - // getHours - registrar.add("duration_to_hours", Duration.class, Durations::toHours); - - // getMinutes - registrar.add("duration_to_minutes", Duration.class, Durations::toMinutes); - - // getSeconds - registrar.add("duration_to_seconds", Duration.class, Durations::toSeconds); - - // getMilliseconds - // duration as milliseconds and not just the millisecond part of a duration. - registrar.add( - "duration_to_milliseconds", - Duration.class, - (Duration arg) -> Durations.toMillis(arg) % ofSeconds(1).toMillis()); - } - - private static void addIntFunctions(Registrar registrar, CelOptions celOptions) { - // Comparison functions. - registrar.add("less_int64", Long.class, Long.class, (Long x, Long y) -> x < y); - registrar.add("less_equals_int64", Long.class, Long.class, (Long x, Long y) -> x <= y); - registrar.add("greater_int64", Long.class, Long.class, (Long x, Long y) -> x > y); - registrar.add("greater_equals_int64", Long.class, Long.class, (Long x, Long y) -> x >= y); - - // Arithmetic functions. - registrar.add( - "add_int64", - Long.class, - Long.class, - (Long x, Long y) -> { - try { - return RuntimeHelpers.int64Add(x, y, celOptions); - } catch (ArithmeticException e) { - throw new InterpreterException.Builder(e.getMessage()) - .setCause(e) - .setErrorCode(getArithmeticErrorCode(e)) - .build(); - } - }); - registrar.add( - "subtract_int64", - Long.class, - Long.class, - (Long x, Long y) -> { - try { - return RuntimeHelpers.int64Subtract(x, y, celOptions); - } catch (ArithmeticException e) { - throw new InterpreterException.Builder(e.getMessage()) - .setCause(e) - .setErrorCode(getArithmeticErrorCode(e)) - .build(); - } - }); - registrar.add( - "multiply_int64", - Long.class, - Long.class, - (Long x, Long y) -> { - try { - return RuntimeHelpers.int64Multiply(x, y, celOptions); - } catch (ArithmeticException e) { - throw new InterpreterException.Builder(e.getMessage()) - .setCause(e) - .setErrorCode(getArithmeticErrorCode(e)) - .build(); - } - }); - registrar.add( - "divide_int64", - Long.class, - Long.class, - (Long x, Long y) -> { - try { - return RuntimeHelpers.int64Divide(x, y, celOptions); - } catch (ArithmeticException e) { - throw new InterpreterException.Builder(e.getMessage()) - .setCause(e) - .setErrorCode(getArithmeticErrorCode(e)) - .build(); - } - }); - registrar.add( - "modulo_int64", - Long.class, - Long.class, - (Long x, Long y) -> { - try { - return x % y; - } catch (ArithmeticException e) { - throw new InterpreterException.Builder(e.getMessage()) - .setCause(e) - .setErrorCode(getArithmeticErrorCode(e)) - .build(); - } - }); - registrar.add( - "negate_int64", - Long.class, - (Long x) -> { - try { - return RuntimeHelpers.int64Negate(x, celOptions); - } catch (ArithmeticException e) { - throw new InterpreterException.Builder(e.getMessage()) - .setCause(e) - .setErrorCode(getArithmeticErrorCode(e)) - .build(); - } - }); - - // Conversions to int - if (celOptions.enableUnsignedLongs()) { - registrar.add( - "uint64_to_int64", - UnsignedLong.class, - (UnsignedLong arg) -> { - if (arg.compareTo(UnsignedLong.valueOf(Long.MAX_VALUE)) > 0) { - throw new InterpreterException.Builder("unsigned out of int range") - .setErrorCode(CelErrorCode.NUMERIC_OVERFLOW) - .build(); - } - return arg.longValue(); - }); - } else { - registrar.add( - "uint64_to_int64", - Long.class, - (Long arg) -> { - if (celOptions.errorOnIntWrap() && arg < 0) { - throw new InterpreterException.Builder("unsigned out of int range") - .setErrorCode(CelErrorCode.NUMERIC_OVERFLOW) - .build(); - } - return arg; - }); - } - registrar.add( - "double_to_int64", - Double.class, - (Double arg) -> { - if (celOptions.errorOnIntWrap()) { - return RuntimeHelpers.doubleToLongChecked(arg) - .orElseThrow( - () -> - new InterpreterException.Builder("double is out of range for int") - .setErrorCode(CelErrorCode.NUMERIC_OVERFLOW) - .build()); - } - return arg.longValue(); - }); - registrar.add( - "string_to_int64", - String.class, - (String arg) -> { - try { - return Long.parseLong(arg); - } catch (NumberFormatException e) { - throw new InterpreterException.Builder(e.getMessage()) - .setCause(e) - .setErrorCode(CelErrorCode.BAD_FORMAT) - .build(); - } - }); - registrar.add("timestamp_to_int64", Timestamp.class, Timestamps::toSeconds); - } - - @SuppressWarnings({"rawtypes", "unchecked"}) - private static void addListFunctions( - Registrar registrar, RuntimeEquality runtimeEquality, CelOptions celOptions) { - // List concatenation. - registrar.add("add_list", List.class, List.class, RuntimeHelpers::concat); - - // List indexing, a[b] - registrar.add("index_list", List.class, Number.class, RuntimeHelpers::indexList); - - // Global and receiver overloads for size(list) and list.size() respectively. - registrar.add("size_list", List.class, (List list1) -> (long) list1.size()); - registrar.add("list_size", List.class, (List list1) -> (long) list1.size()); - - // TODO: Deprecate in(a, b). - // In function: in(a, b) - registrar.add( - "in_function_list", - List.class, - Object.class, - (List list, Object value) -> runtimeEquality.inList(list, value, celOptions)); - } - - @SuppressWarnings({"rawtypes", "unchecked"}) - private static void addMapFunctions( - Registrar registrar, RuntimeEquality runtimeEquality, CelOptions celOptions) { - // Map indexing, a[b] - registrar.add( - "index_map", - Map.class, - Object.class, - (Map map, Object key) -> runtimeEquality.indexMap(map, key, celOptions)); - - // Global and receiver overloads for size(map) and map.size() respectively. - registrar.add("size_map", Map.class, (Map map1) -> (long) map1.size()); - registrar.add("map_size", Map.class, (Map map1) -> (long) map1.size()); - - // TODO: Deprecate in(a, b). - registrar.add( - "in_function_map", - Map.class, - Object.class, - (Map map, Object key) -> runtimeEquality.inMap(map, key, celOptions)); - } - - private static void addStringFunctions(Registrar registrar, CelOptions celOptions) { - // String ordering functions: <, <=, >=, >. - registrar.add( - "less_string", String.class, String.class, (String x, String y) -> x.compareTo(y) < 0); - registrar.add( - "less_equals_string", - String.class, - String.class, - (String x, String y) -> x.compareTo(y) <= 0); - registrar.add( - "greater_string", String.class, String.class, (String x, String y) -> x.compareTo(y) > 0); - registrar.add( - "greater_equals_string", - String.class, - String.class, - (String x, String y) -> x.compareTo(y) >= 0); - - // String concatenation. - registrar.add("add_string", String.class, String.class, (String x, String y) -> x + y); - - // Global and receiver function for size(string) and string.size() respectively. - registrar.add( - "size_string", String.class, (String s) -> (long) s.codePointCount(0, s.length())); - registrar.add( - "string_size", String.class, (String s) -> (long) s.codePointCount(0, s.length())); - - // String operation functions. There's a 'match' function which is part of this set, but is - // declared elsewhere as some implementations special case it. - registrar.add("contains_string", String.class, String.class, String::contains); - registrar.add("ends_with_string", String.class, String.class, String::endsWith); - registrar.add("starts_with_string", String.class, String.class, String::startsWith); - - // Conversions to string. - registrar.add("int64_to_string", Long.class, (Long arg) -> arg.toString()); - if (celOptions.enableUnsignedLongs()) { - registrar.add("uint64_to_string", UnsignedLong.class, UnsignedLong::toString); - } else { - registrar.add("uint64_to_string", Long.class, UnsignedLongs::toString); - } - registrar.add("double_to_string", Double.class, (Double arg) -> arg.toString()); - registrar.add("bytes_to_string", ByteString.class, ByteString::toStringUtf8); - registrar.add("timestamp_to_string", Timestamp.class, Timestamps::toString); - registrar.add("duration_to_string", Duration.class, Durations::toString); - } - - // We specifically need to only access nanos-of-second field for - // timestamp_to_milliseconds overload - @SuppressWarnings("JavaLocalDateTimeGetNano") - private static void addTimestampFunctions(Registrar registrar) { - // Timestamp relation operators: <, <=, >=, > - registrar.add( - "less_timestamp", - Timestamp.class, - Timestamp.class, - (Timestamp x, Timestamp y) -> Timestamps.compare(x, y) < 0); - registrar.add( - "less_equals_timestamp", - Timestamp.class, - Timestamp.class, - (Timestamp x, Timestamp y) -> Timestamps.compare(x, y) <= 0); - registrar.add( - "greater_timestamp", - Timestamp.class, - Timestamp.class, - (Timestamp x, Timestamp y) -> Timestamps.compare(x, y) > 0); - registrar.add( - "greater_equals_timestamp", - Timestamp.class, - Timestamp.class, - (Timestamp x, Timestamp y) -> Timestamps.compare(x, y) >= 0); - - // Timestamp and timestamp/duration arithmetic operators. - registrar.add("add_timestamp_duration", Timestamp.class, Duration.class, Timestamps::add); - registrar.add( - "add_duration_timestamp", - Duration.class, - Timestamp.class, - (Duration x, Timestamp y) -> Timestamps.add(y, x)); - registrar.add( - "subtract_timestamp_timestamp", - Timestamp.class, - Timestamp.class, - (Timestamp x, Timestamp y) -> Timestamps.between(y, x)); - registrar.add( - "subtract_timestamp_duration", Timestamp.class, Duration.class, Timestamps::subtract); - - // Conversions to timestamp. - registrar.add( - "string_to_timestamp", - String.class, - (String ts) -> { - try { - return Timestamps.parse(ts); - } catch (ParseException e) { - throw new InterpreterException.Builder(e.getMessage()) - .setErrorCode(CelErrorCode.BAD_FORMAT) - .build(); - } - }); - registrar.add("int64_to_timestamp", Long.class, Timestamps::fromSeconds); - - // Date/time functions - // getFullYear - registrar.add( - "timestamp_to_year", - Timestamp.class, - (Timestamp ts) -> (long) newLocalDateTime(ts, UTC).getYear()); - registrar.add( - "timestamp_to_year_with_tz", - Timestamp.class, - String.class, - (Timestamp ts, String tz) -> (long) newLocalDateTime(ts, tz).getYear()); - - // getMonth - registrar.add( - "timestamp_to_month", - Timestamp.class, - (Timestamp ts) -> (long) newLocalDateTime(ts, UTC).getMonthValue() - 1); - registrar.add( - "timestamp_to_month_with_tz", - Timestamp.class, - String.class, - (Timestamp ts, String tz) -> (long) newLocalDateTime(ts, tz).getMonthValue() - 1); - - // getDayOfYear - registrar.add( - "timestamp_to_day_of_year", - Timestamp.class, - (Timestamp ts) -> (long) newLocalDateTime(ts, UTC).getDayOfYear() - 1); - registrar.add( - "timestamp_to_day_of_year_with_tz", - Timestamp.class, - String.class, - (Timestamp ts, String tz) -> (long) newLocalDateTime(ts, tz).getDayOfYear() - 1); - - // getDayOfMonth - registrar.add( - "timestamp_to_day_of_month", - Timestamp.class, - (Timestamp ts) -> (long) newLocalDateTime(ts, UTC).getDayOfMonth() - 1); - registrar.add( - "timestamp_to_day_of_month_with_tz", - Timestamp.class, - String.class, - (Timestamp ts, String tz) -> (long) newLocalDateTime(ts, tz).getDayOfMonth() - 1); - - // getDate - registrar.add( - "timestamp_to_day_of_month_1_based", - Timestamp.class, - (Timestamp ts) -> (long) newLocalDateTime(ts, UTC).getDayOfMonth()); - registrar.add( - "timestamp_to_day_of_month_1_based_with_tz", - Timestamp.class, - String.class, - (Timestamp ts, String tz) -> (long) newLocalDateTime(ts, tz).getDayOfMonth()); - - // getDayOfWeek - registrar.add( - "timestamp_to_day_of_week", - Timestamp.class, - (Timestamp ts) -> { - // CEL treats Sunday as day 0, but Java.time treats it as day 7. - DayOfWeek dayOfWeek = newLocalDateTime(ts, UTC).getDayOfWeek(); - return (long) dayOfWeek.getValue() % 7; - }); - registrar.add( - "timestamp_to_day_of_week_with_tz", - Timestamp.class, - String.class, - (Timestamp ts, String tz) -> { - // CEL treats Sunday as day 0, but Java.time treats it as day 7. - DayOfWeek dayOfWeek = newLocalDateTime(ts, tz).getDayOfWeek(); - return (long) dayOfWeek.getValue() % 7; - }); - - // getHours - registrar.add( - "timestamp_to_hours", - Timestamp.class, - (Timestamp ts) -> (long) newLocalDateTime(ts, UTC).getHour()); - registrar.add( - "timestamp_to_hours_with_tz", - Timestamp.class, - String.class, - (Timestamp ts, String tz) -> (long) newLocalDateTime(ts, tz).getHour()); - registrar.add( - "timestamp_to_minutes", - Timestamp.class, - (Timestamp ts) -> (long) newLocalDateTime(ts, UTC).getMinute()); - registrar.add( - "timestamp_to_minutes_with_tz", - Timestamp.class, - String.class, - (Timestamp ts, String tz) -> (long) newLocalDateTime(ts, tz).getMinute()); - registrar.add( - "timestamp_to_seconds", - Timestamp.class, - (Timestamp ts) -> (long) newLocalDateTime(ts, UTC).getSecond()); - registrar.add( - "timestamp_to_seconds_with_tz", - Timestamp.class, - String.class, - (Timestamp ts, String tz) -> (long) newLocalDateTime(ts, tz).getSecond()); - registrar.add( - "timestamp_to_milliseconds", - Timestamp.class, - (Timestamp ts) -> (long) (newLocalDateTime(ts, UTC).getNano() / 1e+6)); - registrar.add( - "timestamp_to_milliseconds_with_tz", - Timestamp.class, - String.class, - (Timestamp ts, String tz) -> (long) (newLocalDateTime(ts, tz).getNano() / 1e+6)); - } - - private static void addSignedUintFunctions(Registrar registrar, CelOptions celOptions) { - // Uint relation operators: <, <=, >=, > - registrar.add( - "less_uint64", - Long.class, - Long.class, - (Long x, Long y) -> RuntimeHelpers.uint64CompareTo(x, y, celOptions) < 0); - registrar.add( - "less_equals_uint64", - Long.class, - Long.class, - (Long x, Long y) -> RuntimeHelpers.uint64CompareTo(x, y, celOptions) <= 0); - registrar.add( - "greater_uint64", - Long.class, - Long.class, - (Long x, Long y) -> RuntimeHelpers.uint64CompareTo(x, y, celOptions) > 0); - registrar.add( - "greater_equals_uint64", - Long.class, - Long.class, - (Long x, Long y) -> RuntimeHelpers.uint64CompareTo(x, y, celOptions) >= 0); - - // Uint arithmetic operators. - registrar.add( - "add_uint64", - Long.class, - Long.class, - (Long x, Long y) -> { - try { - return RuntimeHelpers.uint64Add(x, y, celOptions); - } catch (ArithmeticException e) { - throw new InterpreterException.Builder(e.getMessage()) - .setCause(e) - .setErrorCode(getArithmeticErrorCode(e)) - .build(); - } - }); - registrar.add( - "subtract_uint64", - Long.class, - Long.class, - (Long x, Long y) -> { - try { - return RuntimeHelpers.uint64Subtract(x, y, celOptions); - } catch (ArithmeticException e) { - throw new InterpreterException.Builder(e.getMessage()) - .setCause(e) - .setErrorCode(getArithmeticErrorCode(e)) - .build(); - } - }); - registrar.add( - "multiply_uint64", - Long.class, - Long.class, - (Long x, Long y) -> { - try { - return RuntimeHelpers.uint64Multiply(x, y, celOptions); - } catch (ArithmeticException e) { - throw new InterpreterException.Builder(e.getMessage()) - .setCause(e) - .setErrorCode(getArithmeticErrorCode(e)) - .build(); - } - }); - registrar.add( - "divide_uint64", - Long.class, - Long.class, - (Long x, Long y) -> RuntimeHelpers.uint64Divide(x, y, celOptions)); - registrar.add( - "modulo_uint64", - Long.class, - Long.class, - (Long x, Long y) -> RuntimeHelpers.uint64Mod(x, y, celOptions)); - - // Conversions to uint. - registrar.add( - "int64_to_uint64", - Long.class, - (Long arg) -> { - if (celOptions.errorOnIntWrap() && arg < 0) { - throw new InterpreterException.Builder("int out of uint range") - .setErrorCode(CelErrorCode.NUMERIC_OVERFLOW) - .build(); - } - return arg; - }); - registrar.add( - "double_to_uint64", - Double.class, - (Double arg) -> { - if (celOptions.errorOnIntWrap()) { - return RuntimeHelpers.doubleToUnsignedChecked(arg) - .map(UnsignedLong::longValue) - .orElseThrow( - () -> - new InterpreterException.Builder("double out of uint range") - .setErrorCode(CelErrorCode.NUMERIC_OVERFLOW) - .build()); - } - return arg.longValue(); - }); - registrar.add( - "string_to_uint64", - String.class, - (String arg) -> { - try { - return UnsignedLongs.parseUnsignedLong(arg); - } catch (NumberFormatException e) { - throw new InterpreterException.Builder(e.getMessage()) - .setCause(e) - .setErrorCode(CelErrorCode.BAD_FORMAT) - .build(); - } - }); - } - - private static void addUintFunctions(Registrar registrar, CelOptions celOptions) { - registrar.add( - "less_uint64", - UnsignedLong.class, - UnsignedLong.class, - (UnsignedLong x, UnsignedLong y) -> RuntimeHelpers.uint64CompareTo(x, y) < 0); - registrar.add( - "less_equals_uint64", - UnsignedLong.class, - UnsignedLong.class, - (UnsignedLong x, UnsignedLong y) -> RuntimeHelpers.uint64CompareTo(x, y) <= 0); - registrar.add( - "greater_uint64", - UnsignedLong.class, - UnsignedLong.class, - (UnsignedLong x, UnsignedLong y) -> RuntimeHelpers.uint64CompareTo(x, y) > 0); - registrar.add( - "greater_equals_uint64", - UnsignedLong.class, - UnsignedLong.class, - (UnsignedLong x, UnsignedLong y) -> RuntimeHelpers.uint64CompareTo(x, y) >= 0); - - registrar.add( - "add_uint64", - UnsignedLong.class, - UnsignedLong.class, - (UnsignedLong x, UnsignedLong y) -> { - try { - return RuntimeHelpers.uint64Add(x, y); - } catch (ArithmeticException e) { - throw new InterpreterException.Builder(e.getMessage()) - .setCause(e) - .setErrorCode(getArithmeticErrorCode(e)) - .build(); - } - }); - registrar.add( - "subtract_uint64", - UnsignedLong.class, - UnsignedLong.class, - (UnsignedLong x, UnsignedLong y) -> { - try { - return RuntimeHelpers.uint64Subtract(x, y); - } catch (ArithmeticException e) { - throw new InterpreterException.Builder(e.getMessage()) - .setCause(e) - .setErrorCode(getArithmeticErrorCode(e)) - .build(); - } - }); - registrar.add( - "multiply_uint64", - UnsignedLong.class, - UnsignedLong.class, - (UnsignedLong x, UnsignedLong y) -> { - try { - return RuntimeHelpers.uint64Multiply(x, y); - } catch (ArithmeticException e) { - throw new InterpreterException.Builder(e.getMessage()) - .setCause(e) - .setErrorCode(getArithmeticErrorCode(e)) - .build(); - } - }); - registrar.add( - "divide_uint64", UnsignedLong.class, UnsignedLong.class, RuntimeHelpers::uint64Divide); - - // Modulo - registrar.add( - "modulo_uint64", UnsignedLong.class, UnsignedLong.class, RuntimeHelpers::uint64Mod); - - // Conversions to uint. - registrar.add( - "int64_to_uint64", - Long.class, - (Long arg) -> { - if (celOptions.errorOnIntWrap() && arg < 0) { - throw new InterpreterException.Builder("int out of uint range") - .setErrorCode(CelErrorCode.NUMERIC_OVERFLOW) - .build(); - } - return UnsignedLong.valueOf(arg); - }); - registrar.add( - "double_to_uint64", - Double.class, - (Double arg) -> { - if (celOptions.errorOnIntWrap()) { - return RuntimeHelpers.doubleToUnsignedChecked(arg) - .orElseThrow( - () -> - new InterpreterException.Builder("double out of uint range") - .setErrorCode(CelErrorCode.NUMERIC_OVERFLOW) - .build()); - } - return UnsignedLong.valueOf(BigDecimal.valueOf(arg).toBigInteger()); - }); - registrar.add( - "string_to_uint64", - String.class, - (String arg) -> { - try { - return UnsignedLong.valueOf(arg); - } catch (NumberFormatException e) { - throw new InterpreterException.Builder(e.getMessage()) - .setCause(e) - .setErrorCode(CelErrorCode.BAD_FORMAT) - .build(); - } - }); - } - - private static void addCrossTypeNumericFunctions(Registrar registrar) { - // Cross-type numeric less than. - registrar.add( - "less_int64_uint64", - Long.class, - UnsignedLong.class, - (Long x, UnsignedLong y) -> ComparisonFunctions.compareIntUint(x, y) == -1); - registrar.add( - "less_uint64_int64", - UnsignedLong.class, - Long.class, - (UnsignedLong x, Long y) -> ComparisonFunctions.compareUintInt(x, y) == -1); - registrar.add( - "less_int64_double", - Long.class, - Double.class, - (Long x, Double y) -> ComparisonFunctions.compareIntDouble(x, y) == -1); - registrar.add( - "less_double_int64", - Double.class, - Long.class, - (Double x, Long y) -> ComparisonFunctions.compareDoubleInt(x, y) == -1); - registrar.add( - "less_uint64_double", - UnsignedLong.class, - Double.class, - (UnsignedLong x, Double y) -> ComparisonFunctions.compareUintDouble(x, y) == -1); - registrar.add( - "less_double_uint64", - Double.class, - UnsignedLong.class, - (Double x, UnsignedLong y) -> ComparisonFunctions.compareDoubleUint(x, y) == -1); - // Cross-type numeric less than or equal. - registrar.add( - "less_equals_int64_uint64", - Long.class, - UnsignedLong.class, - (Long x, UnsignedLong y) -> ComparisonFunctions.compareIntUint(x, y) <= 0); - registrar.add( - "less_equals_uint64_int64", - UnsignedLong.class, - Long.class, - (UnsignedLong x, Long y) -> ComparisonFunctions.compareUintInt(x, y) <= 0); - registrar.add( - "less_equals_int64_double", - Long.class, - Double.class, - (Long x, Double y) -> ComparisonFunctions.compareIntDouble(x, y) <= 0); - registrar.add( - "less_equals_double_int64", - Double.class, - Long.class, - (Double x, Long y) -> ComparisonFunctions.compareDoubleInt(x, y) <= 0); - registrar.add( - "less_equals_uint64_double", - UnsignedLong.class, - Double.class, - (UnsignedLong x, Double y) -> ComparisonFunctions.compareUintDouble(x, y) <= 0); - registrar.add( - "less_equals_double_uint64", - Double.class, - UnsignedLong.class, - (Double x, UnsignedLong y) -> ComparisonFunctions.compareDoubleUint(x, y) <= 0); - // Cross-type numeric greater than. - registrar.add( - "greater_int64_uint64", - Long.class, - UnsignedLong.class, - (Long x, UnsignedLong y) -> ComparisonFunctions.compareIntUint(x, y) == 1); - registrar.add( - "greater_uint64_int64", - UnsignedLong.class, - Long.class, - (UnsignedLong x, Long y) -> ComparisonFunctions.compareUintInt(x, y) == 1); - registrar.add( - "greater_int64_double", - Long.class, - Double.class, - (Long x, Double y) -> ComparisonFunctions.compareIntDouble(x, y) == 1); - registrar.add( - "greater_double_int64", - Double.class, - Long.class, - (Double x, Long y) -> ComparisonFunctions.compareDoubleInt(x, y) == 1); - registrar.add( - "greater_uint64_double", - UnsignedLong.class, - Double.class, - (UnsignedLong x, Double y) -> ComparisonFunctions.compareUintDouble(x, y) == 1); - registrar.add( - "greater_double_uint64", - Double.class, - UnsignedLong.class, - (Double x, UnsignedLong y) -> ComparisonFunctions.compareDoubleUint(x, y) == 1); - // Cross-type numeric greater than or equal. - registrar.add( - "greater_equals_int64_uint64", - Long.class, - UnsignedLong.class, - (Long x, UnsignedLong y) -> ComparisonFunctions.compareIntUint(x, y) >= 0); - registrar.add( - "greater_equals_uint64_int64", - UnsignedLong.class, - Long.class, - (UnsignedLong x, Long y) -> ComparisonFunctions.compareUintInt(x, y) >= 0); - registrar.add( - "greater_equals_int64_double", - Long.class, - Double.class, - (Long x, Double y) -> ComparisonFunctions.compareIntDouble(x, y) >= 0); - registrar.add( - "greater_equals_double_int64", - Double.class, - Long.class, - (Double x, Long y) -> ComparisonFunctions.compareDoubleInt(x, y) >= 0); - registrar.add( - "greater_equals_uint64_double", - UnsignedLong.class, - Double.class, - (UnsignedLong x, Double y) -> ComparisonFunctions.compareUintDouble(x, y) >= 0); - registrar.add( - "greater_equals_double_uint64", - Double.class, - UnsignedLong.class, - (Double x, UnsignedLong y) -> ComparisonFunctions.compareDoubleUint(x, y) >= 0); - } - - /** - * Note: These aren't part of the standard language definitions, but it is being defined here to - * support runtime bindings for CelOptionalLibrary, as it requires specific dependencies such as - * {@link RuntimeEquality} that is only available here. - * - *

Conversely, declarations related to Optional values should NOT be added as part of the - * standard definitions to avoid accidental exposure of this optional feature. - */ - @SuppressWarnings({"rawtypes"}) - private static void addOptionalValueFunctions( - Registrar registrar, RuntimeEquality runtimeEquality, CelOptions options) { - registrar.add( - "select_optional_field", // This only handles map selection. Proto selection is special - // cased inside the interpreter. - Map.class, - String.class, - (Map map, String key) -> runtimeEquality.findInMap(map, key, options)); - registrar.add( - "map_optindex_optional_value", - Map.class, - Object.class, - (Map map, Object key) -> runtimeEquality.findInMap(map, key, options)); - registrar.add( - "optional_map_optindex_optional_value", - Optional.class, - Object.class, - (Optional optionalMap, Object key) -> - indexOptionalMap(optionalMap, key, options, runtimeEquality)); - registrar.add( - "optional_map_index_value", - Optional.class, - Object.class, - (Optional optionalMap, Object key) -> - indexOptionalMap(optionalMap, key, options, runtimeEquality)); - registrar.add( - "optional_list_index_int", - Optional.class, - Long.class, - StandardFunctions::indexOptionalList); - registrar.add( - "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)); - }); - registrar.add( - "optional_list_optindex_optional_int", - Optional.class, - Long.class, - StandardFunctions::indexOptionalList); - } - - private static Object indexOptionalMap( - Optional optionalMap, Object key, CelOptions options, RuntimeEquality runtimeEquality) { - if (!optionalMap.isPresent()) { - return Optional.empty(); - } - - Map map = (Map) optionalMap.get(); - - return runtimeEquality.findInMap(map, key, options); - } - - private static Object indexOptionalList(Optional optionalList, long index) { - if (!optionalList.isPresent()) { - return Optional.empty(); - } - List list = (List) optionalList.get(); - int castIndex = Ints.checkedCast(index); - if (castIndex < 0 || castIndex >= list.size()) { - return Optional.empty(); - } - return Optional.of(list.get(castIndex)); - } - - /** - * Get the DateTimeZone Instance. - * - * @param tz the ID of the datetime zone - * @return the ZoneId object - * @throws InterpreterException if there is an invalid timezone - */ - private static ZoneId timeZone(String tz) throws InterpreterException { - try { - return ZoneId.of(tz); - } catch (DateTimeException e) { - // If timezone is not a string name (for example, 'US/Central'), it should be a numerical - // offset from UTC in the format [+/-]HH:MM. - try { - int ind = tz.indexOf(":"); - if (ind == -1) { - throw new InterpreterException.Builder(e.getMessage()).build(); - } - - int hourOffset = Integer.parseInt(tz.substring(0, ind)); - int minOffset = Integer.parseInt(tz.substring(ind + 1)); - // Ensures that the offset are properly formatted in [+/-]HH:MM to conform with - // ZoneOffset's format requirements. - // Example: "-9:30" -> "-09:30" and "9:30" -> "+09:30" - String formattedOffset = - ((hourOffset < 0) ? "-" : "+") - + String.format("%02d:%02d", Math.abs(hourOffset), minOffset); - - return ZoneId.of(formattedOffset); - - } catch (DateTimeException e2) { - throw new InterpreterException.Builder(e2.getMessage()).build(); - } - } - } - - /** - * Constructs a new {@link LocalDateTime} instance - * - * @param ts Timestamp protobuf object - * @param tz Timezone based on the CEL specification. This is either the canonical name from tz - * database or a standard offset represented in (+/-)HH:MM. Few valid examples are: - *

    - *
  • UTC - *
  • America/Los_Angeles - *
  • -09:30 or -9:30 (Leading zeroes can be omitted though not allowed by spec) - *
- * - * @return If an Invalid timezone is supplied. - */ - private static LocalDateTime newLocalDateTime(Timestamp ts, String tz) - throws InterpreterException { - return Instant.ofEpochSecond(ts.getSeconds(), ts.getNanos()) - .atZone(timeZone(tz)) - .toLocalDateTime(); - } - - private static CelErrorCode getArithmeticErrorCode(ArithmeticException e) { - String exceptionMessage = e.getMessage(); - // The two known cases for an arithmetic exception is divide by zero and overflow. - if (exceptionMessage.equals("/ by zero")) { - return CelErrorCode.DIVIDE_BY_ZERO; - } - return CelErrorCode.NUMERIC_OVERFLOW; - } - - private StandardFunctions() {} -} diff --git a/runtime/src/main/java/dev/cel/runtime/StandardTypeResolver.java b/runtime/src/main/java/dev/cel/runtime/StandardTypeResolver.java deleted file mode 100644 index c0860f26b..000000000 --- a/runtime/src/main/java/dev/cel/runtime/StandardTypeResolver.java +++ /dev/null @@ -1,236 +0,0 @@ -// 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.runtime; - -import static com.google.common.base.Preconditions.checkNotNull; - -import dev.cel.expr.Type; -import dev.cel.expr.Type.PrimitiveType; -import dev.cel.expr.Type.TypeKindCase; -import dev.cel.expr.Value; -import dev.cel.expr.Value.KindCase; -import com.google.common.collect.ImmutableMap; -import com.google.common.primitives.UnsignedLong; -import com.google.errorprone.annotations.Immutable; -import com.google.protobuf.ByteString; -import com.google.protobuf.MessageOrBuilder; -import com.google.protobuf.NullValue; -import dev.cel.common.CelOptions; -import dev.cel.common.annotations.Internal; -import dev.cel.common.types.CelKind; -import dev.cel.common.types.CelType; -import dev.cel.common.types.TypeType; -import java.util.Collection; -import java.util.Map; -import org.jspecify.nullness.Nullable; - -/** - * The {@code StandardTypeResolver} implements the {@link TypeResolver} and resolves types supported - * by the CEL standard environment. - * - *

CEL Library Internals. Do Not Use. - */ -@Immutable -@Internal -public final class StandardTypeResolver implements TypeResolver { - - /** - * Obtain a singleton instance of the {@link StandardTypeResolver} appropriate for the {@code - * celOptions} provided. - */ - public static TypeResolver getInstance(CelOptions celOptions) { - return celOptions.enableUnsignedLongs() ? INSTANCE_WITH_UNSIGNED_LONGS : INSTANCE; - } - - private static final TypeResolver INSTANCE = - new StandardTypeResolver(commonTypes(/* unsignedLongs= */ false)); - - private static final TypeResolver INSTANCE_WITH_UNSIGNED_LONGS = - new StandardTypeResolver(commonTypes(/* unsignedLongs= */ true)); - - // Type of type which is modelled as a value instance rather than as a Java POJO. - private static final Value TYPE_VALUE = createType("type"); - - // Built-in types. - private static ImmutableMap> commonTypes(boolean unsignedLongs) { - return ImmutableMap.>builder() - .put(createType("bool"), Boolean.class) - .put(createType("bytes"), ByteString.class) - .put(createType("double"), Double.class) - .put(createType("int"), Long.class) - .put(createType("uint"), unsignedLongs ? UnsignedLong.class : Long.class) - .put(createType("string"), String.class) - .put(createType("null_type"), NullValue.class) - // Aggregate types. - .put(createType("list"), Collection.class) - .put(createType("map"), Map.class) - .buildOrThrow(); - } - - private final ImmutableMap> types; - - private StandardTypeResolver(ImmutableMap> types) { - this.types = types; - } - - @Nullable - @Override - public Value resolveObjectType(Object obj, @Nullable Value checkedTypeValue) { - if (checkedTypeValue != null && (obj instanceof Long || obj instanceof NullValue)) { - return checkedTypeValue; - } - return resolveObjectType(obj); - } - - @Nullable - private Value resolveObjectType(Object obj) { - for (Value type : types.keySet()) { - Class impl = types.get(type); - // Generally, the type will be an instance of a class. - if (impl.isInstance(obj)) { - return type; - } - } - // In the case 'type' values, the obj will be a api.expr.Value. - if (obj instanceof Value) { - Value objVal = (Value) obj; - if (objVal.getKindCase() == KindCase.TYPE_VALUE) { - return TYPE_VALUE; - } - } - // Otherwise, this is a protobuf type. - if (obj instanceof MessageOrBuilder) { - MessageOrBuilder msg = (MessageOrBuilder) obj; - return createType(msg.getDescriptorForType().getFullName()); - } - return null; - } - - /** {@inheritDoc} */ - @Override - public @Nullable Value adaptType(CelType type) { - checkNotNull(type); - // TODO: Add enum type support here. - Value.Builder typeValue = Value.newBuilder(); - switch (type.kind()) { - case OPAQUE: - case STRUCT: - return typeValue.setTypeValue(type.name()).build(); - case LIST: - return typeValue.setTypeValue("list").build(); - case MAP: - return typeValue.setTypeValue("map").build(); - case TYPE: - CelType typeOfType = ((TypeType) type).type(); - if (typeOfType.kind() == CelKind.DYN) { - return typeValue.setTypeValue("type").build(); - } - return adaptType(typeOfType); - case NULL_TYPE: - return typeValue.setTypeValue("null_type").build(); - case DURATION: - return typeValue.setTypeValue("google.protobuf.Duration").build(); - case TIMESTAMP: - return typeValue.setTypeValue("google.protobuf.Timestamp").build(); - case BOOL: - return typeValue.setTypeValue("bool").build(); - case BYTES: - return typeValue.setTypeValue("bytes").build(); - case DOUBLE: - return typeValue.setTypeValue("double").build(); - case INT: - return typeValue.setTypeValue("int").build(); - case STRING: - return typeValue.setTypeValue("string").build(); - case UINT: - return typeValue.setTypeValue("uint").build(); - default: - break; - } - return null; - } - - /** {@inheritDoc} */ - @Override - @Deprecated - public @Nullable Value adaptType(@Nullable Type type) { - if (type == null) { - return null; - } - // TODO: Add enum type support here. - Value.Builder typeValue = Value.newBuilder(); - switch (type.getTypeKindCase()) { - case ABSTRACT_TYPE: - return typeValue.setTypeValue(type.getAbstractType().getName()).build(); - case MESSAGE_TYPE: - return typeValue.setTypeValue(type.getMessageType()).build(); - case LIST_TYPE: - return typeValue.setTypeValue("list").build(); - case MAP_TYPE: - return typeValue.setTypeValue("map").build(); - case TYPE: - Type typeOfType = type.getType(); - if (typeOfType.getTypeKindCase() == TypeKindCase.DYN) { - return typeValue.setTypeValue("type").build(); - } - return adaptType(typeOfType); - case NULL: - return typeValue.setTypeValue("null_type").build(); - case PRIMITIVE: - return adaptPrimitive(type.getPrimitive()); - case WRAPPER: - return adaptPrimitive(type.getWrapper()); - case WELL_KNOWN: - switch (type.getWellKnown()) { - case DURATION: - return typeValue.setTypeValue("google.protobuf.Duration").build(); - case TIMESTAMP: - return typeValue.setTypeValue("google.protobuf.Timestamp").build(); - default: - break; - } - break; - default: - break; - } - return null; - } - - @Nullable - private static Value adaptPrimitive(PrimitiveType primitiveType) { - Value.Builder typeValue = Value.newBuilder(); - switch (primitiveType) { - case BOOL: - return typeValue.setTypeValue("bool").build(); - case BYTES: - return typeValue.setTypeValue("bytes").build(); - case DOUBLE: - return typeValue.setTypeValue("double").build(); - case INT64: - return typeValue.setTypeValue("int").build(); - case STRING: - return typeValue.setTypeValue("string").build(); - case UINT64: - return typeValue.setTypeValue("uint").build(); - default: - break; - } - return null; - } - - private static Value createType(String name) { - return Value.newBuilder().setTypeValue(name).build(); - } -} diff --git a/runtime/src/main/java/dev/cel/runtime/TypeResolver.java b/runtime/src/main/java/dev/cel/runtime/TypeResolver.java index 271c65bba..691810837 100644 --- a/runtime/src/main/java/dev/cel/runtime/TypeResolver.java +++ b/runtime/src/main/java/dev/cel/runtime/TypeResolver.java @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// 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. @@ -14,54 +14,197 @@ package dev.cel.runtime; -import dev.cel.expr.Type; -import dev.cel.expr.Value; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.primitives.UnsignedLong; import com.google.errorprone.annotations.Immutable; +import com.google.protobuf.ByteString; +import com.google.protobuf.Duration; +import com.google.protobuf.MessageLiteOrBuilder; +import com.google.protobuf.NullValue; +import com.google.protobuf.Timestamp; import dev.cel.common.annotations.Internal; import dev.cel.common.types.CelType; -import org.jspecify.nullness.Nullable; +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.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; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; /** - * The {@code TypeResolver} determines the CEL type of Java-native values and assists with adapting - * check-time types to runtime values. + * {@code TypeResolver} resolves incoming {@link CelType} into {@link TypeType}., either as part of + * a type call (type('foo'), type(1), etc.) or as a type literal (type, int, string, etc.) * *

CEL Library Internals. Do Not Use. */ @Immutable @Internal -public interface TypeResolver { - - /** - * Resolve the CEL type of the {@code obj}, using the {@code checkedTypeValue} as hint for type - * disambiguation. - * - *

The {@code checkedTypeValue} indicates the statically determined type of the object at - * check-time. Often, the check-time and runtime phases agree, but there are cases where the - * runtime type is ambiguous, as is the case when a {@code Long} value is supplied as this could - * either be an int, uint, or enum type. - * - *

Type resolution is biased toward the runtime value type, given the dynamically typed nature - * of CEL. - */ - @Nullable Value resolveObjectType(Object obj, @Nullable Value checkedTypeValue); - - /** - * Adapt the check-time {@code type} instance to a runtime {@code Value}. - * - *

When the checked {@code type} does not have a runtime equivalent, e.g. {@code Type#DYN}, the - * return value will be {@code null}. - */ - @Nullable Value adaptType(CelType type); - - /** - * Adapt the check-time {@code type} instance to a runtime {@code Value}. - * - *

When the checked {@code type} does not have a runtime equivalent, e.g. {@code Type#DYN}, the - * return value will be {@code null}. - * - * @deprecated use {@link #adaptType(CelType)} instead. This only exists to maintain compatibility - * with legacy async evaluator. - */ - @Deprecated - @Nullable Value adaptType(@Nullable Type type); +public class 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 + // true: type == type(string) && type == type(type("foo")) + @VisibleForTesting static final TypeType RUNTIME_TYPE_TYPE = TypeType.create(SimpleType.DYN); + + private static final ImmutableMap, TypeType> COMMON_TYPES = + ImmutableMap., TypeType>builder() + .put(Boolean.class, TypeType.create(SimpleType.BOOL)) + .put(Double.class, TypeType.create(SimpleType.DOUBLE)) + .put(Long.class, TypeType.create(SimpleType.INT)) + .put(UnsignedLong.class, TypeType.create(SimpleType.UINT)) + .put(String.class, TypeType.create(SimpleType.STRING)) + .put(NullValue.class, TypeType.create(SimpleType.NULL_TYPE)) + .put(dev.cel.common.values.NullValue.class, TypeType.create(SimpleType.NULL_TYPE)) + .put(java.time.Duration.class, TypeType.create(SimpleType.DURATION)) + .put(Instant.class, TypeType.create(SimpleType.TIMESTAMP)) + .put( + Duration.class, + TypeType.create( + SimpleType.DURATION)) // TODO: Remove once clients have been migrated + .put( + Timestamp.class, + TypeType.create( + SimpleType + .TIMESTAMP)) // TODO: Remove once clients have been migrated + .put(ArrayList.class, TypeType.create(ListType.create(SimpleType.DYN))) + .put(HashMap.class, TypeType.create(MapType.create(SimpleType.DYN, SimpleType.DYN))) + .put(Optional.class, TypeType.create(OptionalType.create(SimpleType.DYN))) + .put(CelByteString.class, TypeType.create(SimpleType.BYTES)) + .buildOrThrow(); + + private static final ImmutableMap, TypeType> EXTENDABLE_TYPES = + ImmutableMap., TypeType>builder() + .put( + ByteString.class, + TypeType.create( + SimpleType.BYTES)) // TODO: Remove once clients have been migrated + .put(Collection.class, TypeType.create(ListType.create(SimpleType.DYN))) + .put(Map.class, TypeType.create(MapType.create(SimpleType.DYN, SimpleType.DYN))) + .buildOrThrow(); + + /** Adapt the type-checked {@link CelType} into a runtime type value {@link TypeType}. */ + TypeType adaptType(CelType typeCheckedType) { + checkNotNull(typeCheckedType); + + switch (typeCheckedType.kind()) { + case TYPE: + CelType typeOfType = ((TypeType) typeCheckedType).type(); + switch (typeOfType.kind()) { + case STRUCT: + return TypeType.create(adaptStructType((StructType) typeOfType)); + default: + return (TypeType) typeCheckedType; + } + case UNSPECIFIED: + throw new IllegalArgumentException("Unsupported CelType kind: " + typeCheckedType.kind()); + default: + return TypeType.create(typeCheckedType); + } + } + + Optional resolveWellKnownObjectType(Object obj) { + if (obj instanceof TypeType) { + return Optional.of(RUNTIME_TYPE_TYPE); + } + + Class currentClass = obj.getClass(); + TypeType commonType = COMMON_TYPES.get(currentClass); + if (commonType != null) { + return Optional.of(commonType); + } + + // Guava's Immutable classes use package-private implementations, such that they require an + // explicit check. + if (ImmutableList.class.isAssignableFrom(currentClass)) { + return Optional.of(TypeType.create(ListType.create(SimpleType.DYN))); + } else if (ImmutableMap.class.isAssignableFrom(currentClass)) { + return Optional.of(TypeType.create(MapType.create(SimpleType.DYN, SimpleType.DYN))); + } + + return Optional.empty(); + } + + /** Resolve the CEL type of the {@code obj}. */ + public TypeType resolveObjectType(Object obj, CelType typeCheckedType) { + checkNotNull(obj); + Optional wellKnownTypeType = resolveWellKnownObjectType(obj); + if (wellKnownTypeType.isPresent()) { + 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"); + } + + Class currentClass = obj.getClass(); + TypeType runtimeType; + + // Handle types that the client may have extended. + while (currentClass != null) { + runtimeType = EXTENDABLE_TYPES.get(currentClass); + if (runtimeType != null) { + return runtimeType; + } + + // Check interfaces + for (Class interfaceClass : currentClass.getInterfaces()) { + runtimeType = EXTENDABLE_TYPES.get(interfaceClass); + if (runtimeType != null) { + return runtimeType; + } + } + currentClass = currentClass.getSuperclass(); + } + + // This is an opaque type, or something CEL doesn't know about. + return (TypeType) typeCheckedType; + } + + private static CelType adaptStructType(StructType typeOfType) { + String structName = typeOfType.name(); + CelType newTypeOfType; + if (structName.equals(SimpleType.DURATION.name())) { + newTypeOfType = SimpleType.DURATION; + } else if (structName.equals(SimpleType.TIMESTAMP.name())) { + newTypeOfType = SimpleType.TIMESTAMP; + } else { + // Coerces ProtoMessageTypeProvider to be a struct type reference for accurate + // equality tests. + // In the future, we can plumb ProtoMessageTypeProvider through the runtime to retain + // ProtoMessageType here. + newTypeOfType = StructTypeReference.create(typeOfType.name()); + } + return newTypeOfType; + } + + protected TypeResolver(CelValueConverter celValueConverter) { + this.celValueConverter = checkNotNull(celValueConverter); + } } diff --git a/runtime/src/main/java/dev/cel/runtime/UnknownContext.java b/runtime/src/main/java/dev/cel/runtime/UnknownContext.java index 457511523..c494ff252 100644 --- a/runtime/src/main/java/dev/cel/runtime/UnknownContext.java +++ b/runtime/src/main/java/dev/cel/runtime/UnknownContext.java @@ -55,7 +55,7 @@ private UnknownContext( ImmutableList unresolvedAttributes, ImmutableMap resolvedAttributes) { this.unresolvedAttributes = unresolvedAttributes; - variableResolver = resolver; + this.variableResolver = resolver; this.resolvedAttributes = resolvedAttributes; } @@ -76,6 +76,17 @@ public static UnknownContext create( createExprVariableResolver(resolver), ImmutableList.copyOf(attributes), ImmutableMap.of()); } + /** Extends an existing {@code UnknownContext} by adding more attribute patterns to it. */ + public UnknownContext extend(Collection attributePatterns) { + return new UnknownContext( + this.variableResolver(), + ImmutableList.builder() + .addAll(this.unresolvedAttributes) + .addAll(attributePatterns) + .build(), + this.resolvedAttributes); + } + /** Adapts a CelVariableResolver to the legacy impl equivalent GlobalResolver. */ private static GlobalResolver createExprVariableResolver(CelVariableResolver resolver) { return (String name) -> resolver.find(name).orElse(null); diff --git a/runtime/src/main/java/dev/cel/runtime/UnknownTrackingInterpretable.java b/runtime/src/main/java/dev/cel/runtime/UnknownTrackingInterpretable.java index 96dbd2b6d..8422910a1 100644 --- a/runtime/src/main/java/dev/cel/runtime/UnknownTrackingInterpretable.java +++ b/runtime/src/main/java/dev/cel/runtime/UnknownTrackingInterpretable.java @@ -15,6 +15,7 @@ package dev.cel.runtime; import dev.cel.common.annotations.Internal; +import java.util.Optional; /** * An interpretable that allows for tracking unknowns at runtime. @@ -23,6 +24,18 @@ */ @Internal public interface UnknownTrackingInterpretable { - Object evalTrackingUnknowns(RuntimeUnknownResolver resolver, CelEvaluationListener listener) - throws InterpreterException; + /** + * Runs interpretation with the given activation which supplies name/value bindings with the + * ability to track and resolve unknown variables as they are encountered. + * + *

This method allows for late-binding functions to be provided per-evaluation, which can be + * useful for binding functions which might have side-effects that are not observable to CEL + * directly such as recording telemetry or evaluation state in a more granular fashion than a more + * general evaluation listener might permit. + */ + Object evalTrackingUnknowns( + RuntimeUnknownResolver resolver, + Optional lateBoundFunctionResolver, + Optional listener) + throws CelEvaluationException; } diff --git a/runtime/src/main/java/dev/cel/runtime/async/AsyncProgramImpl.java b/runtime/src/main/java/dev/cel/runtime/async/AsyncProgramImpl.java index 98e0c4eb6..b53c6254b 100644 --- a/runtime/src/main/java/dev/cel/runtime/async/AsyncProgramImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/async/AsyncProgramImpl.java @@ -14,12 +14,14 @@ package dev.cel.runtime.async; +import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableMap.toImmutableMap; import static com.google.common.util.concurrent.Futures.immediateFailedFuture; import static com.google.common.util.concurrent.Futures.immediateFuture; import static com.google.common.util.concurrent.Futures.transformAsync; import static com.google.common.util.concurrent.Futures.whenAllSucceed; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.util.concurrent.Futures; @@ -27,7 +29,6 @@ import com.google.common.util.concurrent.ListeningExecutorService; import javax.annotation.concurrent.ThreadSafe; import dev.cel.runtime.CelAttribute; -import dev.cel.runtime.CelAttributePattern; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelRuntime.Program; import dev.cel.runtime.CelUnknownSet; @@ -55,29 +56,32 @@ final class AsyncProgramImpl implements CelAsyncRuntime.AsyncProgram { // Safety limit for resolution rounds. private final int maxEvaluateIterations; + private final UnknownContext startingUnknownContext; private final Program program; private final ListeningExecutorService executor; - private final ImmutableMap resolvers; AsyncProgramImpl( Program program, ListeningExecutorService executor, - ImmutableMap resolvers, - int maxEvaluateIterations) { + int maxEvaluateIterations, + UnknownContext startingUnknownContext) { this.program = program; this.executor = executor; - this.resolvers = resolvers; this.maxEvaluateIterations = maxEvaluateIterations; + // The following is populated from CelAsyncRuntime. The impl is immutable, thus safe to reuse as + // a starting context. + this.startingUnknownContext = startingUnknownContext; } - private Optional lookupResolver(CelAttribute attribute) { + private Optional lookupResolver( + Iterable resolvableAttributePatterns, CelAttribute attribute) { // TODO: may need to handle multiple resolvers for partial case. - for (Map.Entry entry : - resolvers.entrySet()) { - if (entry.getKey().isPartialMatch(attribute)) { - return Optional.of(entry.getValue()); + for (CelResolvableAttributePattern entry : resolvableAttributePatterns) { + if (entry.attributePattern().isPartialMatch(attribute)) { + return Optional.of(entry.resolver()); } } + return Optional.empty(); } @@ -102,10 +106,14 @@ private ListenableFuture> allAsMapOnSuccess( } private ListenableFuture resolveAndReevaluate( - CelUnknownSet unknowns, UnknownContext ctx, int iteration) { + CelUnknownSet unknowns, + UnknownContext ctx, + Iterable resolvableAttributePatterns, + int iteration) { Map> futureMap = new LinkedHashMap<>(); for (CelAttribute attr : unknowns.attributes()) { - Optional maybeResolver = lookupResolver(attr); + Optional maybeResolver = + lookupResolver(resolvableAttributePatterns, attr); maybeResolver.ifPresent((resolver) -> futureMap.put(attr, resolver.resolve(executor, attr))); } @@ -120,13 +128,21 @@ private ListenableFuture resolveAndReevaluate( // need to be configurable in the future. return transformAsync( allAsMapOnSuccess(futureMap), - (result) -> evalPass(ctx.withResolvedAttributes(result), unknowns, iteration), + (result) -> + evalPass( + ctx.withResolvedAttributes(result), + resolvableAttributePatterns, + unknowns, + iteration), executor); } private ListenableFuture evalPass( - UnknownContext ctx, CelUnknownSet lastSet, int iteration) { - Object result = null; + UnknownContext ctx, + Iterable resolvableAttributePatterns, + CelUnknownSet lastSet, + int iteration) { + Object result; try { result = program.advanceEvaluation(ctx); } catch (CelEvaluationException e) { @@ -143,14 +159,29 @@ private ListenableFuture evalPass( return immediateFailedFuture( new CelEvaluationException("Max Evaluation iterations exceeded: " + iteration)); } - return resolveAndReevaluate((CelUnknownSet) result, ctx, iteration); + return resolveAndReevaluate( + (CelUnknownSet) result, ctx, resolvableAttributePatterns, iteration); } return immediateFuture(result); } @Override - public ListenableFuture evaluateToCompletion(UnknownContext ctx) { - return evalPass(ctx, CelUnknownSet.create(ImmutableSet.of()), 0); + public ListenableFuture evaluateToCompletion( + CelResolvableAttributePattern... resolvableAttributes) { + return evaluateToCompletion(ImmutableList.copyOf(resolvableAttributes)); + } + + @Override + public ListenableFuture evaluateToCompletion( + Iterable resolvableAttributePatterns) { + UnknownContext newAsyncContext = + startingUnknownContext.extend( + ImmutableList.copyOf(resolvableAttributePatterns).stream() + .map(CelResolvableAttributePattern::attributePattern) + .collect(toImmutableList())); + + return evalPass( + newAsyncContext, resolvableAttributePatterns, CelUnknownSet.create(ImmutableSet.of()), 0); } } diff --git a/runtime/src/main/java/dev/cel/runtime/async/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/async/BUILD.bazel index 8e2042bc9..56fa42a02 100644 --- a/runtime/src/main/java/dev/cel/runtime/async/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/async/BUILD.bazel @@ -1,5 +1,7 @@ # Reference implementation for an Async evaluator for the CEL runtime. +load("@rules_java//java:defs.bzl", "java_library") + package( default_applicable_licenses = [ "//:license", @@ -11,6 +13,7 @@ package( ASYNC_RUNTIME_SOURCES = [ "AsyncProgramImpl.java", + "CelResolvableAttributePattern.java", "CelAsyncRuntimeImpl.java", "CelAsyncRuntime.java", "CelAsyncRuntimeBuilder.java", @@ -24,7 +27,7 @@ java_library( srcs = ASYNC_RUNTIME_SOURCES, deps = [ "//:auto_value", - "//common", + "//common:cel_ast", "//runtime", "//runtime:unknown_attributes", "@maven//:com_google_code_findbugs_annotations", diff --git a/runtime/src/main/java/dev/cel/runtime/async/CelAsyncRuntime.java b/runtime/src/main/java/dev/cel/runtime/async/CelAsyncRuntime.java index 113b84bc1..a8a2b789e 100644 --- a/runtime/src/main/java/dev/cel/runtime/async/CelAsyncRuntime.java +++ b/runtime/src/main/java/dev/cel/runtime/async/CelAsyncRuntime.java @@ -36,17 +36,13 @@ @ThreadSafe public interface CelAsyncRuntime { - /** - * Initialize a new async context for iterative evaluation. - * - *

This maintains the state related to tracking which parts of the environment are unknown or - * have been resolved. - */ - UnknownContext newAsyncContext(); - /** AsyncProgram wraps a CEL Program with a driver to resolve unknowns as they are encountered. */ interface AsyncProgram { - ListenableFuture evaluateToCompletion(UnknownContext ctx); + ListenableFuture evaluateToCompletion( + CelResolvableAttributePattern... resolvableAttributes); + + ListenableFuture evaluateToCompletion( + Iterable resolvableAttributes); } /** diff --git a/runtime/src/main/java/dev/cel/runtime/async/CelAsyncRuntimeBuilder.java b/runtime/src/main/java/dev/cel/runtime/async/CelAsyncRuntimeBuilder.java index 8b7dda2ef..23400fcf0 100644 --- a/runtime/src/main/java/dev/cel/runtime/async/CelAsyncRuntimeBuilder.java +++ b/runtime/src/main/java/dev/cel/runtime/async/CelAsyncRuntimeBuilder.java @@ -1,5 +1,4 @@ // 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 @@ -15,26 +14,16 @@ package dev.cel.runtime.async; import com.google.errorprone.annotations.CanIgnoreReturnValue; -import dev.cel.runtime.CelAttributePattern; import dev.cel.runtime.CelRuntime; import java.util.concurrent.ExecutorService; /** Builder interface for {@link CelAsyncRuntime}. */ public interface CelAsyncRuntimeBuilder { - public static final int DEFAULT_MAX_EVALUATE_ITERATIONS = 10; + int DEFAULT_MAX_EVALUATE_ITERATIONS = 10; /** Set the CEL runtime for running incremental evaluation. */ @CanIgnoreReturnValue - public CelAsyncRuntimeBuilder setRuntime(CelRuntime runtime); - - /** Add attributes that are declared as Unknown, without any resolver. */ - @CanIgnoreReturnValue - public CelAsyncRuntimeBuilder addUnknownAttributePatterns(CelAttributePattern... attributes); - - /** Marks an attribute pattern as unknown and associates a resolver with it. */ - @CanIgnoreReturnValue - public CelAsyncRuntimeBuilder addResolvableAttributePattern( - CelAttributePattern attribute, CelUnknownAttributeValueResolver resolver); + CelAsyncRuntimeBuilder setRuntime(CelRuntime runtime); /** * Set the maximum number of allowed evaluation passes. @@ -42,10 +31,10 @@ public CelAsyncRuntimeBuilder addResolvableAttributePattern( *

This is a safety mechanism for expressions that chain dependent unknowns (e.g. via the * conditional operator or nested function calls). * - *

Implementations should default to {@value DEFAULT_MAX_EVALUATION_ITERATIONS}. + *

Implementations should default to {@value DEFAULT_MAX_EVALUATE_ITERATIONS}. */ @CanIgnoreReturnValue - public CelAsyncRuntimeBuilder setMaxEvaluateIterations(int n); + CelAsyncRuntimeBuilder setMaxEvaluateIterations(int n); /** * Sets the variable resolver for simple CelVariable names (e.g. 'x' or 'com.google.x'). @@ -67,7 +56,7 @@ public CelAsyncRuntimeBuilder addResolvableAttributePattern( * resolvers. */ @CanIgnoreReturnValue - public CelAsyncRuntimeBuilder setExecutorService(ExecutorService executorService); + CelAsyncRuntimeBuilder setExecutorService(ExecutorService executorService); - public CelAsyncRuntime build(); + CelAsyncRuntime build(); } diff --git a/runtime/src/main/java/dev/cel/runtime/async/CelAsyncRuntimeImpl.java b/runtime/src/main/java/dev/cel/runtime/async/CelAsyncRuntimeImpl.java index fe3655294..902d9d7c6 100644 --- a/runtime/src/main/java/dev/cel/runtime/async/CelAsyncRuntimeImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/async/CelAsyncRuntimeImpl.java @@ -15,13 +15,11 @@ package dev.cel.runtime.async; import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import javax.annotation.concurrent.ThreadSafe; import dev.cel.common.CelAbstractSyntaxTree; -import dev.cel.runtime.CelAttributePattern; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelRuntime; import dev.cel.runtime.CelRuntimeFactory; @@ -32,32 +30,24 @@ /** Default {@link CelAsyncRuntime} runtime implementation. See {@link AsyncProgramImpl}. */ @ThreadSafe final class CelAsyncRuntimeImpl implements CelAsyncRuntime { - private final ImmutableMap - unknownAttributeResolvers; - private final ImmutableSet unknownAttributePatterns; private final CelRuntime runtime; private final ListeningExecutorService executorService; private final ThreadSafeCelVariableResolver variableResolver; private final int maxEvaluateIterations; private CelAsyncRuntimeImpl( - ImmutableMap unknownAttributeResolvers, - ImmutableSet unknownAttributePatterns, ThreadSafeCelVariableResolver variableResolver, CelRuntime runtime, ListeningExecutorService executorService, int maxEvaluateIterations) { - this.unknownAttributeResolvers = unknownAttributeResolvers; - this.unknownAttributePatterns = unknownAttributePatterns; this.variableResolver = variableResolver; this.runtime = runtime; this.executorService = executorService; this.maxEvaluateIterations = maxEvaluateIterations; } - @Override - public UnknownContext newAsyncContext() { - return UnknownContext.create(variableResolver, unknownAttributePatterns); + private UnknownContext newAsyncContext() { + return UnknownContext.create(variableResolver, ImmutableList.of()); } @Override @@ -65,8 +55,8 @@ public AsyncProgram createProgram(CelAbstractSyntaxTree ast) throws CelEvaluatio return new AsyncProgramImpl( runtime.createProgram(ast), executorService, - unknownAttributeResolvers, - maxEvaluateIterations); + maxEvaluateIterations, + newAsyncContext()); } static Builder newBuilder() { @@ -76,17 +66,12 @@ static Builder newBuilder() { /** {@link CelAsyncRuntimeBuilder} implementation for {@link CelAsyncRuntimeImpl}. */ private static final class Builder implements CelAsyncRuntimeBuilder { private CelRuntime runtime; - private final ImmutableSet.Builder unknownAttributePatterns; - private final ImmutableMap.Builder - unknownAttributeResolvers; private ListeningExecutorService executorService; private Optional variableResolver; private int maxEvaluateIterations; private Builder() { runtime = CelRuntimeFactory.standardCelRuntimeBuilder().build(); - unknownAttributeResolvers = ImmutableMap.builder(); - unknownAttributePatterns = ImmutableSet.builder(); variableResolver = Optional.empty(); maxEvaluateIterations = DEFAULT_MAX_EVALUATE_ITERATIONS; } @@ -97,20 +82,6 @@ public Builder setRuntime(CelRuntime runtime) { return this; } - @Override - public Builder addUnknownAttributePatterns(CelAttributePattern... attributes) { - unknownAttributePatterns.add(attributes); - return this; - } - - @Override - public Builder addResolvableAttributePattern( - CelAttributePattern attribute, CelUnknownAttributeValueResolver resolver) { - unknownAttributeResolvers.put(attribute, resolver); - unknownAttributePatterns.add(attribute); - return this; - } - @Override public Builder setMaxEvaluateIterations(int n) { Preconditions.checkArgument(n > 0, "maxEvaluateIterations must be positive"); @@ -134,8 +105,6 @@ public Builder setExecutorService(ExecutorService executorService) { public CelAsyncRuntime build() { Preconditions.checkNotNull(executorService, "executorService must be specified."); return new CelAsyncRuntimeImpl( - unknownAttributeResolvers.buildOrThrow(), - unknownAttributePatterns.build(), variableResolver.orElse((unused) -> Optional.empty()), runtime, executorService, diff --git a/runtime/src/main/java/dev/cel/runtime/async/CelResolvableAttributePattern.java b/runtime/src/main/java/dev/cel/runtime/async/CelResolvableAttributePattern.java new file mode 100644 index 000000000..8e243476b --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/async/CelResolvableAttributePattern.java @@ -0,0 +1,36 @@ +// 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.async; + +import com.google.auto.value.AutoValue; +import dev.cel.runtime.CelAttributePattern; + +/** + * CelResolvableAttributePattern wraps {@link CelAttributePattern} to represent a CEL attribute + * whose value is initially unknown and needs to be resolved. It couples the attribute pattern with + * a {@link CelUnknownAttributeValueResolver} that can fetch the actual value for the attribute when + * it becomes available. + */ +@AutoValue +public abstract class CelResolvableAttributePattern { + public abstract CelAttributePattern attributePattern(); + + public abstract CelUnknownAttributeValueResolver resolver(); + + public static CelResolvableAttributePattern of( + CelAttributePattern attribute, CelUnknownAttributeValueResolver resolver) { + return new AutoValue_CelResolvableAttributePattern(attribute, resolver); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/ActivationWrapper.java b/runtime/src/main/java/dev/cel/runtime/planner/ActivationWrapper.java new file mode 100644 index 000000000..8883ac12c --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/ActivationWrapper.java @@ -0,0 +1,25 @@ +// 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.GlobalResolver; + +/** 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/Attribute.java b/runtime/src/main/java/dev/cel/runtime/planner/Attribute.java new file mode 100644 index 000000000..90165c1ac --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/Attribute.java @@ -0,0 +1,26 @@ +// 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.errorprone.annotations.Immutable; +import dev.cel.runtime.GlobalResolver; + +/** Represents a resolvable symbol or path (such as a variable or a field selection). */ +@Immutable +interface Attribute { + Object resolve(long exprId, GlobalResolver ctx, ExecutionFrame frame); + + Attribute addQualifier(Qualifier qualifier); +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/AttributeFactory.java b/runtime/src/main/java/dev/cel/runtime/planner/AttributeFactory.java new file mode 100644 index 000000000..fabf7ade5 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/AttributeFactory.java @@ -0,0 +1,63 @@ +// 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.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.CelContainer; +import dev.cel.common.types.CelTypeProvider; +import dev.cel.common.values.CelValueConverter; + +@Immutable +final class AttributeFactory { + + private final CelContainer container; + private final CelTypeProvider typeProvider; + private final CelValueConverter celValueConverter; + + NamespacedAttribute newAbsoluteAttribute(String... names) { + return NamespacedAttribute.create(typeProvider, celValueConverter, ImmutableSet.copyOf(names)); + } + + RelativeAttribute newRelativeAttribute(PlannedInterpretable operand) { + return new RelativeAttribute(operand, celValueConverter); + } + + MaybeAttribute newMaybeAttribute(String name) { + // When there's a single name with a dot prefix, it indicates that the 'maybe' attribute is a + // globally namespaced identifier. + // Otherwise, the candidate names resolved from the container should be inferred. + ImmutableSet names = + name.startsWith(".") ? ImmutableSet.of(name) : container.resolveCandidateNames(name); + + return new MaybeAttribute( + this, ImmutableList.of(NamespacedAttribute.create(typeProvider, celValueConverter, names))); + } + + static AttributeFactory newAttributeFactory( + CelContainer celContainer, + CelTypeProvider typeProvider, + CelValueConverter celValueConverter) { + return new AttributeFactory(celContainer, typeProvider, celValueConverter); + } + + private AttributeFactory( + CelContainer container, CelTypeProvider typeProvider, CelValueConverter celValueConverter) { + this.container = container; + this.typeProvider = typeProvider; + this.celValueConverter = celValueConverter; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel new file mode 100644 index 000000000..801e56d73 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel @@ -0,0 +1,557 @@ +load("@rules_java//java:defs.bzl", "java_library") + +package( + default_applicable_licenses = ["//:license"], + default_visibility = [ + "//runtime/planner:__pkg__", + ], +) + +java_library( + name = "program_planner", + srcs = ["ProgramPlanner.java"], + tags = [ + ], + deps = [ + ":attribute", + ":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_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", + "//common:operator", + "//common:options", + "//common/annotations", + "//common/ast", + "//common/exceptions:overload_not_found", + "//common/types", + "//common/types:type_providers", + "//common/values", + "//common/values:cel_value_provider", + "//runtime:dispatcher", + "//runtime:evaluation_exception", + "//runtime:evaluation_exception_builder", + "//runtime:program", + "//runtime:resolved_overload", + "@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", + ":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//:org_jspecify_jspecify", + ], +) + +java_library( + name = "eval_const", + srcs = ["EvalConstant.java"], + deps = [ + ":planned_interpretable", + "//common/ast", + "//runtime:interpretable", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +java_library( + name = "interpretable_attribute", + srcs = ["InterpretableAttribute.java"], + deps = [ + ":planned_interpretable", + ":qualifier", + "//common/ast", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +java_library( + name = "attribute", + srcs = [ + "Attribute.java", + "AttributeFactory.java", + "MaybeAttribute.java", + "MissingAttribute.java", + "NamespacedAttribute.java", + "RelativeAttribute.java", + ], + deps = [ + ":activation_wrapper", + ":eval_helpers", + ":planned_interpretable", + ":qualifier", + "//common:container", + "//common/exceptions:attribute_not_found", + "//common/types", + "//common/types:type_providers", + "//common/values", + "//runtime:accumulated_unknowns", + "//runtime:interpretable", + "//runtime:interpreter_util", + "//runtime:partial_vars", + "//runtime:unknown_attributes", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven//:org_jspecify_jspecify", + ], +) + +java_library( + name = "activation_wrapper", + srcs = ["ActivationWrapper.java"], + deps = [ + "//runtime:interpretable", + ], +) + +java_library( + name = "qualifier", + srcs = ["Qualifier.java"], + deps = [ + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +java_library( + name = "presence_test_qualifier", + srcs = ["PresenceTestQualifier.java"], + deps = [ + ":attribute", + ":qualifier", + "//common/values", + ], +) + +java_library( + name = "string_qualifier", + srcs = ["StringQualifier.java"], + deps = [ + ":qualifier", + "//common/exceptions:attribute_not_found", + "//common/values", + ], +) + +java_library( + name = "eval_attribute", + srcs = ["EvalAttribute.java"], + deps = [ + ":attribute", + ":interpretable_attribute", + ":planned_interpretable", + ":qualifier", + "//common/ast", + "//runtime:interpretable", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +java_library( + name = "eval_test_only", + srcs = ["EvalTestOnly.java"], + deps = [ + ":interpretable_attribute", + ":planned_interpretable", + ":presence_test_qualifier", + ":qualifier", + "//common/ast", + "//runtime:evaluation_exception", + "//runtime:interpretable", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +java_library( + name = "eval_zero_arity", + srcs = ["EvalZeroArity.java"], + deps = [ + ":eval_helpers", + ":planned_interpretable", + "//common/ast", + "//common/values", + "//runtime:evaluation_exception", + "//runtime:interpretable", + "//runtime:resolved_overload", + ], +) + +java_library( + name = "eval_unary", + srcs = ["EvalUnary.java"], + deps = [ + ":eval_helpers", + ":planned_interpretable", + "//common/ast", + "//common/values", + "//runtime:evaluation_exception", + "//runtime:interpretable", + "//runtime:resolved_overload", + ], +) + +java_library( + name = "eval_binary", + srcs = ["EvalBinary.java"], + deps = [ + ":eval_helpers", + ":planned_interpretable", + "//common/ast", + "//common/values", + "//runtime:accumulated_unknowns", + "//runtime:evaluation_exception", + "//runtime:interpretable", + "//runtime:resolved_overload", + ], +) + +java_library( + name = "eval_var_args_call", + srcs = ["EvalVarArgsCall.java"], + deps = [ + ":eval_helpers", + ":planned_interpretable", + "//common/ast", + "//common/values", + "//runtime:accumulated_unknowns", + "//runtime:evaluation_exception", + "//runtime:interpretable", + "//runtime:resolved_overload", + ], +) + +java_library( + name = "eval_late_bound_call", + srcs = ["EvalLateBoundCall.java"], + deps = [ + ":eval_helpers", + ":planned_interpretable", + "//common/ast", + "//common/exceptions:overload_not_found", + "//common/values", + "//runtime:accumulated_unknowns", + "//runtime:evaluation_exception", + "//runtime:interpretable", + "//runtime:resolved_overload", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "eval_or", + srcs = ["EvalOr.java"], + deps = [ + ":eval_helpers", + ":planned_interpretable", + "//common/ast", + "//common/values", + "//runtime:accumulated_unknowns", + "//runtime:interpretable", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "eval_and", + srcs = ["EvalAnd.java"], + deps = [ + ":eval_helpers", + ":planned_interpretable", + "//common/ast", + "//common/values", + "//runtime:accumulated_unknowns", + "//runtime:interpretable", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "eval_conditional", + srcs = ["EvalConditional.java"], + deps = [ + ":planned_interpretable", + "//common/ast", + "//runtime:accumulated_unknowns", + "//runtime:evaluation_exception", + "//runtime:interpretable", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "eval_create_struct", + srcs = ["EvalCreateStruct.java"], + deps = [ + ":eval_helpers", + ":planned_interpretable", + "//common/ast", + "//common/types:type_providers", + "//common/values", + "//common/values:cel_value_provider", + "//runtime:accumulated_unknowns", + "//runtime:interpretable", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "eval_create_list", + srcs = ["EvalCreateList.java"], + 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 = "eval_create_map", + srcs = ["EvalCreateMap.java"], + deps = [ + ":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 = "eval_fold", + srcs = ["EvalFold.java"], + deps = [ + ":activation_wrapper", + ":planned_interpretable", + "//common/ast", + "//common/exceptions:runtime_exception", + "//common/values:mutable_map_value", + "//runtime:accumulated_unknowns", + "//runtime:concatenated_list_view", + "//runtime:evaluation_exception", + "//runtime:interpretable", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven//:org_jspecify_jspecify", + ], +) + +java_library( + name = "eval_helpers", + srcs = ["EvalHelpers.java"], + deps = [ + ":localized_evaluation_exception", + ":planned_interpretable", + "//common:error_codes", + "//common/exceptions:runtime_exception", + "//common/values", + "//runtime:evaluation_exception", + "//runtime:interpretable", + "//runtime:resolved_overload", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "localized_evaluation_exception", + srcs = ["LocalizedEvaluationException.java"], + deps = [ + "//common:error_codes", + "//common/exceptions:runtime_exception", + ], +) + +java_library( + name = "error_metadata", + srcs = ["ErrorMetadata.java"], + deps = [ + "//runtime:metadata", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "planned_interpretable", + srcs = [ + "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", + ], +) + +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_optional_or_value", + srcs = ["EvalOptionalOrValue.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_optional_select_field", + srcs = ["EvalOptionalSelectField.java"], + deps = [ + ":eval_helpers", + ":planned_interpretable", + "//common/ast", + "//common/values", + "//runtime:accumulated_unknowns", + "//runtime:interpretable", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "eval_exhaustive_and", + srcs = ["EvalExhaustiveAnd.java"], + deps = [ + ":eval_helpers", + ":planned_interpretable", + "//common/ast", + "//common/values", + "//runtime:accumulated_unknowns", + "//runtime:interpretable", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +java_library( + name = "eval_exhaustive_or", + srcs = ["EvalExhaustiveOr.java"], + deps = [ + ":eval_helpers", + ":planned_interpretable", + "//common/ast", + "//common/values", + "//runtime:accumulated_unknowns", + "//runtime:interpretable", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +java_library( + name = "eval_exhaustive_conditional", + srcs = ["EvalExhaustiveConditional.java"], + deps = [ + ":eval_helpers", + ":planned_interpretable", + "//common/ast", + "//runtime:accumulated_unknowns", + "//runtime:evaluation_exception", + "//runtime:interpretable", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +java_library( + name = "eval_block", + srcs = ["EvalBlock.java"], + deps = [ + ":planned_interpretable", + "//common/ast", + "//runtime:evaluation_exception", + "//runtime:interpretable", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) diff --git a/runtime/src/main/java/dev/cel/runtime/planner/BlockMemoizer.java b/runtime/src/main/java/dev/cel/runtime/planner/BlockMemoizer.java 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/ErrorMetadata.java b/runtime/src/main/java/dev/cel/runtime/planner/ErrorMetadata.java new file mode 100644 index 000000000..2358bd0f9 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/ErrorMetadata.java @@ -0,0 +1,52 @@ +// 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 com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.collect.ImmutableMap; +import com.google.errorprone.annotations.Immutable; +import dev.cel.runtime.Metadata; + +@Immutable +final class ErrorMetadata implements Metadata { + + private final ImmutableMap exprIdToPositionMap; + private final String location; + + @Override + public String getLocation() { + return location; + } + + @Override + public int getPosition(long exprId) { + return exprIdToPositionMap.getOrDefault(exprId, 0); + } + + @Override + public boolean hasPosition(long exprId) { + return exprIdToPositionMap.containsKey(exprId); + } + + static ErrorMetadata create(ImmutableMap exprIdToPositionMap, String location) { + return new ErrorMetadata(exprIdToPositionMap, location); + } + + private ErrorMetadata(ImmutableMap exprIdToPositionMap, String location) { + this.exprIdToPositionMap = checkNotNull(exprIdToPositionMap); + this.location = checkNotNull(location); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalAnd.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalAnd.java new file mode 100644 index 000000000..11da26a50 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalAnd.java @@ -0,0 +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.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 new file mode 100644 index 000000000..56ea8a832 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalAttribute.java @@ -0,0 +1,50 @@ +// 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.errorprone.annotations.Immutable; +import dev.cel.common.ast.CelExpr; +import dev.cel.runtime.GlobalResolver; + +@Immutable +final class EvalAttribute extends InterpretableAttribute { + + private final Attribute attr; + + @Override + Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) { + Object resolved = attr.resolve(expr().id(), resolver, frame); + if (resolved instanceof MissingAttribute) { + ((MissingAttribute) resolved).resolve(expr().id(), resolver, frame); + } + + return resolved; + } + + @Override + public EvalAttribute addQualifier(CelExpr expr, Qualifier qualifier) { + Attribute newAttribute = attr.addQualifier(qualifier); + return create(expr, newAttribute); + } + + static EvalAttribute create(CelExpr expr, Attribute attr) { + return new EvalAttribute(expr, attr); + } + + 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..fcade7789 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalBinary.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 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 { + Object argVal1 = + resolvedOverload.isStrict() + ? evalStrictly(arg1, resolver, frame) + : evalNonstrictly(arg1, resolver, frame); + Object argVal2 = + resolvedOverload.isStrict() + ? evalStrictly(arg2, resolver, frame) + : evalNonstrictly(arg2, resolver, frame); + + AccumulatedUnknowns unknowns = AccumulatedUnknowns.maybeMerge(null, argVal1); + unknowns = AccumulatedUnknowns.maybeMerge(unknowns, argVal2); + + if (unknowns != null) { + return unknowns; + } + + return EvalHelpers.dispatch( + 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 new file mode 100644 index 000000000..c2d730cdf --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalConditional.java @@ -0,0 +1,59 @@ +// 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.Preconditions; +import dev.cel.common.ast.CelExpr; +import dev.cel.runtime.AccumulatedUnknowns; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.GlobalResolver; + +final class EvalConditional 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); + if (condResult instanceof AccumulatedUnknowns) { + return condResult; + } + if (!(condResult instanceof Boolean)) { + throw new IllegalArgumentException( + String.format("Expected boolean value, found :%s", condResult)); + } + + // TODO: Handle exhaustive eval + if ((boolean) condResult) { + return truthy.eval(resolver, frame); + } + + return falsy.eval(resolver, frame); + } + + static EvalConditional create(CelExpr expr, PlannedInterpretable[] args) { + return new EvalConditional(expr, args); + } + + private EvalConditional(CelExpr expr, PlannedInterpretable[] args) { + super(expr); + Preconditions.checkArgument(args.length == 3); + this.args = args; + } +} diff --git a/common/src/main/java/dev/cel/common/values/StringValue.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalConstant.java similarity index 51% rename from common/src/main/java/dev/cel/common/values/StringValue.java rename to runtime/src/main/java/dev/cel/runtime/planner/EvalConstant.java index e14c8979c..55554069b 100644 --- a/common/src/main/java/dev/cel/common/values/StringValue.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalConstant.java @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// 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. @@ -12,32 +12,29 @@ // See the License for the specific language governing permissions and // limitations under the License. -package dev.cel.common.values; +package dev.cel.runtime.planner; -import com.google.auto.value.AutoValue; import com.google.errorprone.annotations.Immutable; -import dev.cel.common.types.CelType; -import dev.cel.common.types.SimpleType; +import dev.cel.common.ast.CelExpr; +import dev.cel.runtime.GlobalResolver; -/** StringValue is a simple CelValue wrapper around Java strings. */ -@AutoValue @Immutable -public abstract class StringValue extends CelValue { +final class EvalConstant extends PlannedInterpretable { - @Override - public abstract String value(); + @SuppressWarnings("Immutable") // Known CEL constants that aren't mutated are stored + private final Object constant; @Override - public boolean isZeroValue() { - return value().isEmpty(); + Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) { + return constant; } - @Override - public CelType celType() { - return SimpleType.STRING; + static EvalConstant create(CelExpr expr, Object value) { + return new EvalConstant(expr, value); } - public static StringValue create(String value) { - return new AutoValue_StringValue(value); + 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 new file mode 100644 index 000000000..265da3ab3 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateList.java @@ -0,0 +1,78 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import com.google.common.collect.ImmutableList; +import com.google.errorprone.annotations.Immutable; +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 { + + @SuppressWarnings("Immutable") + private final PlannedInterpretable[] values; + + @SuppressWarnings("Immutable") + private final boolean[] isOptional; + + @Override + Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) { + ImmutableList.Builder builder = ImmutableList.builderWithExpectedSize(values.length); + AccumulatedUnknowns unknowns = null; + for (int i = 0; i < values.length; i++) { + Object element = EvalHelpers.evalStrictly(values[i], resolver, frame); + + if (element instanceof AccumulatedUnknowns) { + unknowns = AccumulatedUnknowns.maybeMerge(unknowns, element); + continue; + } + + if (isOptional[i]) { + if (!(element instanceof Optional)) { + throw new IllegalArgumentException( + 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(CelExpr expr, PlannedInterpretable[] values, boolean[] isOptional) { + return new EvalCreateList(expr, values, isOptional); + } + + 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 new file mode 100644 index 000000000..8d34c10d0 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateMap.java @@ -0,0 +1,142 @@ +// 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.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 { + + // Array contents are not mutated + @SuppressWarnings("Immutable") + private final PlannedInterpretable[] keys; + + // Array contents are not mutated + @SuppressWarnings("Immutable") + private final PlannedInterpretable[] values; + + // Array contents are not mutated + @SuppressWarnings("Immutable") + private final boolean[] isOptional; + + @Override + 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++) { + 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()); + } + + 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()); + } + } + + 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( + CelExpr expr, + PlannedInterpretable[] keys, + PlannedInterpretable[] values, + boolean[] isOptional) { + return new EvalCreateMap(expr, keys, values, isOptional); + } + + 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 new file mode 100644 index 000000000..a2e8a9da6 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateStruct.java @@ -0,0 +1,121 @@ +// 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.collect.Maps; +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.ast.CelExpr; +import dev.cel.common.types.CelType; +import dev.cel.common.values.CelValueProvider; +import dev.cel.common.values.StructValue; +import dev.cel.runtime.AccumulatedUnknowns; +import dev.cel.runtime.GlobalResolver; +import java.util.Collections; +import java.util.Map; +import java.util.Optional; + +@Immutable +final class EvalCreateStruct extends PlannedInterpretable { + + private final CelValueProvider valueProvider; + private final CelType structType; + + // Array contents are not mutated + @SuppressWarnings("Immutable") + private final String[] keys; + + // Array contents are not mutated + @SuppressWarnings("Immutable") + private final PlannedInterpretable[] values; + + // Array contents are not mutated + @SuppressWarnings("Immutable") + private final boolean[] isOptional; + + @Override + 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 = 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 value; + } + + static EvalCreateStruct create( + CelExpr expr, + CelValueProvider valueProvider, + CelType structType, + String[] keys, + PlannedInterpretable[] values, + boolean[] isOptional) { + return new EvalCreateStruct(expr, valueProvider, structType, keys, values, isOptional); + } + + private EvalCreateStruct( + CelExpr expr, + CelValueProvider valueProvider, + CelType structType, + String[] keys, + 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 new file mode 100644 index 000000000..2de52e982 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalFold.java @@ -0,0 +1,240 @@ +// 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.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; +import java.util.Collection; +import java.util.Map; +import org.jspecify.annotations.Nullable; + +@Immutable +final class EvalFold extends PlannedInterpretable { + + private final String accuVar; + private final PlannedInterpretable accuInit; + private final String iterVar; + private final String iterVar2; + private final PlannedInterpretable iterRange; + private final PlannedInterpretable condition; + private final PlannedInterpretable loopStep; + private final PlannedInterpretable result; + + static EvalFold create( + CelExpr expr, + String accuVar, + PlannedInterpretable accuInit, + String iterVar, + String iterVar2, + PlannedInterpretable iterRange, + PlannedInterpretable loopCondition, + PlannedInterpretable loopStep, + PlannedInterpretable result) { + return new EvalFold( + expr, accuVar, accuInit, iterVar, iterVar2, iterRange, loopCondition, loopStep, result); + } + + private EvalFold( + CelExpr expr, + String accuVar, + PlannedInterpretable accuInit, + String iterVar, + String iterVar2, + PlannedInterpretable iterRange, + PlannedInterpretable condition, + PlannedInterpretable loopStep, + PlannedInterpretable result) { + super(expr); + this.accuVar = accuVar; + this.accuInit = accuInit; + this.iterVar = iterVar; + this.iterVar2 = iterVar2; + this.iterRange = iterRange; + this.condition = condition; + this.loopStep = loopStep; + this.result = result; + } + + @Override + Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { + Object iterRangeRaw = iterRange.eval(resolver, frame); + if (iterRangeRaw instanceof AccumulatedUnknowns) { + return iterRangeRaw; + } + Folder folder = new Folder(resolver, frame, accuInit, accuVar, iterVar, iterVar2); + + Object result; + if (iterRangeRaw instanceof Map) { + result = evalMap((Map) iterRangeRaw, folder, frame); + } else if (iterRangeRaw instanceof Collection) { + result = evalList((Collection) iterRangeRaw, folder, frame); + } else { + throw new IllegalArgumentException("Unexpected iter_range type: " + iterRangeRaw.getClass()); + } + + return maybeUnwrapAccumulator(result); + } + + private Object evalMap(Map iterRange, Folder folder, ExecutionFrame frame) + throws CelEvaluationException { + for (Map.Entry entry : iterRange.entrySet()) { + frame.incrementIterations(); + + folder.iterVarVal = entry.getKey(); + if (!iterVar2.isEmpty()) { + folder.iterVar2Val = entry.getValue(); + } + + boolean cond = (boolean) condition.eval(folder, frame); + if (!cond) { + folder.computeResult = true; + return result.eval(folder, frame); + } + + folder.accuVal = loopStep.eval(folder, frame); + folder.initialized = true; + } + folder.computeResult = true; + return result.eval(folder, frame); + } + + private Object evalList(Collection iterRange, Folder folder, ExecutionFrame frame) + throws CelEvaluationException { + int index = 0; + for (Object item : iterRange) { + frame.incrementIterations(); + + if (iterVar2.isEmpty()) { + folder.iterVarVal = item; + } else { + folder.iterVarVal = (long) index; + folder.iterVar2Val = item; + } + + boolean cond = (boolean) condition.eval(folder, frame); + if (!cond) { + folder.computeResult = true; + return maybeUnwrapAccumulator(result.eval(folder, frame)); + } + + folder.accuVal = loopStep.eval(folder, frame); + folder.initialized = true; + index++; + } + folder.computeResult = true; + return maybeUnwrapAccumulator(result.eval(folder, frame)); + } + + private static Object maybeWrapAccumulator(Object val) { + if (val instanceof Collection) { + return new ConcatenatedListView<>((Collection) val); + } + if (val instanceof Map) { + return MutableMapValue.create((Map) val); + } + return val; + } + + private static Object maybeUnwrapAccumulator(Object val) { + if (val instanceof ConcatenatedListView) { + return ImmutableList.copyOf((ConcatenatedListView) val); + } + 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; + + private Object iterVarVal; + private Object iterVar2Val; + private Object accuVal; + private boolean initialized = false; + private boolean computeResult = false; + + private Folder( + GlobalResolver resolver, + ExecutionFrame frame, + PlannedInterpretable accuInit, + String accuVar, + String iterVar, + String iterVar2) { + this.resolver = resolver; + this.frame = frame; + this.accuInit = accuInit; + this.accuVar = accuVar; + this.iterVar = iterVar; + this.iterVar2 = iterVar2; + } + + @Override + 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 (!computeResult) { + if (name.equals(iterVar)) { + return this.iterVarVal; + } + + 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 new file mode 100644 index 000000000..f9812793e --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java @@ -0,0 +1,116 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import com.google.common.base.Joiner; +import dev.cel.common.CelErrorCode; +import dev.cel.common.exceptions.CelRuntimeException; +import dev.cel.common.values.CelValueConverter; +import dev.cel.common.values.ErrorValue; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.CelResolvedOverload; +import dev.cel.runtime.GlobalResolver; + +final class EvalHelpers { + + static Object evalNonstrictly( + PlannedInterpretable interpretable, GlobalResolver resolver, ExecutionFrame frame) { + try { + return interpretable.eval(resolver, frame); + } catch (LocalizedEvaluationException e) { + // Intercept the localized exception to get a more specific expr ID for error reporting + // Example: foo [1] && strict_err [2] -> ID 2 is propagated. + return ErrorValue.create(e.exprId(), e); + } catch (Exception e) { + return ErrorValue.create(interpretable.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 valueConverter.maybeUnwrap(valueConverter.toRuntimeValue(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 valueConverter.maybeUnwrap(valueConverter.toRuntimeValue(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 valueConverter.maybeUnwrap(valueConverter.toRuntimeValue(result)); + } catch (RuntimeException e) { + throw handleDispatchException(e, overload, arg1, arg2); + } + } + + private static RuntimeException handleDispatchException( + RuntimeException e, CelResolvedOverload overload, Object... args) { + if (e instanceof CelRuntimeException) { + // Function dispatch failure that's already been handled -- just propagate. + return e; + } + // Unexpected function dispatch failure. + return new IllegalArgumentException( + String.format( + "Function '%s' failed with arg(s) '%s'", + overload.getFunctionName(), Joiner.on(", ").join(args)), + e); + } + + private EvalHelpers() {} +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalLateBoundCall.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalLateBoundCall.java new file mode 100644 index 000000000..719b4af21 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalLateBoundCall.java @@ -0,0 +1,83 @@ +// 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.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; + +final class EvalLateBoundCall extends PlannedInterpretable { + + private final String functionName; + private final ImmutableList overloadIds; + + @SuppressWarnings("Immutable") + private final PlannedInterpretable[] args; + + private final CelValueConverter celValueConverter; + + @Override + 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 = + frame + .findOverload(functionName, overloadIds, argVals) + .orElseThrow(() -> new CelOverloadNotFoundException(functionName, overloadIds)); + + return EvalHelpers.dispatch(functionName, resolvedOverload, celValueConverter, argVals); + } + + static EvalLateBoundCall create( + CelExpr expr, + String functionName, + ImmutableList overloadIds, + PlannedInterpretable[] args, + CelValueConverter celValueConverter) { + return new EvalLateBoundCall(expr, functionName, overloadIds, args, celValueConverter); + } + + private EvalLateBoundCall( + CelExpr expr, + String functionName, + ImmutableList overloadIds, + PlannedInterpretable[] args, + CelValueConverter celValueConverter) { + super(expr); + this.functionName = functionName; + this.overloadIds = overloadIds; + this.args = args; + this.celValueConverter = celValueConverter; + } +} 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 new file mode 100644 index 000000000..849b6e7b4 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalOr.java @@ -0,0 +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.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 new file mode 100644 index 000000000..b3d2563f0 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalTestOnly.java @@ -0,0 +1,46 @@ +// 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.errorprone.annotations.Immutable; +import dev.cel.common.ast.CelExpr; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.GlobalResolver; + +@Immutable +final class EvalTestOnly extends InterpretableAttribute { + + private final InterpretableAttribute attr; + + @Override + Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { + return attr.eval(resolver, frame); + } + + @Override + public EvalTestOnly addQualifier(CelExpr expr, Qualifier qualifier) { + PresenceTestQualifier presenceTestQualifier = PresenceTestQualifier.create(qualifier.value()); + return new EvalTestOnly(expr(), attr.addQualifier(expr, presenceTestQualifier)); + } + + static EvalTestOnly create(CelExpr expr, InterpretableAttribute attr) { + return new EvalTestOnly(expr, attr); + } + + 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 new file mode 100644 index 000000000..867371ff1 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalUnary.java @@ -0,0 +1,63 @@ +// 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 static dev.cel.runtime.planner.EvalHelpers.evalStrictly; + +import dev.cel.common.ast.CelExpr; +import dev.cel.common.values.CelValueConverter; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.CelResolvedOverload; +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 + Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { + Object argVal = + resolvedOverload.isStrict() + ? evalStrictly(arg, resolver, frame) + : evalNonstrictly(arg, resolver, frame); + return EvalHelpers.dispatch(functionName, resolvedOverload, celValueConverter, argVal); + } + + static EvalUnary create( + CelExpr expr, + String functionName, + CelResolvedOverload resolvedOverload, + PlannedInterpretable arg, + CelValueConverter celValueConverter) { + return new EvalUnary(expr, functionName, resolvedOverload, arg, celValueConverter); + } + + private EvalUnary( + CelExpr expr, + String functionName, + CelResolvedOverload resolvedOverload, + PlannedInterpretable arg, + CelValueConverter celValueConverter) { + 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 new file mode 100644 index 000000000..4b0171b8f --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalVarArgsCall.java @@ -0,0 +1,79 @@ +// 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 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") + private final PlannedInterpretable[] args; + + private final CelValueConverter celValueConverter; + + @Override + 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]; + argVals[i] = + resolvedOverload.isStrict() + ? evalStrictly(arg, resolver, frame) + : evalNonstrictly(arg, resolver, frame); + + unknowns = AccumulatedUnknowns.maybeMerge(unknowns, argVals[i]); + } + + if (unknowns != null) { + return unknowns; + } + + return EvalHelpers.dispatch(functionName, resolvedOverload, celValueConverter, argVals); + } + + static EvalVarArgsCall create( + CelExpr expr, + String functionName, + CelResolvedOverload resolvedOverload, + PlannedInterpretable[] args, + CelValueConverter celValueConverter) { + return new EvalVarArgsCall(expr, functionName, resolvedOverload, args, celValueConverter); + } + + private EvalVarArgsCall( + CelExpr expr, + String functionName, + CelResolvedOverload resolvedOverload, + PlannedInterpretable[] args, + CelValueConverter celValueConverter) { + 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 new file mode 100644 index 000000000..6e35bc22b --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalZeroArity.java @@ -0,0 +1,53 @@ +// 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 dev.cel.common.ast.CelExpr; +import dev.cel.common.values.CelValueConverter; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.CelResolvedOverload; +import dev.cel.runtime.GlobalResolver; + +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 + Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { + return EvalHelpers.dispatch(functionName, resolvedOverload, celValueConverter, EMPTY_ARRAY); + } + + static EvalZeroArity create( + CelExpr expr, + String functionName, + CelResolvedOverload resolvedOverload, + CelValueConverter celValueConverter) { + return new EvalZeroArity(expr, functionName, resolvedOverload, celValueConverter); + } + + private EvalZeroArity( + 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 new file mode 100644 index 000000000..b67f5520c --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/ExecutionFrame.java @@ -0,0 +1,94 @@ +// 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 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) + throws CelEvaluationException { + if (overloadIds.isEmpty()) { + return functionResolver.findOverloadMatchingArgs(functionName, args); + } + return functionResolver.findOverloadMatchingArgs(functionName, overloadIds, args); + } + + void incrementIterations() { + if (comprehensionIterationLimit < 0) { + return; + } + if (++iterationCount > comprehensionIterationLimit) { + throw new CelIterationLimitExceededException(comprehensionIterationLimit); + } + } + + 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, + @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 new file mode 100644 index 000000000..9ce726f0e --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/InterpretableAttribute.java @@ -0,0 +1,28 @@ +// 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.errorprone.annotations.Immutable; +import dev.cel.common.ast.CelExpr; + +@Immutable +abstract class InterpretableAttribute extends PlannedInterpretable { + + abstract InterpretableAttribute addQualifier(CelExpr expr, Qualifier qualifier); + + InterpretableAttribute(CelExpr expr) { + super(expr); + } +} \ No newline at end of file diff --git a/runtime/src/main/java/dev/cel/runtime/planner/LocalizedEvaluationException.java b/runtime/src/main/java/dev/cel/runtime/planner/LocalizedEvaluationException.java new file mode 100644 index 000000000..603f0b0a5 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/LocalizedEvaluationException.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.planner; + +import dev.cel.common.CelErrorCode; +import dev.cel.common.exceptions.CelRuntimeException; + +/** + * Wraps a {@link CelRuntimeException} with its source expression ID for error reporting. + * + *

This is the ONLY exception type that propagates through evaluation in the planner. All + * CelRuntimeExceptions from runtime helpers are immediately wrapped with location information to + * track where the error occurred in the expression tree. + * + *

Note: This exception should not be surfaced directly to users - it's unwrapped in {@link + * PlannedProgram}. + */ +final class LocalizedEvaluationException extends CelRuntimeException { + + private final long exprId; + + long exprId() { + return exprId; + } + + LocalizedEvaluationException(CelRuntimeException cause, long exprId) { + this(cause, cause.getErrorCode(), exprId); + } + + LocalizedEvaluationException(Throwable cause, CelErrorCode errorCode, long exprId) { + super(cause, errorCode); + this.exprId = exprId; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/MaybeAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/MaybeAttribute.java new file mode 100644 index 000000000..1506eb180 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/MaybeAttribute.java @@ -0,0 +1,81 @@ +// 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.collect.ImmutableList; +import com.google.errorprone.annotations.Immutable; +import dev.cel.runtime.GlobalResolver; + +/** + * An attribute that attempts to resolve a variable against a list of potential namespaced + * attributes. This is used during parsed-only evaluation. + */ +@Immutable +final class MaybeAttribute implements Attribute { + private final AttributeFactory attrFactory; + private final ImmutableList attributes; + + @Override + public Object resolve(long exprId, GlobalResolver ctx, ExecutionFrame frame) { + MissingAttribute maybeError = null; + for (NamespacedAttribute attr : attributes) { + Object value = attr.resolve(exprId, ctx, frame); + if (value == null) { + continue; + } + + if (value instanceof MissingAttribute) { + maybeError = (MissingAttribute) value; + // When the variable is missing in a maybe attribute, defer erroring. + // The variable may exist in other namespaced attributes. + continue; + } + + return value; + } + + return maybeError; + } + + @Override + public Attribute addQualifier(Qualifier qualifier) { + Object strQualifier = qualifier.value(); + ImmutableList.Builder augmentedNamesBuilder = ImmutableList.builder(); + ImmutableList.Builder attributesBuilder = ImmutableList.builder(); + for (NamespacedAttribute attr : attributes) { + if (strQualifier instanceof String && attr.qualifiers().isEmpty()) { + for (String varName : attr.candidateVariableNames()) { + augmentedNamesBuilder.add(varName + "." + strQualifier); + } + } + + attributesBuilder.add(attr.addQualifier(qualifier)); + } + ImmutableList augmentedNames = augmentedNamesBuilder.build(); + ImmutableList.Builder namespacedAttributeBuilder = ImmutableList.builder(); + if (!augmentedNames.isEmpty()) { + namespacedAttributeBuilder.add( + attrFactory.newAbsoluteAttribute(augmentedNames.toArray(new String[0]))); + } + + namespacedAttributeBuilder.addAll(attributesBuilder.build()); + return new MaybeAttribute(attrFactory, namespacedAttributeBuilder.build()); + } + + MaybeAttribute(AttributeFactory attrFactory, ImmutableList attributes) { + this.attrFactory = attrFactory; + this.attributes = attributes; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/MissingAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/MissingAttribute.java new file mode 100644 index 000000000..b7fb8ad72 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/MissingAttribute.java @@ -0,0 +1,65 @@ +// 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.collect.ImmutableSet; +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. */ +final class MissingAttribute implements Attribute { + + private final ImmutableSet missingAttributes; + private final Kind kind; + + @Override + public Object resolve(long exprId, GlobalResolver ctx, ExecutionFrame frame) { + switch (kind) { + case ATTRIBUTE_NOT_FOUND: + throw CelAttributeNotFoundException.forMissingAttributes(missingAttributes); + case FIELD_NOT_FOUND: + throw CelAttributeNotFoundException.forFieldResolution(missingAttributes); + } + + throw new IllegalArgumentException("Unexpected kind: " + kind); + } + + @Override + public Attribute addQualifier(Qualifier qualifier) { + throw new UnsupportedOperationException("Unsupported operation"); + } + + static MissingAttribute newMissingAttribute(ImmutableSet attributeNames) { + return new MissingAttribute(attributeNames, Kind.ATTRIBUTE_NOT_FOUND); + } + + static MissingAttribute newMissingField(String... attributeNames) { + return newMissingField(ImmutableSet.copyOf(attributeNames)); + } + + static MissingAttribute newMissingField(ImmutableSet attributeNames) { + return new MissingAttribute(attributeNames, Kind.FIELD_NOT_FOUND); + } + + private MissingAttribute(ImmutableSet missingAttributes, Kind kind) { + this.missingAttributes = missingAttributes; + this.kind = kind; + } + + private enum Kind { + ATTRIBUTE_NOT_FOUND, + FIELD_NOT_FOUND + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java new file mode 100644 index 000000000..95a4489fd --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java @@ -0,0 +1,245 @@ +// 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.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; +import dev.cel.common.types.CelTypeProvider; +import dev.cel.common.types.EnumType; +import dev.cel.common.types.SimpleType; +import dev.cel.common.types.TypeType; +import dev.cel.common.values.CelValueConverter; +import dev.cel.runtime.AccumulatedUnknowns; +import dev.cel.runtime.CelAttribute; +import dev.cel.runtime.CelAttributePattern; +import dev.cel.runtime.GlobalResolver; +import dev.cel.runtime.InterpreterUtil; +import dev.cel.runtime.PartialVars; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Optional; +import org.jspecify.annotations.Nullable; + +@Immutable +final class NamespacedAttribute implements Attribute { + private final boolean disambiguateNames; + private final ImmutableMap candidateAttributes; + private final ImmutableList qualifiers; + private final CelValueConverter celValueConverter; + private final CelTypeProvider typeProvider; + + ImmutableList qualifiers() { + return qualifiers; + } + + ImmutableSet candidateVariableNames() { + return candidateAttributes.keySet(); + } + + @Override + 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 + // variables. + if (disambiguateNames) { + inputVars = unwrapToNonLocal(ctx); + } + + for (Map.Entry entry : candidateAttributes.entrySet()) { + String name = entry.getKey(); + CelAttribute attr = entry.getValue(); + + GlobalResolver resolver = ctx; + if (disambiguateNames) { + resolver = inputVars; + } + + Object value = resolver.resolve(name); + value = InterpreterUtil.maybeAdaptToAccumulatedUnknowns(value); + + PartialVars partialVars = frame.partialVars().orElse(null); + + if (partialVars != null && !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) { + return value; + } + } + + return MissingAttribute.newMissingAttribute(candidateAttributes.keySet()); + } + + private @Nullable Object findIdent(String name) { + CelType type = typeProvider.findType(name).orElse(null); + // If the name resolves directly, this is a fully qualified type name + // (ex: 'int' or 'google.protobuf.Timestamp') + if (type != null) { + if (qualifiers.isEmpty()) { + // Resolution of a fully qualified type name: foo.bar.baz + if (type instanceof TypeType) { + // Coalesce all type(foo) "type" into a sentinel runtime type to allow for + // erasure based type comparisons + return TypeType.create(SimpleType.DYN); + } + + return TypeType.create(type); + } + + throw new IllegalStateException( + "Unexpected type resolution when there were remaining qualifiers: " + type.name()); + } + + // The name itself could be a fully qualified reference to an enum value + // (e.g: my.enum_type.BAR) + int lastDotIndex = name.lastIndexOf('.'); + if (lastDotIndex > 0) { + String enumTypeName = name.substring(0, lastDotIndex); + String enumValueQualifier = name.substring(lastDotIndex + 1); + + return typeProvider + .findType(enumTypeName) + .filter(EnumType.class::isInstance) + .map(EnumType.class::cast) + .map(enumType -> getEnumValue(enumType, enumValueQualifier)) + .orElse(null); + } + + return null; + } + + private static Long getEnumValue(EnumType enumType, String field) { + return enumType + .findNumberByName(field) + .map(Integer::longValue) + .orElseThrow( + () -> + new NoSuchElementException( + 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(); + } + return resolver; + } + + @Override + public NamespacedAttribute addQualifier(Qualifier qualifier) { + return new NamespacedAttribute( + typeProvider, + celValueConverter, + candidateAttributes, + disambiguateNames, + ImmutableList.builderWithExpectedSize(qualifiers.size() + 1) + .addAll(qualifiers) + .add(qualifier) + .build()); + } + + private static Object applyQualifiers( + Object value, CelValueConverter celValueConverter, ImmutableList qualifiers) { + Object obj = celValueConverter.toRuntimeValue(value); + + // 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); + } + + return celValueConverter.maybeUnwrap(obj); + } + + private static Optional findPartialMatchingPattern( + CelAttribute attr, ImmutableList patterns) { + for (CelAttributePattern pattern : patterns) { + if (pattern.isPartialMatch(attr)) { + return Optional.of(pattern); + } + } + return Optional.empty(); + } + + static NamespacedAttribute create( + CelTypeProvider typeProvider, + CelValueConverter celValueConverter, + ImmutableSet namespacedNames) { + ImmutableMap.Builder attributesBuilder = ImmutableMap.builder(); + boolean disambiguateNames = false; + + for (String name : namespacedNames) { + String baseName = name; + if (name.startsWith(".")) { + disambiguateNames = true; + baseName = name.substring(1); + } + attributesBuilder.put(baseName, CelAttribute.fromQualifiedIdentifier(baseName)); + } + + return new NamespacedAttribute( + typeProvider, + celValueConverter, + attributesBuilder.buildOrThrow(), + disambiguateNames, + ImmutableList.of()); + } + + private NamespacedAttribute( + CelTypeProvider typeProvider, + CelValueConverter celValueConverter, + ImmutableMap candidateAttributes, + boolean disambiguateNames, + ImmutableList qualifiers) { + this.typeProvider = typeProvider; + this.celValueConverter = celValueConverter; + 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 new file mode 100644 index 000000000..6bdeaf1df --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/PlannedInterpretable.java @@ -0,0 +1,48 @@ +// 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.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 CelExpr expr; + + /** Runs interpretation with the given activation which supplies name/value bindings. */ + 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; + + CelExpr expr() { + return expr; + } + + 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 new file mode 100644 index 000000000..1470e4909 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/PlannedProgram.java @@ -0,0 +1,194 @@ +// 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.auto.value.AutoValue; +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.CelOptions; +import dev.cel.common.annotations.Internal; +import dev.cel.common.exceptions.CelRuntimeException; +import dev.cel.common.values.ErrorValue; +import dev.cel.runtime.Activation; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.CelEvaluationExceptionBuilder; +import dev.cel.runtime.CelEvaluationListener; +import dev.cel.runtime.CelFunctionResolver; +import dev.cel.runtime.CelResolvedOverload; +import dev.cel.runtime.CelVariableResolver; +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 +public abstract class PlannedProgram implements Program { + + 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 abstract PlannedInterpretable interpretable(); + + abstract ErrorMetadata metadata(); + + public abstract CelOptions options(); + + @Override + public Object eval() throws CelEvaluationException { + 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, + /* partialVars= */ null, + /* listener= */ null); + } + + @Override + public Object eval(Map mapValue, CelFunctionResolver lateBoundFunctionResolver) + throws CelEvaluationException { + 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, + /* partialVars= */ null, + /* listener= */ null); + } + + @Override + public Object eval(CelVariableResolver resolver, CelFunctionResolver lateBoundFunctionResolver) + throws CelEvaluationException { + return evalOrThrow( + 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); + } + + public Object evalOrThrow( + PlannedInterpretable interpretable, + GlobalResolver resolver, + CelFunctionResolver functionResolver, + @Nullable PartialVars partialVars, + @Nullable CelEvaluationListener listener) + throws CelEvaluationException { + try { + 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 InterpreterUtil.maybeAdaptToCelUnknownSet(evalResult); + } catch (RuntimeException 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) { + // Use the localized expr ID (most specific error location) + LocalizedEvaluationException localized = (LocalizedEvaluationException) e; + exprId = localized.exprId(); + Throwable cause = localized.getCause(); + if (cause instanceof CelRuntimeException) { + builder = + CelEvaluationExceptionBuilder.newBuilder((CelRuntimeException) localized.getCause()); + } else { + builder = CelEvaluationExceptionBuilder.newBuilder(cause.getMessage()).setCause(cause); + } + } else if (e instanceof CelRuntimeException) { + builder = CelEvaluationExceptionBuilder.newBuilder((CelRuntimeException) e); + } else { + // Unhandled function dispatch failures wraps the original exception with a descriptive + // message + // (e.g: "Function foo failed with...") + // We need to unwrap the cause here to preserve the original exception message and its cause. + Throwable cause = e.getCause() != null ? e.getCause() : e; + builder = CelEvaluationExceptionBuilder.newBuilder(e.getMessage()).setCause(cause); + } + + return builder.setMetadata(metadata(), exprId).build(); + } + + static Program create( + PlannedInterpretable interpretable, ErrorMetadata metadata, CelOptions options) { + return new AutoValue_PlannedProgram(interpretable, metadata, options); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/PresenceTestQualifier.java b/runtime/src/main/java/dev/cel/runtime/planner/PresenceTestQualifier.java new file mode 100644 index 000000000..5c2cba1ed --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/PresenceTestQualifier.java @@ -0,0 +1,53 @@ +// 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.MissingAttribute.newMissingField; + +import dev.cel.common.values.SelectableValue; +import java.util.Map; + +/** A qualifier for presence testing a field or a map key. */ +final class PresenceTestQualifier implements Qualifier { + + @SuppressWarnings("Immutable") + private final Object value; + + @Override + public Object value() { + return value; + } + + @Override + @SuppressWarnings("unchecked") // SelectableValue cast is safe + public Object qualify(Object obj) { + if (obj instanceof SelectableValue) { + return ((SelectableValue) obj).find(value).isPresent(); + } else if (obj instanceof Map) { + Map map = (Map) obj; + return map.containsKey(value); + } + + return newMissingField(value.toString()); + } + + static PresenceTestQualifier create(Object value) { + return new PresenceTestQualifier(value); + } + + private PresenceTestQualifier(Object value) { + this.value = value; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java new file mode 100644 index 000000000..e38d08f8f --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java @@ -0,0 +1,731 @@ +// 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.auto.value.AutoValue; +import com.google.common.base.Strings; +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.CelAbstractSyntaxTree; +import dev.cel.common.CelContainer; +import dev.cel.common.CelOptions; +import dev.cel.common.Operator; +import dev.cel.common.annotations.Internal; +import dev.cel.common.ast.CelConstant; +import dev.cel.common.ast.CelExpr; +import dev.cel.common.ast.CelExpr.CelCall; +import dev.cel.common.ast.CelExpr.CelComprehension; +import dev.cel.common.ast.CelExpr.CelList; +import dev.cel.common.ast.CelExpr.CelMap; +import dev.cel.common.ast.CelExpr.CelSelect; +import dev.cel.common.ast.CelExpr.CelStruct; +import dev.cel.common.ast.CelExpr.CelStruct.Entry; +import dev.cel.common.ast.CelReference; +import dev.cel.common.exceptions.CelOverloadNotFoundException; +import dev.cel.common.types.CelKind; +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.common.values.CelValueConverter; +import dev.cel.common.values.CelValueProvider; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.CelEvaluationExceptionBuilder; +import dev.cel.runtime.CelResolvedOverload; +import dev.cel.runtime.DefaultDispatcher; +import dev.cel.runtime.Program; +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 + * parsed-only or a type-checked expression. + */ +@Immutable +@Internal +public final class ProgramPlanner { + private final CelTypeProvider typeProvider; + private final CelValueProvider valueProvider; + private final DefaultDispatcher dispatcher; + private final AttributeFactory attributeFactory; + private final CelContainer container; + private final CelOptions options; + private final CelValueConverter celValueConverter; + private final ImmutableSet lateBoundFunctionNames; + + /** + * Plans a {@link Program} from the provided parsed-only or type-checked {@link + * CelAbstractSyntaxTree}. + */ + public Program plan(CelAbstractSyntaxTree ast) throws CelEvaluationException { + PlannedInterpretable plannedInterpretable; + ErrorMetadata errorMetadata = + ErrorMetadata.create(ast.getSource().getPositionsMap(), ast.getSource().getDescription()); + try { + plannedInterpretable = plan(ast.getExpr(), PlannerContext.create(ast)); + } catch (RuntimeException e) { + throw CelEvaluationExceptionBuilder.newBuilder(e.getMessage()) + .setMetadata(errorMetadata, ast.getExpr().id()) + .setCause(e) + .build(); + } + + return PlannedProgram.create(plannedInterpretable, errorMetadata, options); + } + + private PlannedInterpretable plan(CelExpr celExpr, PlannerContext ctx) { + switch (celExpr.getKind()) { + case CONSTANT: + return planConstant(celExpr, celExpr.constant()); + case IDENT: + return planIdent(celExpr, ctx); + case SELECT: + return planSelect(celExpr, ctx); + case CALL: + return planCall(celExpr, ctx); + case LIST: + return planCreateList(celExpr, ctx); + case STRUCT: + return planCreateStruct(celExpr, ctx); + case MAP: + return planCreateMap(celExpr, ctx); + case COMPREHENSION: + return planComprehension(celExpr, ctx); + case NOT_SET: + throw new UnsupportedOperationException("Unsupported kind: " + celExpr.getKind()); + default: + throw new UnsupportedOperationException("Unexpected kind: " + celExpr.getKind()); + } + } + + private PlannedInterpretable planSelect(CelExpr celExpr, PlannerContext ctx) { + CelSelect select = celExpr.select(); + PlannedInterpretable operand = plan(select.operand(), ctx); + + InterpretableAttribute attribute; + if (operand instanceof EvalAttribute) { + attribute = (EvalAttribute) operand; + } else { + attribute = EvalAttribute.create(celExpr, attributeFactory.newRelativeAttribute(operand)); + } + + if (select.testOnly()) { + attribute = EvalTestOnly.create(celExpr, attribute); + } + + Qualifier qualifier = StringQualifier.create(select.field()); + + return attribute.addQualifier(celExpr, qualifier); + } + + private PlannedInterpretable planConstant(CelExpr expr, CelConstant celConstant) { + switch (celConstant.getKind()) { + case NULL_VALUE: + return EvalConstant.create(expr, celConstant.nullValue()); + case BOOLEAN_VALUE: + return EvalConstant.create(expr, celConstant.booleanValue()); + case INT64_VALUE: + return EvalConstant.create(expr, celConstant.int64Value()); + case UINT64_VALUE: + return EvalConstant.create(expr, celConstant.uint64Value()); + case DOUBLE_VALUE: + return EvalConstant.create(expr, celConstant.doubleValue()); + case STRING_VALUE: + return EvalConstant.create(expr, celConstant.stringValue()); + case BYTES_VALUE: + return EvalConstant.create(expr, celConstant.bytesValue()); + default: + throw new IllegalStateException("Unsupported kind: " + celConstant.getKind()); + } + } + + private PlannedInterpretable planIdent(CelExpr celExpr, PlannerContext ctx) { + CelReference ref = ctx.referenceMap().get(celExpr.id()); + if (ref != null) { + 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, attributeFactory.newAbsoluteAttribute(identName)); + } + + return EvalAttribute.create(celExpr, attributeFactory.newMaybeAttribute(identName)); + } + + private PlannedInterpretable planCheckedIdent( + CelExpr expr, CelReference identRef, ImmutableMap typeMap) { + if (identRef.value().isPresent()) { + return planConstant(expr, identRef.value().get()); + } + + CelType type = typeMap.get(expr.id()); + if (type.kind().equals(CelKind.TYPE)) { + TypeType identType = + typeProvider + .findType(identRef.name()) + .map( + t -> + (t instanceof TypeType) + // Coalesce all type(foo) "type" into a sentinel runtime type to allow for + // erasure based type comparisons + ? TypeType.create(SimpleType.DYN) + : TypeType.create(t)) + .orElseThrow( + () -> + new NoSuchElementException( + "Reference to an undefined type: " + identRef.name())); + return EvalConstant.create(expr, identType); + } + + 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(); + + PlannedInterpretable blockCall = maybeInterceptBlockCall(functionName, expr, ctx).orElse(null); + if (blockCall != null) { + return blockCall; + } + + CelExpr target = resolvedFunction.target().orElse(null); + int argCount = expr.call().args().size(); + if (target != null) { + argCount++; + } + + PlannedInterpretable[] evaluatedArgs = new PlannedInterpretable[argCount]; + + int offset = 0; + if (target != null) { + evaluatedArgs[0] = plan(target, ctx); + offset++; + } + + ImmutableList args = expr.call().args(); + for (int argIndex = 0; argIndex < args.size(); argIndex++) { + evaluatedArgs[argIndex + offset] = plan(args.get(argIndex), ctx); + } + + Operator operator = Operator.findReverse(functionName).orElse(null); + if (operator != null) { + switch (operator) { + case LOGICAL_OR: + return options.enableShortCircuiting() + ? EvalOr.create(expr, evaluatedArgs) + : EvalExhaustiveOr.create(expr, evaluatedArgs); + case LOGICAL_AND: + return options.enableShortCircuiting() + ? EvalAnd.create(expr, evaluatedArgs) + : EvalExhaustiveAnd.create(expr, evaluatedArgs); + case CONDITIONAL: + return options.enableShortCircuiting() + ? EvalConditional.create(expr, evaluatedArgs) + : EvalExhaustiveConditional.create(expr, evaluatedArgs); + default: + // fall-through + } + } + + CelResolvedOverload resolvedOverload = null; + if (resolvedFunction.overloadId().isPresent()) { + resolvedOverload = dispatcher.findOverload(resolvedFunction.overloadId().get()).orElse(null); + } + + if (resolvedOverload == null) { + // Parsed-only function dispatch + resolvedOverload = dispatcher.findOverload(functionName).orElse(null); + } + + PlannedInterpretable optionalCall = + maybeInterceptOptionalCalls(resolvedOverload, functionName, evaluatedArgs, expr) + .orElse(null); + if (optionalCall != null) { + return optionalCall; + } + + if (resolvedOverload == null) { + if (!lateBoundFunctionNames.contains(functionName)) { + CelReference reference = ctx.referenceMap().get(expr.id()); + if (reference != null) { + throw new CelOverloadNotFoundException(functionName, reference.overloadIds()); + } else { + throw new CelOverloadNotFoundException(functionName); + } + } + + ImmutableList overloadIds = ImmutableList.of(); + if (resolvedFunction.overloadId().isPresent()) { + overloadIds = ImmutableList.of(resolvedFunction.overloadId().get()); + } + + return EvalLateBoundCall.create( + expr, functionName, overloadIds, evaluatedArgs, celValueConverter); + } + + switch (argCount) { + case 0: + return EvalZeroArity.create(expr, functionName, resolvedOverload, celValueConverter); + case 1: + return EvalUnary.create( + expr, functionName, resolvedOverload, evaluatedArgs[0], celValueConverter); + case 2: + return EvalBinary.create( + expr, + functionName, + resolvedOverload, + evaluatedArgs[0], + evaluatedArgs[1], + celValueConverter); + default: + return EvalVarArgsCall.create( + expr, functionName, resolvedOverload, evaluatedArgs, celValueConverter); + } + } + + private Optional maybeInterceptBlockCall( + String functionName, CelExpr expr, PlannerContext ctx) { + if (!functionName.equals("cel.@block")) { + return Optional.empty(); + } + + CelCall blockCall = expr.call(); + + if (blockCall.args().size() != 2) { + throw new IllegalArgumentException( + "Expected 2 arguments for cel.@block call. Got: " + blockCall.args().size()); + } + + CelList exprList = blockCall.args().get(0).list(); + PlannedInterpretable[] slotExprs = new PlannedInterpretable[exprList.elements().size()]; + for (int i = 0; i < slotExprs.length; i++) { + slotExprs[i] = plan(exprList.elements().get(i), ctx); + } + PlannedInterpretable resultExpr = plan(blockCall.args().get(1), ctx); + return Optional.of(EvalBlock.create(expr, 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 (Operator.OPTIONAL_SELECT.getFunction().equals(functionName)) { + String field = expr.call().args().get(1).constant().stringValue(); + InterpretableAttribute attribute; + if (evaluatedArgs[0] instanceof EvalAttribute) { + attribute = (EvalAttribute) evaluatedArgs[0]; + } else { + attribute = + EvalAttribute.create(expr, 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(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, 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()]; + + for (int i = 0; i < elements.size(); i++) { + values[i] = plan(elements.get(i), ctx); + } + + 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) { + CelMap map = celExpr.map(); + + 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, keys, values, isOptional); + } + + private PlannedInterpretable planComprehension(CelExpr expr, PlannerContext ctx) { + CelComprehension comprehension = expr.comprehension(); + + PlannedInterpretable accuInit = plan(comprehension.accuInit(), ctx); + PlannedInterpretable iterRange = plan(comprehension.iterRange(), ctx); + + ctx.pushLocalVars(comprehension.accuVar(), comprehension.iterVar(), comprehension.iterVar2()); + + PlannedInterpretable loopCondition = plan(comprehension.loopCondition(), ctx); + PlannedInterpretable loopStep = plan(comprehension.loopStep(), ctx); + + ctx.popLocalVars(comprehension.iterVar(), comprehension.iterVar2()); + + PlannedInterpretable result = plan(comprehension.result(), ctx); + + ctx.popLocalVars(comprehension.accuVar()); + + return EvalFold.create( + expr, + comprehension.accuVar(), + accuInit, + comprehension.iterVar(), + comprehension.iterVar2(), + iterRange, + loopCondition, + loopStep, + result); + } + + /** + * resolveFunction determines the call target, function name, and overload name (when unambiguous) + * from the given call expr. + */ + private ResolvedFunction resolveFunction( + CelExpr expr, ImmutableMap referenceMap) { + CelCall call = expr.call(); + Optional maybeTarget = call.target(); + String functionName = call.function(); + + CelReference reference = referenceMap.get(expr.id()); + if (reference != null) { + // Checked expression + if (reference.overloadIds().size() == 1) { + ResolvedFunction.Builder builder = + ResolvedFunction.newBuilder() + .setFunctionName(functionName) + .setOverloadId(reference.overloadIds().get(0)); + + maybeTarget.ifPresent(builder::setTarget); + + return builder.build(); + } + } + + // Parsed-only function resolution. + // + // There are two distinct cases we must handle: + // + // 1. Non-qualified function calls. This will resolve into either: + // - A simple global call foo() + // - A fully qualified global call through normal container resolution foo.bar.qux() + // 2. Qualified function calls: + // - A member call on an identifier foo.bar() + // - A fully qualified global call, through normal container resolution or abbreviations + // foo.bar.qux() + if (!maybeTarget.isPresent()) { + for (String cand : container.resolveCandidateNames(functionName)) { + CelResolvedOverload overload = dispatcher.findOverload(cand).orElse(null); + if (overload != null) { + return ResolvedFunction.newBuilder().setFunctionName(cand).build(); + } + } + + // Normal global call + return ResolvedFunction.newBuilder().setFunctionName(functionName).build(); + } + + CelExpr target = maybeTarget.get(); + String qualifiedPrefix = toQualifiedName(target).orElse(null); + if (qualifiedPrefix != null) { + String qualifiedName = qualifiedPrefix + "." + functionName; + for (String cand : container.resolveCandidateNames(qualifiedName)) { + CelResolvedOverload overload = dispatcher.findOverload(cand).orElse(null); + if (overload != null) { + return ResolvedFunction.newBuilder().setFunctionName(cand).build(); + } + } + } + + // Normal member call + return ResolvedFunction.newBuilder().setFunctionName(functionName).setTarget(target).build(); + } + + 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); + if (structType == null) { + continue; + } + + CelKind kind = structType.kind(); + + if (!isValidStructKind(kind)) { + throw new IllegalArgumentException( + String.format( + "Expected struct type for %s, got %s", structType.name(), structType.kind())); + } + + return structType; + } + + 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()) { + case IDENT: + return Optional.of(operand.ident().name()); + case SELECT: + CelSelect select = operand.select(); + String maybeQualified = toQualifiedName(select.operand()).orElse(null); + if (maybeQualified != null) { + return Optional.of(maybeQualified + "." + select.field()); + } + + break; + default: + // fall-through + } + + return Optional.empty(); + } + + @AutoValue + abstract static class ResolvedFunction { + + abstract String functionName(); + + abstract Optional target(); + + abstract Optional overloadId(); + + @AutoValue.Builder + abstract static class Builder { + abstract Builder setFunctionName(String functionName); + + abstract Builder setTarget(CelExpr target); + + abstract Builder setOverloadId(String overloadId); + + @CheckReturnValue + abstract ResolvedFunction build(); + } + + private static Builder newBuilder() { + return new AutoValue_ProgramPlanner_ResolvedFunction.Builder(); + } + } + + static final class PlannerContext { + private final ImmutableMap referenceMap; + private final ImmutableMap typeMap; + private final HashMap localVars = new HashMap<>(); + + ImmutableMap referenceMap() { + return referenceMap; + } + + ImmutableMap typeMap() { + return typeMap; + } + + private void pushLocalVars(String... names) { + for (String name : names) { + if (Strings.isNullOrEmpty(name)) { + continue; + } + localVars.merge(name, 1, Integer::sum); + } + } + + private void popLocalVars(String... names) { + for (String name : names) { + if (Strings.isNullOrEmpty(name)) { + continue; + } + Integer count = localVars.get(name); + if (count != null) { + if (count == 1) { + localVars.remove(name); + } else { + localVars.put(name, count - 1); + } + } + } + } + + /** Checks if the given name is a local variable in the current scope. */ + private boolean isLocalVar(String name) { + return localVars.containsKey(name); + } + + private PlannerContext( + ImmutableMap referenceMap, ImmutableMap typeMap) { + this.referenceMap = referenceMap; + this.typeMap = typeMap; + } + + static PlannerContext create(CelAbstractSyntaxTree ast) { + return new PlannerContext(ast.getReferenceMap(), ast.getTypeMap()); + } + } + + public static ProgramPlanner newPlanner( + CelTypeProvider typeProvider, + CelValueProvider valueProvider, + DefaultDispatcher dispatcher, + CelValueConverter celValueConverter, + CelContainer container, + CelOptions options, + ImmutableSet lateBoundFunctionNames) { + return new ProgramPlanner( + typeProvider, + valueProvider, + dispatcher, + celValueConverter, + container, + options, + lateBoundFunctionNames); + } + + private ProgramPlanner( + CelTypeProvider typeProvider, + CelValueProvider valueProvider, + DefaultDispatcher dispatcher, + CelValueConverter celValueConverter, + CelContainer container, + CelOptions options, + ImmutableSet lateBoundFunctionNames) { + this.typeProvider = typeProvider; + this.valueProvider = valueProvider; + this.dispatcher = dispatcher; + this.celValueConverter = celValueConverter; + this.container = container; + this.options = options; + this.lateBoundFunctionNames = lateBoundFunctionNames; + this.attributeFactory = + AttributeFactory.newAttributeFactory(container, typeProvider, celValueConverter); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/Qualifier.java b/runtime/src/main/java/dev/cel/runtime/planner/Qualifier.java new file mode 100644 index 000000000..82e48e95a --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/Qualifier.java @@ -0,0 +1,28 @@ +// 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.errorprone.annotations.Immutable; + +/** + * Represents a qualification step (such as a field selection or map key lookup) applied to an + * intermediate value during attribute resolution. + */ +@Immutable +interface Qualifier { + Object value(); + + Object qualify(Object value); +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java new file mode 100644 index 000000000..38f733c79 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java @@ -0,0 +1,76 @@ +// 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.collect.ImmutableList; +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.values.CelValueConverter; +import dev.cel.runtime.AccumulatedUnknowns; +import dev.cel.runtime.GlobalResolver; + +/** + * An attribute resolved relative to a base expression (operand) by applying a sequence of + * qualifiers. + */ +@Immutable +final class RelativeAttribute implements Attribute { + + private final PlannedInterpretable operand; + private final CelValueConverter celValueConverter; + private final ImmutableList qualifiers; + + @Override + 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); + + // 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); + } + + return celValueConverter.maybeUnwrap(obj); + } + + @Override + public Attribute addQualifier(Qualifier qualifier) { + return new RelativeAttribute( + this.operand, + celValueConverter, + ImmutableList.builderWithExpectedSize(qualifiers.size() + 1) + .addAll(this.qualifiers) + .add(qualifier) + .build()); + } + + RelativeAttribute(PlannedInterpretable operand, CelValueConverter celValueConverter) { + this(operand, celValueConverter, ImmutableList.of()); + } + + private RelativeAttribute( + PlannedInterpretable operand, + CelValueConverter celValueConverter, + ImmutableList qualifiers) { + this.operand = operand; + this.celValueConverter = celValueConverter; + this.qualifiers = qualifiers; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/StringQualifier.java b/runtime/src/main/java/dev/cel/runtime/planner/StringQualifier.java new file mode 100644 index 000000000..293ca5c7d --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/StringQualifier.java @@ -0,0 +1,75 @@ +// 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 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. */ +final class StringQualifier implements Qualifier { + + private final String value; + + @Override + public String value() { + return 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); + } + + if (obj instanceof Map) { + Map map = (Map) obj; + Object mapVal = map.get(value); + + if (mapVal != null) { + return mapVal; + } + + if (!map.containsKey(value)) { + throw CelAttributeNotFoundException.forMissingMapKey(value); + } + + throw CelAttributeNotFoundException.of( + String.format("Map value cannot be null for key: %s", value)); + } + + throw CelAttributeNotFoundException.forFieldResolution(value); + } + + static StringQualifier create(String value) { + return new StringQualifier(value); + } + + private StringQualifier(String value) { + this.value = value; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/standard/AddOperator.java b/runtime/src/main/java/dev/cel/runtime/standard/AddOperator.java new file mode 100644 index 000000000..c4e0bec42 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/AddOperator.java @@ -0,0 +1,175 @@ +// 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.standard; + +import static dev.cel.common.Operator.ADD; + +import com.google.common.collect.ImmutableSet; +import com.google.common.primitives.UnsignedLong; +import com.google.protobuf.ByteString; +import com.google.protobuf.Duration; +import com.google.protobuf.Timestamp; +import dev.cel.common.CelOptions; +import dev.cel.common.exceptions.CelNumericOverflowException; +import dev.cel.common.internal.DateTimeHelpers; +import dev.cel.common.internal.ProtoTimeUtils; +import dev.cel.common.values.CelByteString; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.RuntimeEquality; +import dev.cel.runtime.RuntimeHelpers; +import java.time.Instant; +import java.util.Arrays; +import java.util.List; + +/** Standard function for the addition (+) operator. */ +public final class AddOperator extends CelStandardFunction { + private static final AddOperator ALL_OVERLOADS = create(AddOverload.values()); + + public static AddOperator create() { + return ALL_OVERLOADS; + } + + public static AddOperator create(AddOverload... overloads) { + return create(Arrays.asList(overloads)); + } + + public static AddOperator create(Iterable overloads) { + return new AddOperator(ImmutableSet.copyOf(overloads)); + } + + /** Overloads for the standard function. */ + public enum AddOverload implements CelStandardOverload { + ADD_INT64( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "add_int64", + Long.class, + Long.class, + (Long x, Long y) -> { + try { + return RuntimeHelpers.int64Add(x, y, celOptions); + } catch (ArithmeticException e) { + throw new CelNumericOverflowException(e); + } + })), + ADD_UINT64( + (celOptions, runtimeEquality) -> { + if (celOptions.enableUnsignedLongs()) { + return CelFunctionBinding.from( + "add_uint64", + UnsignedLong.class, + UnsignedLong.class, + (UnsignedLong x, UnsignedLong y) -> { + try { + return RuntimeHelpers.uint64Add(x, y); + } catch (ArithmeticException e) { + throw new CelNumericOverflowException(e); + } + }); + } else { + return CelFunctionBinding.from( + "add_uint64", + Long.class, + Long.class, + (Long x, Long y) -> { + try { + return RuntimeHelpers.uint64Add(x, y, celOptions); + } catch (ArithmeticException e) { + throw new CelNumericOverflowException(e); + } + }); + } + }), + ADD_BYTES( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "add_bytes", CelByteString.class, CelByteString.class, CelByteString::concat); + } else { + return CelFunctionBinding.from( + "add_bytes", ByteString.class, ByteString.class, ByteString::concat); + } + }), + ADD_DOUBLE( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from("add_double", Double.class, Double.class, Double::sum)), + ADD_DURATION_DURATION( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "add_duration_duration", + java.time.Duration.class, + java.time.Duration.class, + DateTimeHelpers::add); + } else { + return CelFunctionBinding.from( + "add_duration_duration", Duration.class, Duration.class, ProtoTimeUtils::add); + } + }), + ADD_TIMESTAMP_DURATION( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "add_timestamp_duration", + Instant.class, + java.time.Duration.class, + DateTimeHelpers::add); + } else { + return CelFunctionBinding.from( + "add_timestamp_duration", Timestamp.class, Duration.class, ProtoTimeUtils::add); + } + }), + ADD_STRING( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "add_string", String.class, String.class, (String x, String y) -> x + y)), + ADD_DURATION_TIMESTAMP( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "add_duration_timestamp", + java.time.Duration.class, + Instant.class, + (java.time.Duration d, Instant i) -> DateTimeHelpers.add(i, d)); + } else { + return CelFunctionBinding.from( + "add_duration_timestamp", + Duration.class, + Timestamp.class, + (Duration d, Timestamp t) -> ProtoTimeUtils.add(t, d)); + } + }), + @SuppressWarnings({"unchecked"}) + ADD_LIST( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from("add_list", List.class, List.class, RuntimeHelpers::concat)); + + private final CelStandardOverload standardOverload; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return standardOverload.newFunctionBinding(celOptions, runtimeEquality); + } + + AddOverload(CelStandardOverload standardOverload) { + this.standardOverload = standardOverload; + } + } + + private AddOperator(ImmutableSet overloads) { + super(ADD.getFunction(), overloads); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/standard/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/standard/BUILD.bazel new file mode 100644 index 000000000..0a76b6135 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/BUILD.bazel @@ -0,0 +1,1520 @@ +load("@rules_java//java:defs.bzl", "java_library") +load("//:cel_android_rules.bzl", "cel_android_library") + +package( + default_applicable_licenses = ["//:license"], + default_visibility = [ + "//publish:__pkg__", + "//runtime/standard:__pkg__", + ], +) + +java_library( + name = "standard_function", + srcs = ["CelStandardFunction.java"], + tags = [ + ], + deps = [ + ":standard_overload", + "//common:options", + "//runtime:function_binding", + "//runtime:runtime_equality", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "standard_function_android", + srcs = ["CelStandardFunction.java"], + tags = [ + ], + deps = [ + ":standard_overload_android", + "//common:options", + "//runtime:function_binding_android", + "//runtime:runtime_equality_android", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", + ], +) + +java_library( + name = "add", + srcs = ["AddOperator.java"], + tags = [ + ], + deps = [ + ":standard_function", + ":standard_overload", + "//common:operator", + "//common:options", + "//common/exceptions:numeric_overflow", + "//common/internal:date_time_helpers", + "//common/internal:proto_time_utils", + "//common/values:cel_byte_string", + "//runtime:function_binding", + "//runtime:runtime_equality", + "//runtime:runtime_helpers", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + ], +) + +cel_android_library( + name = "add_android", + srcs = ["AddOperator.java"], + tags = [ + ], + deps = [ + ":standard_function_android", + ":standard_overload_android", + "//common:operator_android", + "//common:options", + "//common/exceptions:numeric_overflow", + "//common/internal:date_time_helpers_android", + "//common/internal:proto_time_utils_android", + "//common/values:cel_byte_string", + "//runtime:function_binding_android", + "//runtime:runtime_equality_android", + "//runtime:runtime_helpers_android", + "@maven_android//:com_google_guava_guava", + "@maven_android//:com_google_protobuf_protobuf_javalite", + ], +) + +java_library( + name = "subtract", + srcs = ["SubtractOperator.java"], + tags = [ + ], + deps = [ + ":standard_function", + ":standard_overload", + "//common:operator", + "//common:options", + "//common/exceptions:numeric_overflow", + "//common/internal:date_time_helpers", + "//common/internal:proto_time_utils", + "//runtime:function_binding", + "//runtime:runtime_equality", + "//runtime:runtime_helpers", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + ], +) + +cel_android_library( + name = "subtract_android", + srcs = ["SubtractOperator.java"], + tags = [ + ], + deps = [ + ":standard_function_android", + ":standard_overload_android", + "//common:operator_android", + "//common:options", + "//common/exceptions:numeric_overflow", + "//common/internal:date_time_helpers_android", + "//common/internal:proto_time_utils_android", + "//runtime:function_binding_android", + "//runtime:runtime_equality_android", + "//runtime:runtime_helpers_android", + "@maven_android//:com_google_guava_guava", + "@maven_android//:com_google_protobuf_protobuf_javalite", + ], +) + +java_library( + name = "bool", + srcs = ["BoolFunction.java"], + tags = [ + ], + deps = [ + ":standard_overload", + "//common:options", + "//common/exceptions:bad_format", + "//common/internal:safe_string_formatter", + "//runtime:function_binding", + "//runtime:runtime_equality", + "//runtime/standard:standard_function", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "bool_android", + srcs = ["BoolFunction.java"], + tags = [ + ], + deps = [ + ":standard_function_android", + ":standard_overload_android", + "//common:options", + "//common/exceptions:bad_format", + "//common/internal:safe_string_formatter", + "//runtime:function_binding_android", + "//runtime:runtime_equality_android", + "@maven_android//:com_google_guava_guava", + ], +) + +java_library( + name = "bytes", + srcs = ["BytesFunction.java"], + tags = [ + ], + deps = [ + ":standard_overload", + "//common:options", + "//common/values:cel_byte_string", + "//runtime:function_binding", + "//runtime:runtime_equality", + "//runtime/standard:standard_function", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + ], +) + +cel_android_library( + name = "bytes_android", + srcs = ["BytesFunction.java"], + tags = [ + ], + deps = [ + ":standard_function_android", + ":standard_overload_android", + "//common:options", + "//common/values:cel_byte_string", + "//runtime:function_binding_android", + "//runtime:runtime_equality_android", + "@maven_android//:com_google_guava_guava", + "@maven_android//:com_google_protobuf_protobuf_javalite", + ], +) + +java_library( + name = "contains", + srcs = ["ContainsFunction.java"], + tags = [ + ], + deps = [ + ":standard_overload", + "//common:options", + "//runtime:function_binding", + "//runtime:runtime_equality", + "//runtime/standard:standard_function", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "contains_android", + srcs = ["ContainsFunction.java"], + tags = [ + ], + deps = [ + ":standard_function_android", + ":standard_overload_android", + "//common:options", + "//runtime:function_binding_android", + "//runtime:runtime_equality_android", + "@maven_android//:com_google_guava_guava", + ], +) + +java_library( + name = "double", + srcs = ["DoubleFunction.java"], + tags = [ + ], + deps = [ + ":standard_overload", + "//common:options", + "//common/exceptions:bad_format", + "//runtime:function_binding", + "//runtime:runtime_equality", + "//runtime/standard:standard_function", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "double_android", + srcs = ["DoubleFunction.java"], + tags = [ + ], + deps = [ + ":standard_function_android", + ":standard_overload_android", + "//common:options", + "//common/exceptions:bad_format", + "//runtime:function_binding_android", + "//runtime:runtime_equality_android", + "@maven_android//:com_google_guava_guava", + ], +) + +java_library( + name = "duration", + srcs = ["DurationFunction.java"], + tags = [ + ], + deps = [ + ":standard_overload", + "//common:options", + "//common/exceptions:bad_format", + "//runtime:function_binding", + "//runtime:runtime_equality", + "//runtime:runtime_helpers", + "//runtime/standard:standard_function", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + ], +) + +cel_android_library( + name = "duration_android", + srcs = ["DurationFunction.java"], + tags = [ + ], + deps = [ + ":standard_function_android", + ":standard_overload_android", + "//common:options", + "//common/exceptions:bad_format", + "//runtime:function_binding_android", + "//runtime:runtime_equality_android", + "//runtime:runtime_helpers_android", + "@maven_android//:com_google_guava_guava", + "@maven_android//:com_google_protobuf_protobuf_javalite", + ], +) + +java_library( + name = "dyn", + srcs = ["DynFunction.java"], + tags = [ + ], + deps = [ + ":standard_overload", + "//common:options", + "//runtime:function_binding", + "//runtime:runtime_equality", + "//runtime/standard:standard_function", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "dyn_android", + srcs = ["DynFunction.java"], + tags = [ + ], + deps = [ + ":standard_function_android", + ":standard_overload_android", + "//common:options", + "//runtime:function_binding_android", + "//runtime:runtime_equality_android", + "@maven_android//:com_google_guava_guava", + ], +) + +java_library( + name = "ends_with", + srcs = ["EndsWithFunction.java"], + tags = [ + ], + deps = [ + ":standard_overload", + "//common:options", + "//runtime:function_binding", + "//runtime:runtime_equality", + "//runtime/standard:standard_function", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "ends_with_android", + srcs = ["EndsWithFunction.java"], + tags = [ + ], + deps = [ + ":standard_function_android", + ":standard_overload_android", + "//common:options", + "//runtime:function_binding_android", + "//runtime:runtime_equality_android", + "@maven_android//:com_google_guava_guava", + ], +) + +java_library( + name = "equals", + srcs = ["EqualsOperator.java"], + tags = [ + ], + deps = [ + ":standard_overload", + "//common:operator", + "//common:options", + "//runtime:function_binding", + "//runtime:runtime_equality", + "//runtime/standard:standard_function", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "equals_android", + srcs = ["EqualsOperator.java"], + tags = [ + ], + deps = [ + ":standard_function_android", + ":standard_overload_android", + "//common:operator_android", + "//common:options", + "//runtime:function_binding_android", + "//runtime:runtime_equality_android", + "@maven_android//:com_google_guava_guava", + ], +) + +java_library( + name = "get_day_of_year", + srcs = ["GetDayOfYearFunction.java"], + tags = [ + ], + deps = [ + ":standard_overload", + "//common:options", + "//common/internal:date_time_helpers", + "//runtime:function_binding", + "//runtime:runtime_equality", + "//runtime/standard:standard_function", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + ], +) + +cel_android_library( + name = "get_day_of_year_android", + srcs = ["GetDayOfYearFunction.java"], + tags = [ + ], + deps = [ + ":standard_function_android", + ":standard_overload_android", + "//common:options", + "//common/internal:date_time_helpers_android", + "//runtime:function_binding_android", + "//runtime:runtime_equality_android", + "@maven_android//:com_google_guava_guava", + "@maven_android//:com_google_protobuf_protobuf_javalite", + ], +) + +java_library( + name = "get_day_of_month", + srcs = ["GetDayOfMonthFunction.java"], + tags = [ + ], + deps = [ + ":standard_overload", + "//common:options", + "//common/internal:date_time_helpers", + "//runtime:function_binding", + "//runtime:runtime_equality", + "//runtime/standard:standard_function", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + ], +) + +cel_android_library( + name = "get_day_of_month_android", + srcs = ["GetDayOfMonthFunction.java"], + tags = [ + ], + deps = [ + ":standard_function_android", + ":standard_overload_android", + "//common:options", + "//common/internal:date_time_helpers_android", + "//runtime:function_binding_android", + "//runtime:runtime_equality_android", + "@maven_android//:com_google_guava_guava", + "@maven_android//:com_google_protobuf_protobuf_javalite", + ], +) + +java_library( + name = "get_day_of_week", + srcs = ["GetDayOfWeekFunction.java"], + tags = [ + ], + deps = [ + ":standard_overload", + "//common:options", + "//common/internal:date_time_helpers", + "//runtime:function_binding", + "//runtime:runtime_equality", + "//runtime/standard:standard_function", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + ], +) + +cel_android_library( + name = "get_day_of_week_android", + srcs = ["GetDayOfWeekFunction.java"], + tags = [ + ], + deps = [ + ":standard_function_android", + ":standard_overload_android", + "//common:options", + "//common/internal:date_time_helpers_android", + "//runtime:function_binding_android", + "//runtime:runtime_equality_android", + "@maven_android//:com_google_guava_guava", + "@maven_android//:com_google_protobuf_protobuf_javalite", + ], +) + +java_library( + name = "get_date", + srcs = ["GetDateFunction.java"], + tags = [ + ], + deps = [ + ":standard_overload", + "//common:options", + "//common/internal:date_time_helpers", + "//runtime:function_binding", + "//runtime:runtime_equality", + "//runtime/standard:standard_function", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + ], +) + +cel_android_library( + name = "get_date_android", + srcs = ["GetDateFunction.java"], + tags = [ + ], + deps = [ + ":standard_function_android", + ":standard_overload_android", + "//common:options", + "//common/internal:date_time_helpers_android", + "//runtime:function_binding_android", + "//runtime:runtime_equality_android", + "@maven_android//:com_google_guava_guava", + "@maven_android//:com_google_protobuf_protobuf_javalite", + ], +) + +java_library( + name = "get_full_year", + srcs = ["GetFullYearFunction.java"], + tags = [ + ], + deps = [ + ":standard_overload", + "//common:options", + "//common/internal:date_time_helpers", + "//runtime:function_binding", + "//runtime:runtime_equality", + "//runtime/standard:standard_function", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + ], +) + +cel_android_library( + name = "get_full_year_android", + srcs = ["GetFullYearFunction.java"], + tags = [ + ], + deps = [ + ":standard_function_android", + ":standard_overload_android", + "//common:options", + "//common/internal:date_time_helpers_android", + "//runtime:function_binding_android", + "//runtime:runtime_equality_android", + "@maven_android//:com_google_guava_guava", + "@maven_android//:com_google_protobuf_protobuf_javalite", + ], +) + +java_library( + name = "get_hours", + srcs = ["GetHoursFunction.java"], + tags = [ + ], + deps = [ + ":standard_overload", + "//common:options", + "//common/internal:date_time_helpers", + "//common/internal:proto_time_utils", + "//runtime:function_binding", + "//runtime:runtime_equality", + "//runtime/standard:standard_function", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + ], +) + +cel_android_library( + name = "get_hours_android", + srcs = ["GetHoursFunction.java"], + tags = [ + ], + deps = [ + ":standard_function_android", + ":standard_overload_android", + "//common:options", + "//common/internal:date_time_helpers_android", + "//common/internal:proto_time_utils_android", + "//runtime:function_binding_android", + "//runtime:runtime_equality_android", + "@maven_android//:com_google_guava_guava", + "@maven_android//:com_google_protobuf_protobuf_javalite", + ], +) + +java_library( + name = "get_milliseconds", + srcs = ["GetMillisecondsFunction.java"], + tags = [ + ], + deps = [ + ":standard_overload", + "//common:options", + "//common/internal:date_time_helpers", + "//common/internal:proto_time_utils", + "//runtime:function_binding", + "//runtime:runtime_equality", + "//runtime/standard:standard_function", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + ], +) + +cel_android_library( + name = "get_milliseconds_android", + srcs = ["GetMillisecondsFunction.java"], + tags = [ + ], + deps = [ + ":standard_function_android", + ":standard_overload_android", + "//common:options", + "//common/internal:date_time_helpers_android", + "//common/internal:proto_time_utils_android", + "//runtime:function_binding_android", + "//runtime:runtime_equality_android", + "@maven_android//:com_google_guava_guava", + "@maven_android//:com_google_protobuf_protobuf_javalite", + ], +) + +java_library( + name = "get_minutes", + srcs = ["GetMinutesFunction.java"], + tags = [ + ], + deps = [ + ":standard_overload", + "//common:options", + "//common/internal:date_time_helpers", + "//common/internal:proto_time_utils", + "//runtime:function_binding", + "//runtime:runtime_equality", + "//runtime/standard:standard_function", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + ], +) + +cel_android_library( + name = "get_minutes_android", + srcs = ["GetMinutesFunction.java"], + tags = [ + ], + deps = [ + ":standard_function_android", + ":standard_overload_android", + "//common:options", + "//common/internal:date_time_helpers_android", + "//common/internal:proto_time_utils_android", + "//runtime:function_binding_android", + "//runtime:runtime_equality_android", + "@maven_android//:com_google_guava_guava", + "@maven_android//:com_google_protobuf_protobuf_javalite", + ], +) + +java_library( + name = "get_month", + srcs = ["GetMonthFunction.java"], + tags = [ + ], + deps = [ + ":standard_overload", + "//common:options", + "//common/internal:date_time_helpers", + "//runtime:function_binding", + "//runtime:runtime_equality", + "//runtime/standard:standard_function", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + ], +) + +cel_android_library( + name = "get_month_android", + srcs = ["GetMonthFunction.java"], + tags = [ + ], + deps = [ + ":standard_function_android", + ":standard_overload_android", + "//common:options", + "//common/internal:date_time_helpers_android", + "//runtime:function_binding_android", + "//runtime:runtime_equality_android", + "@maven_android//:com_google_guava_guava", + "@maven_android//:com_google_protobuf_protobuf_javalite", + ], +) + +java_library( + name = "get_seconds", + srcs = ["GetSecondsFunction.java"], + tags = [ + ], + deps = [ + ":standard_overload", + "//common:options", + "//common/internal:date_time_helpers", + "//common/internal:proto_time_utils", + "//runtime:function_binding", + "//runtime:runtime_equality", + "//runtime/standard:standard_function", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + ], +) + +cel_android_library( + name = "get_seconds_android", + srcs = ["GetSecondsFunction.java"], + tags = [ + ], + deps = [ + ":standard_function_android", + ":standard_overload_android", + "//common:options", + "//common/internal:date_time_helpers_android", + "//common/internal:proto_time_utils_android", + "//runtime:function_binding_android", + "//runtime:runtime_equality_android", + "@maven_android//:com_google_guava_guava", + "@maven_android//:com_google_protobuf_protobuf_javalite", + ], +) + +java_library( + name = "greater", + srcs = ["GreaterOperator.java"], + tags = [ + ], + deps = [ + ":standard_overload", + "//common:operator", + "//common:options", + "//common/internal:comparison_functions", + "//common/internal:proto_time_utils", + "//common/values:cel_byte_string", + "//runtime:function_binding", + "//runtime:runtime_equality", + "//runtime:runtime_helpers", + "//runtime/standard:standard_function", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + ], +) + +cel_android_library( + name = "greater_android", + srcs = ["GreaterOperator.java"], + tags = [ + ], + deps = [ + ":standard_function_android", + ":standard_overload_android", + "//common:operator_android", + "//common:options", + "//common/internal:comparison_functions_android", + "//common/internal:proto_time_utils_android", + "//common/values:cel_byte_string", + "//runtime:function_binding_android", + "//runtime:runtime_equality_android", + "//runtime:runtime_helpers_android", + "@maven_android//:com_google_guava_guava", + "@maven_android//:com_google_protobuf_protobuf_javalite", + ], +) + +java_library( + name = "greater_equals", + srcs = ["GreaterEqualsOperator.java"], + tags = [ + ], + deps = [ + ":standard_overload", + "//common:operator", + "//common:options", + "//common/internal:comparison_functions", + "//common/internal:proto_time_utils", + "//common/values:cel_byte_string", + "//runtime:function_binding", + "//runtime:runtime_equality", + "//runtime:runtime_helpers", + "//runtime/standard:standard_function", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + ], +) + +cel_android_library( + name = "greater_equals_android", + srcs = ["GreaterEqualsOperator.java"], + tags = [ + ], + deps = [ + ":standard_function_android", + ":standard_overload_android", + "//common:operator_android", + "//common:options", + "//common/internal:comparison_functions_android", + "//common/internal:proto_time_utils_android", + "//common/values:cel_byte_string", + "//runtime:function_binding_android", + "//runtime:runtime_equality_android", + "//runtime:runtime_helpers_android", + "@maven_android//:com_google_guava_guava", + "@maven_android//:com_google_protobuf_protobuf_javalite", + ], +) + +java_library( + name = "in", + srcs = ["InOperator.java"], + tags = [ + ], + deps = [ + ":standard_overload", + "//common:operator", + "//common:options", + "//runtime:function_binding", + "//runtime:runtime_equality", + "//runtime/standard:standard_function", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "in_android", + srcs = ["InOperator.java"], + tags = [ + ], + deps = [ + ":standard_function_android", + ":standard_overload_android", + "//common:operator_android", + "//common:options", + "//runtime:function_binding_android", + "//runtime:runtime_equality_android", + "@maven_android//:com_google_guava_guava", + ], +) + +java_library( + name = "index", + srcs = ["IndexOperator.java"], + tags = [ + ], + deps = [ + ":standard_overload", + "//common:operator", + "//common:options", + "//runtime:function_binding", + "//runtime:runtime_equality", + "//runtime:runtime_helpers", + "//runtime/standard:standard_function", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "index_android", + srcs = ["IndexOperator.java"], + tags = [ + ], + deps = [ + ":standard_function_android", + ":standard_overload_android", + "//common:operator_android", + "//common:options", + "//runtime:function_binding_android", + "//runtime:runtime_equality_android", + "//runtime:runtime_helpers_android", + "@maven_android//:com_google_guava_guava", + ], +) + +java_library( + name = "int", + srcs = ["IntFunction.java"], + tags = [ + ], + deps = [ + ":standard_overload", + "//common:options", + "//common/exceptions:bad_format", + "//common/exceptions:numeric_overflow", + "//common/internal:proto_time_utils", + "//runtime:function_binding", + "//runtime:runtime_equality", + "//runtime:runtime_helpers", + "//runtime/standard:standard_function", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + ], +) + +cel_android_library( + name = "int_android", + srcs = ["IntFunction.java"], + tags = [ + ], + deps = [ + ":standard_function_android", + ":standard_overload_android", + "//common:options", + "//common/exceptions:bad_format", + "//common/exceptions:numeric_overflow", + "//common/internal:proto_time_utils_android", + "//runtime:function_binding_android", + "//runtime:runtime_equality_android", + "//runtime:runtime_helpers_android", + "@maven_android//:com_google_guava_guava", + "@maven_android//:com_google_protobuf_protobuf_javalite", + ], +) + +java_library( + name = "less", + srcs = ["LessOperator.java"], + tags = [ + ], + deps = [ + ":standard_overload", + "//common:operator", + "//common:options", + "//common/internal:comparison_functions", + "//common/internal:proto_time_utils", + "//common/values:cel_byte_string", + "//runtime:function_binding", + "//runtime:runtime_equality", + "//runtime:runtime_helpers", + "//runtime/standard:standard_function", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + ], +) + +cel_android_library( + name = "less_android", + srcs = ["LessOperator.java"], + tags = [ + ], + deps = [ + ":standard_function_android", + ":standard_overload_android", + "//common:operator_android", + "//common:options", + "//common/internal:comparison_functions_android", + "//common/internal:proto_time_utils_android", + "//common/values:cel_byte_string", + "//runtime:function_binding_android", + "//runtime:runtime_equality_android", + "//runtime:runtime_helpers_android", + "@maven_android//:com_google_guava_guava", + "@maven_android//:com_google_protobuf_protobuf_javalite", + ], +) + +java_library( + name = "less_equals", + srcs = ["LessEqualsOperator.java"], + tags = [ + ], + deps = [ + ":standard_overload", + "//common:operator", + "//common:options", + "//common/internal:comparison_functions", + "//common/internal:proto_time_utils", + "//common/values:cel_byte_string", + "//runtime:function_binding", + "//runtime:runtime_equality", + "//runtime:runtime_helpers", + "//runtime/standard:standard_function", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + ], +) + +cel_android_library( + name = "less_equals_android", + srcs = ["LessEqualsOperator.java"], + tags = [ + ], + deps = [ + ":standard_function_android", + ":standard_overload_android", + "//common:operator_android", + "//common:options", + "//common/internal:comparison_functions_android", + "//common/internal:proto_time_utils_android", + "//common/values:cel_byte_string", + "//runtime:function_binding_android", + "//runtime:runtime_equality_android", + "//runtime:runtime_helpers_android", + "@maven_android//:com_google_guava_guava", + "@maven_android//:com_google_protobuf_protobuf_javalite", + ], +) + +java_library( + name = "logical_not", + srcs = ["LogicalNotOperator.java"], + tags = [ + ], + deps = [ + ":standard_overload", + "//common:operator", + "//common:options", + "//runtime:function_binding", + "//runtime:runtime_equality", + "//runtime/standard:standard_function", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "logical_not_android", + srcs = ["LogicalNotOperator.java"], + tags = [ + ], + deps = [ + ":standard_function_android", + ":standard_overload_android", + "//common:operator_android", + "//common:options", + "//runtime:function_binding_android", + "//runtime:runtime_equality_android", + "@maven_android//:com_google_guava_guava", + ], +) + +java_library( + name = "matches", + srcs = ["MatchesFunction.java"], + tags = [ + ], + deps = [ + ":standard_overload", + "//common:options", + "//common/exceptions:invalid_argument", + "//runtime:function_binding", + "//runtime:runtime_equality", + "//runtime:runtime_helpers", + "//runtime/standard:standard_function", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "matches_android", + srcs = ["MatchesFunction.java"], + tags = [ + ], + deps = [ + ":standard_function_android", + ":standard_overload_android", + "//common:options", + "//common/exceptions:invalid_argument", + "//runtime:function_binding_android", + "//runtime:runtime_equality_android", + "//runtime:runtime_helpers_android", + "@maven_android//:com_google_guava_guava", + ], +) + +java_library( + name = "modulo", + srcs = ["ModuloOperator.java"], + tags = [ + ], + deps = [ + ":standard_overload", + "//common:operator", + "//common:options", + "//common/exceptions:divide_by_zero", + "//runtime:function_binding", + "//runtime:runtime_equality", + "//runtime:runtime_helpers", + "//runtime/standard:standard_function", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "modulo_android", + srcs = ["ModuloOperator.java"], + tags = [ + ], + deps = [ + ":standard_function_android", + ":standard_overload_android", + "//common:operator_android", + "//common:options", + "//common/exceptions:divide_by_zero", + "//runtime:function_binding_android", + "//runtime:runtime_equality_android", + "//runtime:runtime_helpers_android", + "@maven_android//:com_google_guava_guava", + ], +) + +java_library( + name = "multiply", + srcs = ["MultiplyOperator.java"], + tags = [ + ], + deps = [ + ":standard_overload", + "//common:operator", + "//common:options", + "//common/exceptions:numeric_overflow", + "//runtime:function_binding", + "//runtime:runtime_equality", + "//runtime:runtime_helpers", + "//runtime/standard:standard_function", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "multiply_android", + srcs = ["MultiplyOperator.java"], + tags = [ + ], + deps = [ + ":standard_function_android", + ":standard_overload_android", + "//common:operator_android", + "//common:options", + "//common/exceptions:numeric_overflow", + "//runtime:function_binding_android", + "//runtime:runtime_equality_android", + "//runtime:runtime_helpers_android", + "@maven_android//:com_google_guava_guava", + ], +) + +java_library( + name = "divide", + srcs = ["DivideOperator.java"], + tags = [ + ], + deps = [ + ":standard_overload", + "//common:operator", + "//common:options", + "//common/exceptions:divide_by_zero", + "//runtime:function_binding", + "//runtime:runtime_equality", + "//runtime:runtime_helpers", + "//runtime/standard:standard_function", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "divide_android", + srcs = ["DivideOperator.java"], + tags = [ + ], + deps = [ + ":standard_function_android", + ":standard_overload_android", + "//common:operator_android", + "//common:options", + "//common/exceptions:divide_by_zero", + "//runtime:function_binding_android", + "//runtime:runtime_equality_android", + "//runtime:runtime_helpers_android", + "@maven_android//:com_google_guava_guava", + ], +) + +java_library( + name = "negate", + srcs = ["NegateOperator.java"], + tags = [ + ], + deps = [ + ":standard_overload", + "//common:operator", + "//common:options", + "//common/exceptions:numeric_overflow", + "//runtime:function_binding", + "//runtime:runtime_equality", + "//runtime:runtime_helpers", + "//runtime/standard:standard_function", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "negate_android", + srcs = ["NegateOperator.java"], + tags = [ + ], + deps = [ + ":standard_function_android", + ":standard_overload_android", + "//common:operator_android", + "//common:options", + "//common/exceptions:numeric_overflow", + "//runtime:function_binding_android", + "//runtime:runtime_equality_android", + "//runtime:runtime_helpers_android", + "@maven_android//:com_google_guava_guava", + ], +) + +java_library( + name = "not_equals", + srcs = ["NotEqualsOperator.java"], + tags = [ + ], + deps = [ + ":standard_overload", + "//common:operator", + "//common:options", + "//runtime:function_binding", + "//runtime:runtime_equality", + "//runtime/standard:standard_function", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "not_equals_android", + srcs = ["NotEqualsOperator.java"], + tags = [ + ], + deps = [ + ":standard_function_android", + ":standard_overload_android", + "//common:operator_android", + "//common:options", + "//runtime:function_binding_android", + "//runtime:runtime_equality_android", + "@maven_android//:com_google_guava_guava", + ], +) + +java_library( + name = "size", + srcs = ["SizeFunction.java"], + tags = [ + ], + deps = [ + ":standard_overload", + "//common:options", + "//common/values:cel_byte_string", + "//runtime:function_binding", + "//runtime:runtime_equality", + "//runtime/standard:standard_function", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + ], +) + +cel_android_library( + name = "size_android", + srcs = ["SizeFunction.java"], + tags = [ + ], + deps = [ + ":standard_function_android", + ":standard_overload_android", + "//common:options", + "//common/values:cel_byte_string", + "//runtime:function_binding_android", + "//runtime:runtime_equality_android", + "@maven_android//:com_google_guava_guava", + "@maven_android//:com_google_protobuf_protobuf_javalite", + ], +) + +java_library( + name = "starts_with", + srcs = ["StartsWithFunction.java"], + tags = [ + ], + deps = [ + ":standard_overload", + "//common:options", + "//runtime:function_binding", + "//runtime:runtime_equality", + "//runtime/standard:standard_function", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "starts_with_android", + srcs = ["StartsWithFunction.java"], + tags = [ + ], + deps = [ + ":standard_function_android", + ":standard_overload_android", + "//common:options", + "//runtime:function_binding_android", + "//runtime:runtime_equality_android", + "@maven_android//:com_google_guava_guava", + ], +) + +java_library( + name = "string", + srcs = ["StringFunction.java"], + tags = [ + ], + deps = [ + ":standard_overload", + "//common:options", + "//common/exceptions:bad_format", + "//common/internal:date_time_helpers", + "//common/internal:proto_time_utils", + "//common/values:cel_byte_string", + "//runtime:function_binding", + "//runtime:runtime_equality", + "//runtime/standard:standard_function", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + ], +) + +cel_android_library( + name = "string_android", + srcs = ["StringFunction.java"], + tags = [ + ], + deps = [ + ":standard_function_android", + ":standard_overload_android", + "//common:options", + "//common/exceptions:bad_format", + "//common/internal:date_time_helpers_android", + "//common/internal:proto_time_utils_android", + "//common/values:cel_byte_string", + "//runtime:function_binding_android", + "//runtime:runtime_equality_android", + "@maven_android//:com_google_guava_guava", + "@maven_android//:com_google_protobuf_protobuf_javalite", + ], +) + +java_library( + name = "timestamp", + srcs = ["TimestampFunction.java"], + tags = [ + ], + deps = [ + ":standard_overload", + "//common:options", + "//common/exceptions:bad_format", + "//common/internal:date_time_helpers", + "//common/internal:proto_time_utils", + "//runtime:function_binding", + "//runtime:runtime_equality", + "//runtime/standard:standard_function", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + ], +) + +cel_android_library( + name = "timestamp_android", + srcs = ["TimestampFunction.java"], + tags = [ + ], + deps = [ + ":standard_function_android", + ":standard_overload_android", + "//common:options", + "//common/exceptions:bad_format", + "//common/internal:date_time_helpers_android", + "//common/internal:proto_time_utils_android", + "//runtime:function_binding_android", + "//runtime:runtime_equality_android", + "@maven_android//:com_google_guava_guava", + "@maven_android//:com_google_protobuf_protobuf_javalite", + ], +) + +java_library( + name = "type", + srcs = ["TypeFunction.java"], + tags = [ + ], + deps = [ + ":standard_function", + ":standard_overload", + "//common:options", + "//common/types", + "//runtime:function_binding", + "//runtime:runtime_equality", + "//runtime:type_resolver", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "type_android", + srcs = ["TypeFunction.java"], + tags = [ + ], + deps = [ + ":standard_function_android", + ":standard_overload_android", + "//common:options", + "//common/types:types_android", + "//runtime:function_binding_android", + "//runtime:runtime_equality_android", + "//runtime:type_resolver_android", + "@maven_android//:com_google_guava_guava", + ], +) + +java_library( + name = "uint", + srcs = ["UintFunction.java"], + tags = [ + ], + deps = [ + ":standard_overload", + "//common:options", + "//common/exceptions:bad_format", + "//common/exceptions:numeric_overflow", + "//runtime:function_binding", + "//runtime:runtime_equality", + "//runtime:runtime_helpers", + "//runtime/standard:standard_function", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "uint_android", + srcs = ["UintFunction.java"], + tags = [ + ], + deps = [ + ":standard_function_android", + ":standard_overload_android", + "//common:options", + "//common/exceptions:bad_format", + "//common/exceptions:numeric_overflow", + "//runtime:function_binding_android", + "//runtime:runtime_equality_android", + "//runtime:runtime_helpers_android", + "@maven_android//:com_google_guava_guava", + ], +) + +java_library( + name = "not_strictly_false", + srcs = ["NotStrictlyFalseFunction.java"], + tags = [ + ], + deps = [ + ":standard_overload", + "//common:operator", + "//common:options", + "//runtime:function_binding", + "//runtime:internal_function_binder", + "//runtime:runtime_equality", + "//runtime/standard:standard_function", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "not_strictly_false_android", + srcs = ["NotStrictlyFalseFunction.java"], + tags = [ + ], + deps = [ + ":standard_function_android", + ":standard_overload_android", + "//common:operator_android", + "//common:options", + "//runtime:function_binding_android", + "//runtime:internal_function_binder_android", + "//runtime:runtime_equality_android", + "@maven_android//:com_google_guava_guava", + ], +) + +java_library( + name = "standard_overload", + srcs = ["CelStandardOverload.java"], + tags = [ + ], + deps = [ + "//common:options", + "//runtime:function_binding", + "//runtime:runtime_equality", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +cel_android_library( + name = "standard_overload_android", + srcs = ["CelStandardOverload.java"], + tags = [ + ], + deps = [ + "//common:options", + "//runtime:function_binding_android", + "//runtime:runtime_equality_android", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) diff --git a/runtime/src/main/java/dev/cel/runtime/standard/BoolFunction.java b/runtime/src/main/java/dev/cel/runtime/standard/BoolFunction.java new file mode 100644 index 000000000..668934aac --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/BoolFunction.java @@ -0,0 +1,89 @@ +// 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.standard; + +import com.google.common.collect.ImmutableSet; +import dev.cel.common.CelOptions; +import dev.cel.common.exceptions.CelBadFormatException; +import dev.cel.common.internal.SafeStringFormatter; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.RuntimeEquality; +import java.util.Arrays; + +/** Standard function for {@code bool} conversion function. */ +public final class BoolFunction extends CelStandardFunction { + private static final BoolFunction ALL_OVERLOADS = create(BoolOverload.values()); + + public static BoolFunction create() { + return ALL_OVERLOADS; + } + + public static BoolFunction create(BoolFunction.BoolOverload... overloads) { + return create(Arrays.asList(overloads)); + } + + public static BoolFunction create(Iterable overloads) { + return new BoolFunction(ImmutableSet.copyOf(overloads)); + } + + /** Overloads for the standard function. */ + public enum BoolOverload implements CelStandardOverload { + BOOL_TO_BOOL( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from("bool_to_bool", Boolean.class, (Boolean x) -> x)), + STRING_TO_BOOL( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "string_to_bool", + String.class, + (String str) -> { + switch (str) { + case "true": + case "TRUE": + case "True": + case "t": + case "1": + return true; + case "false": + case "FALSE": + case "False": + case "f": + case "0": + return false; + default: + throw new CelBadFormatException( + SafeStringFormatter.format( + "Type conversion error from 'string' to 'bool': [%s]", str)); + } + })); + + private final CelStandardOverload standardOverload; + ; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return standardOverload.newFunctionBinding(celOptions, runtimeEquality); + } + + BoolOverload(CelStandardOverload standardOverload) { + this.standardOverload = standardOverload; + } + } + + private BoolFunction(ImmutableSet overloads) { + super("bool", overloads); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/standard/BytesFunction.java b/runtime/src/main/java/dev/cel/runtime/standard/BytesFunction.java new file mode 100644 index 000000000..7e3ab2b2f --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/BytesFunction.java @@ -0,0 +1,81 @@ +// 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.standard; + +import com.google.common.collect.ImmutableSet; +import com.google.protobuf.ByteString; +import dev.cel.common.CelOptions; +import dev.cel.common.values.CelByteString; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.RuntimeEquality; +import java.util.Arrays; + +/** Standard function for {@code bytes} conversion function. */ +public final class BytesFunction extends CelStandardFunction { + private static final BytesFunction ALL_OVERLOADS = create(BytesOverload.values()); + + public static BytesFunction create() { + return ALL_OVERLOADS; + } + + public static BytesFunction create(BytesFunction.BytesOverload... overloads) { + return create(Arrays.asList(overloads)); + } + + public static BytesFunction create(Iterable overloads) { + return new BytesFunction(ImmutableSet.copyOf(overloads)); + } + + /** Overloads for the standard function. */ + public enum BytesOverload implements CelStandardOverload { + BYTES_TO_BYTES( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "bytes_to_bytes", CelByteString.class, (CelByteString x) -> x); + } else { + return CelFunctionBinding.from("bytes_to_bytes", ByteString.class, (ByteString x) -> x); + } + }), + STRING_TO_BYTES( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "string_to_bytes", String.class, CelByteString::copyFromUtf8); + } else { + return CelFunctionBinding.from( + "string_to_bytes", String.class, ByteString::copyFromUtf8); + } + }), + ; + + private final CelStandardOverload standardOverload; + ; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return standardOverload.newFunctionBinding(celOptions, runtimeEquality); + } + + BytesOverload(CelStandardOverload standardOverload) { + this.standardOverload = standardOverload; + } + } + + private BytesFunction(ImmutableSet overloads) { + super("bytes", overloads); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/standard/CelStandardFunction.java b/runtime/src/main/java/dev/cel/runtime/standard/CelStandardFunction.java new file mode 100644 index 000000000..f9f919413 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/CelStandardFunction.java @@ -0,0 +1,52 @@ +// 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.standard; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.collect.ImmutableSet.toImmutableSet; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableSet; +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.CelOptions; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.RuntimeEquality; + +/** + * An abstract class that describes a CEL standard function. An implementation should provide a set + * of overloads for the standard function + */ +@Immutable +public abstract class CelStandardFunction { + private final String name; + private final ImmutableSet overloads; + + public ImmutableSet newFunctionBindings( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + ImmutableSet overloadBindings = + overloads.stream() + .map(overload -> overload.newFunctionBinding(celOptions, runtimeEquality)) + .collect(toImmutableSet()); + + return CelFunctionBinding.fromOverloads(name, overloadBindings); + } + + CelStandardFunction(String name, ImmutableSet overloads) { + checkArgument(!Strings.isNullOrEmpty(name), "Function name must be provided."); + checkArgument(!overloads.isEmpty(), "At least 1 overload must be provided."); + this.overloads = overloads; + this.name = name; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/standard/CelStandardOverload.java b/runtime/src/main/java/dev/cel/runtime/standard/CelStandardOverload.java new file mode 100644 index 000000000..aad6e468b --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/CelStandardOverload.java @@ -0,0 +1,33 @@ +// 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.standard; + +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.CelOptions; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.RuntimeEquality; + +/** + * {@code CelStandardOverload} defines an interface for a standard function's overload. The + * implementation should produce a concrete {@link CelFunctionBinding} for the standard function's + * overload. + */ +@Immutable +@FunctionalInterface +public interface CelStandardOverload { + + /** Constructs a new {@link CelFunctionBinding} for this CEL standard overload. */ + CelFunctionBinding newFunctionBinding(CelOptions celOptions, RuntimeEquality runtimeEquality); +} diff --git a/runtime/src/main/java/dev/cel/runtime/standard/ContainsFunction.java b/runtime/src/main/java/dev/cel/runtime/standard/ContainsFunction.java new file mode 100644 index 000000000..bf3f9f7a7 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/ContainsFunction.java @@ -0,0 +1,63 @@ +// 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.standard; + +import com.google.common.collect.ImmutableSet; +import dev.cel.common.CelOptions; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.RuntimeEquality; +import java.util.Arrays; + +/** Standard function for {@code contains}. */ +public final class ContainsFunction extends CelStandardFunction { + private static final ContainsFunction ALL_OVERLOADS = create(ContainsOverload.values()); + + public static ContainsFunction create() { + return ALL_OVERLOADS; + } + + public static ContainsFunction create(ContainsFunction.ContainsOverload... overloads) { + return create(Arrays.asList(overloads)); + } + + public static ContainsFunction create(Iterable overloads) { + return new ContainsFunction(ImmutableSet.copyOf(overloads)); + } + + /** Overloads for the standard function. */ + public enum ContainsOverload implements CelStandardOverload { + CONTAINS_STRING( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "contains_string", String.class, String.class, String::contains)), + ; + + private final CelStandardOverload standardOverload; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return standardOverload.newFunctionBinding(celOptions, runtimeEquality); + } + + ContainsOverload(CelStandardOverload standardOverload) { + this.standardOverload = standardOverload; + } + } + + private ContainsFunction(ImmutableSet overloads) { + super("contains", overloads); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/standard/DivideOperator.java b/runtime/src/main/java/dev/cel/runtime/standard/DivideOperator.java new file mode 100644 index 000000000..ddd78cef8 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/DivideOperator.java @@ -0,0 +1,95 @@ +// 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.standard; + +import static dev.cel.common.Operator.DIVIDE; + +import com.google.common.collect.ImmutableSet; +import com.google.common.primitives.UnsignedLong; +import dev.cel.common.CelOptions; +import dev.cel.common.exceptions.CelDivideByZeroException; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.RuntimeEquality; +import dev.cel.runtime.RuntimeHelpers; +import java.util.Arrays; + +/** Standard function for the division (/) operator. */ +public final class DivideOperator extends CelStandardFunction { + private static final DivideOperator ALL_OVERLOADS = create(DivideOverload.values()); + + public static DivideOperator create() { + return ALL_OVERLOADS; + } + + public static DivideOperator create(DivideOverload... overloads) { + return create(Arrays.asList(overloads)); + } + + public static DivideOperator create(Iterable overloads) { + return new DivideOperator(ImmutableSet.copyOf(overloads)); + } + + /** Overloads for the standard function. */ + public enum DivideOverload implements CelStandardOverload { + DIVIDE_DOUBLE( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "divide_double", Double.class, Double.class, (Double x, Double y) -> x / y)), + DIVIDE_INT64( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "divide_int64", + Long.class, + Long.class, + (Long x, Long y) -> { + try { + return RuntimeHelpers.int64Divide(x, y, celOptions); + } catch (ArithmeticException e) { + throw new CelDivideByZeroException(e); + } + })), + DIVIDE_UINT64( + (celOptions, runtimeEquality) -> { + if (celOptions.enableUnsignedLongs()) { + return CelFunctionBinding.from( + "divide_uint64", + UnsignedLong.class, + UnsignedLong.class, + RuntimeHelpers::uint64Divide); + } else { + return CelFunctionBinding.from( + "divide_uint64", + Long.class, + Long.class, + (Long x, Long y) -> RuntimeHelpers.uint64Divide(x, y, celOptions)); + } + }); + private final CelStandardOverload standardOverload; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return standardOverload.newFunctionBinding(celOptions, runtimeEquality); + } + + DivideOverload(CelStandardOverload standardOverload) { + this.standardOverload = standardOverload; + } + } + + private DivideOperator(ImmutableSet overloads) { + super(DIVIDE.getFunction(), overloads); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/standard/DoubleFunction.java b/runtime/src/main/java/dev/cel/runtime/standard/DoubleFunction.java new file mode 100644 index 000000000..b8d4b74c0 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/DoubleFunction.java @@ -0,0 +1,91 @@ +// 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.standard; + +import com.google.common.collect.ImmutableSet; +import com.google.common.primitives.UnsignedLong; +import dev.cel.common.CelOptions; +import dev.cel.common.exceptions.CelBadFormatException; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.RuntimeEquality; +import java.util.Arrays; + +/** Standard function for {@code double} conversion function. */ +public final class DoubleFunction extends CelStandardFunction { + private static final DoubleFunction ALL_OVERLOADS = create(DoubleOverload.values()); + + public static DoubleFunction create() { + return ALL_OVERLOADS; + } + + public static DoubleFunction create(DoubleFunction.DoubleOverload... overloads) { + return create(Arrays.asList(overloads)); + } + + public static DoubleFunction create(Iterable overloads) { + return new DoubleFunction(ImmutableSet.copyOf(overloads)); + } + + /** Overloads for the standard function. */ + public enum DoubleOverload implements CelStandardOverload { + DOUBLE_TO_DOUBLE( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from("double_to_double", Double.class, (Double x) -> x)), + INT64_TO_DOUBLE( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from("int64_to_double", Long.class, Long::doubleValue)), + STRING_TO_DOUBLE( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "string_to_double", + String.class, + (String arg) -> { + try { + return Double.parseDouble(arg); + } catch (NumberFormatException e) { + throw new CelBadFormatException(e); + } + })), + UINT64_TO_DOUBLE( + (celOptions, runtimeEquality) -> { + if (celOptions.enableUnsignedLongs()) { + return CelFunctionBinding.from( + "uint64_to_double", UnsignedLong.class, UnsignedLong::doubleValue); + } else { + return CelFunctionBinding.from( + "uint64_to_double", + Long.class, + (Long arg) -> UnsignedLong.fromLongBits(arg).doubleValue()); + } + }), + ; + + private final CelStandardOverload standardOverload; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return standardOverload.newFunctionBinding(celOptions, runtimeEquality); + } + + DoubleOverload(CelStandardOverload standardOverload) { + this.standardOverload = standardOverload; + } + } + + private DoubleFunction(ImmutableSet overloads) { + super("double", overloads); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/standard/DurationFunction.java b/runtime/src/main/java/dev/cel/runtime/standard/DurationFunction.java new file mode 100644 index 000000000..e1f11644e --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/DurationFunction.java @@ -0,0 +1,92 @@ +// 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.standard; + +import com.google.common.collect.ImmutableSet; +import com.google.protobuf.Duration; +import dev.cel.common.CelOptions; +import dev.cel.common.exceptions.CelBadFormatException; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.RuntimeEquality; +import dev.cel.runtime.RuntimeHelpers; +import java.util.Arrays; + +/** Standard function for {@code duration} conversion function. */ +public final class DurationFunction extends CelStandardFunction { + private static final DurationFunction ALL_OVERLOADS = create(DurationOverload.values()); + + public static DurationFunction create() { + return ALL_OVERLOADS; + } + + public static DurationFunction create(DurationFunction.DurationOverload... overloads) { + return create(Arrays.asList(overloads)); + } + + public static DurationFunction create(Iterable overloads) { + return new DurationFunction(ImmutableSet.copyOf(overloads)); + } + + /** Overloads for the standard function. */ + public enum DurationOverload implements CelStandardOverload { + DURATION_TO_DURATION( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "duration_to_duration", java.time.Duration.class, (java.time.Duration d) -> d); + } else { + return CelFunctionBinding.from( + "duration_to_duration", Duration.class, (Duration d) -> d); + } + }), + STRING_TO_DURATION( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "string_to_duration", + String.class, + (String d) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + try { + return RuntimeHelpers.createJavaDurationFromString(d); + } catch (IllegalArgumentException e) { + throw new CelBadFormatException(e); + } + } else { + try { + return RuntimeHelpers.createDurationFromString(d); + } catch (IllegalArgumentException e) { + throw new CelBadFormatException(e); + } + } + })), + ; + + private final CelStandardOverload standardOverload; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return standardOverload.newFunctionBinding(celOptions, runtimeEquality); + } + + DurationOverload(CelStandardOverload standardOverload) { + this.standardOverload = standardOverload; + } + } + + private DurationFunction(ImmutableSet overloads) { + super("duration", overloads); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/standard/DynFunction.java b/runtime/src/main/java/dev/cel/runtime/standard/DynFunction.java new file mode 100644 index 000000000..da855de82 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/DynFunction.java @@ -0,0 +1,62 @@ +// 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.standard; + +import com.google.common.collect.ImmutableSet; +import dev.cel.common.CelOptions; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.RuntimeEquality; +import java.util.Arrays; + +/** Standard function for {@code dyn} conversion function. */ +public final class DynFunction extends CelStandardFunction { + + private static final DynFunction ALL_OVERLOADS = create(DynOverload.values()); + + public static DynFunction create() { + return ALL_OVERLOADS; + } + + public static DynFunction create(DynFunction.DynOverload... overloads) { + return create(Arrays.asList(overloads)); + } + + public static DynFunction create(Iterable overloads) { + return new DynFunction(ImmutableSet.copyOf(overloads)); + } + + /** Overloads for the standard function. */ + public enum DynOverload implements CelStandardOverload { + TO_DYN( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from("to_dyn", Object.class, (Object arg) -> arg)); + + private final CelStandardOverload standardOverload; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return standardOverload.newFunctionBinding(celOptions, runtimeEquality); + } + + DynOverload(CelStandardOverload standardOverload) { + this.standardOverload = standardOverload; + } + } + + private DynFunction(ImmutableSet overloads) { + super("dyn", overloads); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/standard/EndsWithFunction.java b/runtime/src/main/java/dev/cel/runtime/standard/EndsWithFunction.java new file mode 100644 index 000000000..7f4e8c035 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/EndsWithFunction.java @@ -0,0 +1,64 @@ +// 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.standard; + +import com.google.common.collect.ImmutableSet; +import dev.cel.common.CelOptions; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.RuntimeEquality; +import java.util.Arrays; + +/** Standard function for {@code endsWith}. */ +public final class EndsWithFunction extends CelStandardFunction { + private static final EndsWithFunction ALL_OVERLOADS = create(EndsWithOverload.values()); + + public static EndsWithFunction create() { + return ALL_OVERLOADS; + } + + public static EndsWithFunction create(EndsWithFunction.EndsWithOverload... overloads) { + return create(Arrays.asList(overloads)); + } + + public static EndsWithFunction create(Iterable overloads) { + return new EndsWithFunction(ImmutableSet.copyOf(overloads)); + } + + /** Overloads for the standard function. */ + public enum EndsWithOverload implements CelStandardOverload { + ENDS_WITH_STRING( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "ends_with_string", String.class, String.class, String::endsWith)), + ; + + private final CelStandardOverload standardOverload; + ; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return standardOverload.newFunctionBinding(celOptions, runtimeEquality); + } + + EndsWithOverload(CelStandardOverload standardOverload) { + this.standardOverload = standardOverload; + } + } + + private EndsWithFunction(ImmutableSet overloads) { + super("endsWith", overloads); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/standard/EqualsOperator.java b/runtime/src/main/java/dev/cel/runtime/standard/EqualsOperator.java new file mode 100644 index 000000000..c505e069e --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/EqualsOperator.java @@ -0,0 +1,65 @@ +// 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.standard; + +import static dev.cel.common.Operator.EQUALS; + +import com.google.common.collect.ImmutableSet; +import dev.cel.common.CelOptions; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.RuntimeEquality; +import java.util.Arrays; + +/** Standard function for the equals (=) operator. */ +public final class EqualsOperator extends CelStandardFunction { + private static final EqualsOperator ALL_OVERLOADS = create(EqualsOverload.values()); + + public static EqualsOperator create() { + return ALL_OVERLOADS; + } + + public static EqualsOperator create(EqualsOperator.EqualsOverload... overloads) { + return create(Arrays.asList(overloads)); + } + + public static EqualsOperator create(Iterable overloads) { + return new EqualsOperator(ImmutableSet.copyOf(overloads)); + } + + /** Overloads for the standard function. */ + public enum EqualsOverload implements CelStandardOverload { + EQUALS( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "equals", Object.class, Object.class, runtimeEquality::objectEquals)), + ; + + private final CelStandardOverload standardOverload; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return standardOverload.newFunctionBinding(celOptions, runtimeEquality); + } + + EqualsOverload(CelStandardOverload standardOverload) { + this.standardOverload = standardOverload; + } + } + + private EqualsOperator(ImmutableSet overloads) { + super(EQUALS.getFunction(), overloads); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/standard/GetDateFunction.java b/runtime/src/main/java/dev/cel/runtime/standard/GetDateFunction.java new file mode 100644 index 000000000..bb0fb0d79 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/GetDateFunction.java @@ -0,0 +1,94 @@ +// 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.standard; + +import static dev.cel.common.internal.DateTimeHelpers.UTC; +import static dev.cel.common.internal.DateTimeHelpers.newLocalDateTime; + +import com.google.common.collect.ImmutableSet; +import com.google.protobuf.Timestamp; +import dev.cel.common.CelOptions; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.RuntimeEquality; +import java.time.Instant; +import java.util.Arrays; + +/** Standard function for {@code getDate}. */ +public final class GetDateFunction extends CelStandardFunction { + private static final GetDateFunction ALL_OVERLOADS = create(GetDateOverload.values()); + + public static GetDateFunction create() { + return ALL_OVERLOADS; + } + + public static GetDateFunction create(GetDateFunction.GetDateOverload... overloads) { + return create(Arrays.asList(overloads)); + } + + public static GetDateFunction create(Iterable overloads) { + return new GetDateFunction(ImmutableSet.copyOf(overloads)); + } + + /** Overloads for the standard function. */ + public enum GetDateOverload implements CelStandardOverload { + TIMESTAMP_TO_DAY_OF_MONTH_1_BASED( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "timestamp_to_day_of_month_1_based", + Instant.class, + (Instant ts) -> (long) newLocalDateTime(ts, UTC).getDayOfMonth()); + } else { + return CelFunctionBinding.from( + "timestamp_to_day_of_month_1_based", + Timestamp.class, + (Timestamp ts) -> (long) newLocalDateTime(ts, UTC).getDayOfMonth()); + } + }), + TIMESTAMP_TO_DAY_OF_MONTH_1_BASED_WITH_TZ( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "timestamp_to_day_of_month_1_based_with_tz", + Instant.class, + String.class, + (Instant ts, String tz) -> (long) newLocalDateTime(ts, tz).getDayOfMonth()); + } else { + return CelFunctionBinding.from( + "timestamp_to_day_of_month_1_based_with_tz", + Timestamp.class, + String.class, + (Timestamp ts, String tz) -> (long) newLocalDateTime(ts, tz).getDayOfMonth()); + } + }), + ; + + private final CelStandardOverload standardOverload; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return standardOverload.newFunctionBinding(celOptions, runtimeEquality); + } + + GetDateOverload(CelStandardOverload standardOverload) { + this.standardOverload = standardOverload; + } + } + + private GetDateFunction(ImmutableSet overloads) { + super("getDate", overloads); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/standard/GetDayOfMonthFunction.java b/runtime/src/main/java/dev/cel/runtime/standard/GetDayOfMonthFunction.java new file mode 100644 index 000000000..e35888fb5 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/GetDayOfMonthFunction.java @@ -0,0 +1,95 @@ +// 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.standard; + +import static dev.cel.common.internal.DateTimeHelpers.UTC; +import static dev.cel.common.internal.DateTimeHelpers.newLocalDateTime; + +import com.google.common.collect.ImmutableSet; +import com.google.protobuf.Timestamp; +import dev.cel.common.CelOptions; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.RuntimeEquality; +import java.time.Instant; +import java.util.Arrays; + +/** Standard function for {@code getDayOfMonth}. */ +public final class GetDayOfMonthFunction extends CelStandardFunction { + private static final GetDayOfMonthFunction ALL_OVERLOADS = create(GetDayOfMonthOverload.values()); + + public static GetDayOfMonthFunction create() { + return ALL_OVERLOADS; + } + + public static GetDayOfMonthFunction create( + GetDayOfMonthFunction.GetDayOfMonthOverload... overloads) { + return create(Arrays.asList(overloads)); + } + + public static GetDayOfMonthFunction create( + Iterable overloads) { + return new GetDayOfMonthFunction(ImmutableSet.copyOf(overloads)); + } + + /** Overloads for the standard function. */ + public enum GetDayOfMonthOverload implements CelStandardOverload { + TIMESTAMP_TO_DAY_OF_MONTH( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "timestamp_to_day_of_month", + Instant.class, + (Instant ts) -> (long) newLocalDateTime(ts, UTC).getDayOfMonth() - 1); + } else { + return CelFunctionBinding.from( + "timestamp_to_day_of_month", + Timestamp.class, + (Timestamp ts) -> (long) newLocalDateTime(ts, UTC).getDayOfMonth() - 1); + } + }), + TIMESTAMP_TO_DAY_OF_MONTH_WITH_TZ( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "timestamp_to_day_of_month_with_tz", + Instant.class, + String.class, + (Instant ts, String tz) -> (long) newLocalDateTime(ts, tz).getDayOfMonth() - 1); + } else { + return CelFunctionBinding.from( + "timestamp_to_day_of_month_with_tz", + Timestamp.class, + String.class, + (Timestamp ts, String tz) -> (long) newLocalDateTime(ts, tz).getDayOfMonth() - 1); + } + }), + ; + private final CelStandardOverload standardOverload; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return standardOverload.newFunctionBinding(celOptions, runtimeEquality); + } + + GetDayOfMonthOverload(CelStandardOverload standardOverload) { + this.standardOverload = standardOverload; + } + } + + private GetDayOfMonthFunction(ImmutableSet overloads) { + super("getDayOfMonth", overloads); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/standard/GetDayOfWeekFunction.java b/runtime/src/main/java/dev/cel/runtime/standard/GetDayOfWeekFunction.java new file mode 100644 index 000000000..e2fa02961 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/GetDayOfWeekFunction.java @@ -0,0 +1,111 @@ +// 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.standard; + +import static dev.cel.common.internal.DateTimeHelpers.UTC; +import static dev.cel.common.internal.DateTimeHelpers.newLocalDateTime; + +import com.google.common.collect.ImmutableSet; +import com.google.protobuf.Timestamp; +import dev.cel.common.CelOptions; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.RuntimeEquality; +import java.time.DayOfWeek; +import java.time.Instant; +import java.util.Arrays; + +/** Standard function for {@code getDayOfWeek}. */ +public final class GetDayOfWeekFunction extends CelStandardFunction { + private static final GetDayOfWeekFunction ALL_OVERLOADS = create(GetDayOfWeekOverload.values()); + + public static GetDayOfWeekFunction create() { + return ALL_OVERLOADS; + } + + public static GetDayOfWeekFunction create( + GetDayOfWeekFunction.GetDayOfWeekOverload... overloads) { + return create(Arrays.asList(overloads)); + } + + public static GetDayOfWeekFunction create( + Iterable overloads) { + return new GetDayOfWeekFunction(ImmutableSet.copyOf(overloads)); + } + + /** Overloads for the standard function. */ + public enum GetDayOfWeekOverload implements CelStandardOverload { + TIMESTAMP_TO_DAY_OF_WEEK( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "timestamp_to_day_of_week", + Instant.class, + (Instant ts) -> { + // CEL treats Sunday as day 0, but Java.time treats it as day 7. + DayOfWeek dayOfWeek = newLocalDateTime(ts, UTC).getDayOfWeek(); + return (long) dayOfWeek.getValue() % 7; + }); + } else { + return CelFunctionBinding.from( + "timestamp_to_day_of_week", + Timestamp.class, + (Timestamp ts) -> { + // CEL treats Sunday as day 0, but Java.time treats it as day 7. + DayOfWeek dayOfWeek = newLocalDateTime(ts, UTC).getDayOfWeek(); + return (long) dayOfWeek.getValue() % 7; + }); + } + }), + TIMESTAMP_TO_DAY_OF_WEEK_WITH_TZ( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "timestamp_to_day_of_week_with_tz", + Instant.class, + String.class, + (Instant ts, String tz) -> { + // CEL treats Sunday as day 0, but Java.time treats it as day 7. + DayOfWeek dayOfWeek = newLocalDateTime(ts, tz).getDayOfWeek(); + return (long) dayOfWeek.getValue() % 7; + }); + } else { + return CelFunctionBinding.from( + "timestamp_to_day_of_week_with_tz", + Timestamp.class, + String.class, + (Timestamp ts, String tz) -> { + // CEL treats Sunday as day 0, but Java.time treats it as day 7. + DayOfWeek dayOfWeek = newLocalDateTime(ts, tz).getDayOfWeek(); + return (long) dayOfWeek.getValue() % 7; + }); + } + }); + private final CelStandardOverload standardOverload; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return standardOverload.newFunctionBinding(celOptions, runtimeEquality); + } + + GetDayOfWeekOverload(CelStandardOverload standardOverload) { + this.standardOverload = standardOverload; + } + } + + private GetDayOfWeekFunction(ImmutableSet overloads) { + super("getDayOfWeek", overloads); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/standard/GetDayOfYearFunction.java b/runtime/src/main/java/dev/cel/runtime/standard/GetDayOfYearFunction.java new file mode 100644 index 000000000..4c0e62637 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/GetDayOfYearFunction.java @@ -0,0 +1,96 @@ +// 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.standard; + +import static dev.cel.common.internal.DateTimeHelpers.UTC; +import static dev.cel.common.internal.DateTimeHelpers.newLocalDateTime; + +import com.google.common.collect.ImmutableSet; +import com.google.protobuf.Timestamp; +import dev.cel.common.CelOptions; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.RuntimeEquality; +import java.time.Instant; +import java.util.Arrays; + +/** Standard function for {@code getDayOfYear}. */ +public final class GetDayOfYearFunction extends CelStandardFunction { + private static final GetDayOfYearFunction ALL_OVERLOADS = create(GetDayOfYearOverload.values()); + + public static GetDayOfYearFunction create() { + return ALL_OVERLOADS; + } + + public static GetDayOfYearFunction create( + GetDayOfYearFunction.GetDayOfYearOverload... overloads) { + return create(Arrays.asList(overloads)); + } + + public static GetDayOfYearFunction create( + Iterable overloads) { + return new GetDayOfYearFunction(ImmutableSet.copyOf(overloads)); + } + + /** Overloads for the standard function. */ + public enum GetDayOfYearOverload implements CelStandardOverload { + TIMESTAMP_TO_DAY_OF_YEAR( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "timestamp_to_day_of_year", + Instant.class, + (Instant ts) -> (long) newLocalDateTime(ts, UTC).getDayOfYear() - 1); + } else { + return CelFunctionBinding.from( + "timestamp_to_day_of_year", + Timestamp.class, + (Timestamp ts) -> (long) newLocalDateTime(ts, UTC).getDayOfYear() - 1); + } + }), + TIMESTAMP_TO_DAY_OF_YEAR_WITH_TZ( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "timestamp_to_day_of_year_with_tz", + Instant.class, + String.class, + (Instant ts, String tz) -> (long) newLocalDateTime(ts, tz).getDayOfYear() - 1); + } else { + return CelFunctionBinding.from( + "timestamp_to_day_of_year_with_tz", + Timestamp.class, + String.class, + (Timestamp ts, String tz) -> (long) newLocalDateTime(ts, tz).getDayOfYear() - 1); + } + }), + ; + + private final CelStandardOverload standardOverload; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return standardOverload.newFunctionBinding(celOptions, runtimeEquality); + } + + GetDayOfYearOverload(CelStandardOverload standardOverload) { + this.standardOverload = standardOverload; + } + } + + private GetDayOfYearFunction(ImmutableSet overloads) { + super("getDayOfYear", overloads); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/standard/GetFullYearFunction.java b/runtime/src/main/java/dev/cel/runtime/standard/GetFullYearFunction.java new file mode 100644 index 000000000..925f61307 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/GetFullYearFunction.java @@ -0,0 +1,95 @@ +// 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.standard; + +import static dev.cel.common.internal.DateTimeHelpers.UTC; +import static dev.cel.common.internal.DateTimeHelpers.newLocalDateTime; + +import com.google.common.collect.ImmutableSet; +import com.google.protobuf.Timestamp; +import dev.cel.common.CelOptions; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.RuntimeEquality; +import java.time.Instant; +import java.util.Arrays; + +/** Standard function for {@code getFullYear}. */ +public final class GetFullYearFunction extends CelStandardFunction { + private static final GetFullYearFunction ALL_OVERLOADS = create(GetFullYearOverload.values()); + + public static GetFullYearFunction create() { + return ALL_OVERLOADS; + } + + public static GetFullYearFunction create(GetFullYearFunction.GetFullYearOverload... overloads) { + return create(Arrays.asList(overloads)); + } + + public static GetFullYearFunction create( + Iterable overloads) { + return new GetFullYearFunction(ImmutableSet.copyOf(overloads)); + } + + /** Overloads for the standard function. */ + public enum GetFullYearOverload implements CelStandardOverload { + TIMESTAMP_TO_YEAR( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "timestamp_to_year", + Instant.class, + (Instant ts) -> (long) newLocalDateTime(ts, UTC).getYear()); + } else { + return CelFunctionBinding.from( + "timestamp_to_year", + Timestamp.class, + (Timestamp ts) -> (long) newLocalDateTime(ts, UTC).getYear()); + } + }), + TIMESTAMP_TO_YEAR_WITH_TZ( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "timestamp_to_year_with_tz", + Instant.class, + String.class, + (Instant ts, String tz) -> (long) newLocalDateTime(ts, tz).getYear()); + } else { + return CelFunctionBinding.from( + "timestamp_to_year_with_tz", + Timestamp.class, + String.class, + (Timestamp ts, String tz) -> (long) newLocalDateTime(ts, tz).getYear()); + } + }), + ; + + private final CelStandardOverload standardOverload; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return standardOverload.newFunctionBinding(celOptions, runtimeEquality); + } + + GetFullYearOverload(CelStandardOverload standardOverload) { + this.standardOverload = standardOverload; + } + } + + private GetFullYearFunction(ImmutableSet overloads) { + super("getFullYear", overloads); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/standard/GetHoursFunction.java b/runtime/src/main/java/dev/cel/runtime/standard/GetHoursFunction.java new file mode 100644 index 000000000..afe16556f --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/GetHoursFunction.java @@ -0,0 +1,106 @@ +// 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.standard; + +import static dev.cel.common.internal.DateTimeHelpers.UTC; +import static dev.cel.common.internal.DateTimeHelpers.newLocalDateTime; + +import com.google.common.collect.ImmutableSet; +import com.google.protobuf.Duration; +import com.google.protobuf.Timestamp; +import dev.cel.common.CelOptions; +import dev.cel.common.internal.ProtoTimeUtils; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.RuntimeEquality; +import java.time.Instant; +import java.util.Arrays; + +/** Standard function for {@code getHours}. */ +public final class GetHoursFunction extends CelStandardFunction { + private static final GetHoursFunction ALL_OVERLOADS = create(GetHoursOverload.values()); + + public static GetHoursFunction create() { + return ALL_OVERLOADS; + } + + public static GetHoursFunction create(GetHoursFunction.GetHoursOverload... overloads) { + return create(Arrays.asList(overloads)); + } + + public static GetHoursFunction create(Iterable overloads) { + return new GetHoursFunction(ImmutableSet.copyOf(overloads)); + } + + /** Overloads for the standard function. */ + public enum GetHoursOverload implements CelStandardOverload { + TIMESTAMP_TO_HOURS( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "timestamp_to_hours", + Instant.class, + (Instant ts) -> (long) newLocalDateTime(ts, UTC).getHour()); + } else { + return CelFunctionBinding.from( + "timestamp_to_hours", + Timestamp.class, + (Timestamp ts) -> (long) newLocalDateTime(ts, UTC).getHour()); + } + }), + TIMESTAMP_TO_HOURS_WITH_TZ( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "timestamp_to_hours_with_tz", + Instant.class, + String.class, + (Instant ts, String tz) -> (long) newLocalDateTime(ts, tz).getHour()); + } else { + return CelFunctionBinding.from( + "timestamp_to_hours_with_tz", + Timestamp.class, + String.class, + (Timestamp ts, String tz) -> (long) newLocalDateTime(ts, tz).getHour()); + } + }), + DURATION_TO_HOURS( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "duration_to_hours", java.time.Duration.class, java.time.Duration::toHours); + } else { + return CelFunctionBinding.from( + "duration_to_hours", Duration.class, ProtoTimeUtils::toHours); + } + }), + ; + + private final CelStandardOverload standardOverload; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return standardOverload.newFunctionBinding(celOptions, runtimeEquality); + } + + GetHoursOverload(CelStandardOverload standardOverload) { + this.standardOverload = standardOverload; + } + } + + private GetHoursFunction(ImmutableSet overloads) { + super("getHours", overloads); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/standard/GetMillisecondsFunction.java b/runtime/src/main/java/dev/cel/runtime/standard/GetMillisecondsFunction.java new file mode 100644 index 000000000..32b17cc88 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/GetMillisecondsFunction.java @@ -0,0 +1,117 @@ +// 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.standard; + +import static dev.cel.common.internal.DateTimeHelpers.UTC; +import static dev.cel.common.internal.DateTimeHelpers.newLocalDateTime; + +import com.google.common.collect.ImmutableSet; +import com.google.protobuf.Duration; +import com.google.protobuf.Timestamp; +import dev.cel.common.CelOptions; +import dev.cel.common.internal.ProtoTimeUtils; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.RuntimeEquality; +import java.time.Instant; +import java.util.Arrays; + +/** Standard function for {@code getMilliseconds}. */ +public final class GetMillisecondsFunction extends CelStandardFunction { + private static final GetMillisecondsFunction ALL_OVERLOADS = + create(GetMillisecondsOverload.values()); + + public static GetMillisecondsFunction create() { + return ALL_OVERLOADS; + } + + public static GetMillisecondsFunction create( + GetMillisecondsFunction.GetMillisecondsOverload... overloads) { + return create(Arrays.asList(overloads)); + } + + public static GetMillisecondsFunction create( + Iterable overloads) { + return new GetMillisecondsFunction(ImmutableSet.copyOf(overloads)); + } + + /** Overloads for the standard function. */ + public enum GetMillisecondsOverload implements CelStandardOverload { + // We specifically need to only access nanos-of-second field for + // timestamp_to_milliseconds overload + @SuppressWarnings("JavaLocalDateTimeGetNano") + TIMESTAMP_TO_MILLISECONDS( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "timestamp_to_milliseconds", + Instant.class, + (Instant ts) -> (long) (newLocalDateTime(ts, UTC).getNano() / 1e+6)); + } else { + return CelFunctionBinding.from( + "timestamp_to_milliseconds", + Timestamp.class, + (Timestamp ts) -> (long) (newLocalDateTime(ts, UTC).getNano() / 1e+6)); + } + }), + @SuppressWarnings("JavaLocalDateTimeGetNano") + TIMESTAMP_TO_MILLISECONDS_WITH_TZ( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "timestamp_to_milliseconds_with_tz", + Instant.class, + String.class, + (Instant ts, String tz) -> (long) (newLocalDateTime(ts, tz).getNano() / 1e+6)); + } else { + return CelFunctionBinding.from( + "timestamp_to_milliseconds_with_tz", + Timestamp.class, + String.class, + (Timestamp ts, String tz) -> (long) (newLocalDateTime(ts, tz).getNano() / 1e+6)); + } + }), + DURATION_TO_MILLISECONDS( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "duration_to_milliseconds", + java.time.Duration.class, + (java.time.Duration d) -> d.toMillis() % 1_000); + } else { + return CelFunctionBinding.from( + "duration_to_milliseconds", + Duration.class, + (Duration arg) -> + ProtoTimeUtils.toMillis(arg) % java.time.Duration.ofSeconds(1).toMillis()); + } + }); + + private final CelStandardOverload standardOverload; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return standardOverload.newFunctionBinding(celOptions, runtimeEquality); + } + + GetMillisecondsOverload(CelStandardOverload standardOverload) { + this.standardOverload = standardOverload; + } + } + + private GetMillisecondsFunction(ImmutableSet overloads) { + super("getMilliseconds", overloads); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/standard/GetMinutesFunction.java b/runtime/src/main/java/dev/cel/runtime/standard/GetMinutesFunction.java new file mode 100644 index 000000000..5f701f1e1 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/GetMinutesFunction.java @@ -0,0 +1,107 @@ +// 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.standard; + +import static dev.cel.common.internal.DateTimeHelpers.UTC; +import static dev.cel.common.internal.DateTimeHelpers.newLocalDateTime; + +import com.google.common.collect.ImmutableSet; +import com.google.protobuf.Duration; +import com.google.protobuf.Timestamp; +import dev.cel.common.CelOptions; +import dev.cel.common.internal.ProtoTimeUtils; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.RuntimeEquality; +import java.time.Instant; +import java.util.Arrays; + +/** Standard function for {@code getMinutes}. */ +public final class GetMinutesFunction extends CelStandardFunction { + private static final GetMinutesFunction ALL_OVERLOADS = create(GetMinutesOverload.values()); + + public static GetMinutesFunction create() { + return ALL_OVERLOADS; + } + + public static GetMinutesFunction create(GetMinutesFunction.GetMinutesOverload... overloads) { + return create(Arrays.asList(overloads)); + } + + public static GetMinutesFunction create( + Iterable overloads) { + return new GetMinutesFunction(ImmutableSet.copyOf(overloads)); + } + + /** Overloads for the standard function. */ + public enum GetMinutesOverload implements CelStandardOverload { + TIMESTAMP_TO_MINUTES( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "timestamp_to_minutes", + Instant.class, + (Instant ts) -> (long) newLocalDateTime(ts, UTC).getMinute()); + } else { + return CelFunctionBinding.from( + "timestamp_to_minutes", + Timestamp.class, + (Timestamp ts) -> (long) newLocalDateTime(ts, UTC).getMinute()); + } + }), + TIMESTAMP_TO_MINUTES_WITH_TZ( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "timestamp_to_minutes_with_tz", + Instant.class, + String.class, + (Instant ts, String tz) -> (long) newLocalDateTime(ts, tz).getMinute()); + } else { + return CelFunctionBinding.from( + "timestamp_to_minutes_with_tz", + Timestamp.class, + String.class, + (Timestamp ts, String tz) -> (long) newLocalDateTime(ts, tz).getMinute()); + } + }), + DURATION_TO_MINUTES( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "duration_to_minutes", java.time.Duration.class, java.time.Duration::toMinutes); + } else { + return CelFunctionBinding.from( + "duration_to_minutes", Duration.class, ProtoTimeUtils::toMinutes); + } + }), + ; + + private final CelStandardOverload standardOverload; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return standardOverload.newFunctionBinding(celOptions, runtimeEquality); + } + + GetMinutesOverload(CelStandardOverload standardOverload) { + this.standardOverload = standardOverload; + } + } + + private GetMinutesFunction(ImmutableSet overloads) { + super("getMinutes", overloads); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/standard/GetMonthFunction.java b/runtime/src/main/java/dev/cel/runtime/standard/GetMonthFunction.java new file mode 100644 index 000000000..a8a9ded68 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/GetMonthFunction.java @@ -0,0 +1,94 @@ +// 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.standard; + +import static dev.cel.common.internal.DateTimeHelpers.UTC; +import static dev.cel.common.internal.DateTimeHelpers.newLocalDateTime; + +import com.google.common.collect.ImmutableSet; +import com.google.protobuf.Timestamp; +import dev.cel.common.CelOptions; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.RuntimeEquality; +import java.time.Instant; +import java.util.Arrays; + +/** Standard function runtime definition for {@code getMonth}. */ +public final class GetMonthFunction extends CelStandardFunction { + private static final GetMonthFunction ALL_OVERLOADS = create(GetMonthOverload.values()); + + public static GetMonthFunction create() { + return ALL_OVERLOADS; + } + + public static GetMonthFunction create(GetMonthFunction.GetMonthOverload... overloads) { + return create(Arrays.asList(overloads)); + } + + public static GetMonthFunction create(Iterable overloads) { + return new GetMonthFunction(ImmutableSet.copyOf(overloads)); + } + + /** Overloads for the standard function. */ + public enum GetMonthOverload implements CelStandardOverload { + TIMESTAMP_TO_MONTH( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "timestamp_to_month", + Instant.class, + (Instant ts) -> (long) newLocalDateTime(ts, UTC).getMonthValue() - 1); + } else { + return CelFunctionBinding.from( + "timestamp_to_month", + Timestamp.class, + (Timestamp ts) -> (long) newLocalDateTime(ts, UTC).getMonthValue() - 1); + } + }), + TIMESTAMP_TO_MONTH_WITH_TZ( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "timestamp_to_month_with_tz", + Instant.class, + String.class, + (Instant ts, String tz) -> (long) newLocalDateTime(ts, tz).getMonthValue() - 1); + } else { + return CelFunctionBinding.from( + "timestamp_to_month_with_tz", + Timestamp.class, + String.class, + (Timestamp ts, String tz) -> (long) newLocalDateTime(ts, tz).getMonthValue() - 1); + } + }), + ; + + private final CelStandardOverload standardOverload; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return standardOverload.newFunctionBinding(celOptions, runtimeEquality); + } + + GetMonthOverload(CelStandardOverload standardOverload) { + this.standardOverload = standardOverload; + } + } + + private GetMonthFunction(ImmutableSet overloads) { + super("getMonth", overloads); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/standard/GetSecondsFunction.java b/runtime/src/main/java/dev/cel/runtime/standard/GetSecondsFunction.java new file mode 100644 index 000000000..80030f50f --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/GetSecondsFunction.java @@ -0,0 +1,119 @@ +// 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.standard; + +import static dev.cel.common.internal.DateTimeHelpers.UTC; +import static dev.cel.common.internal.DateTimeHelpers.newLocalDateTime; + +import com.google.common.collect.ImmutableSet; +import com.google.protobuf.Duration; +import com.google.protobuf.Timestamp; +import dev.cel.common.CelOptions; +import dev.cel.common.internal.ProtoTimeUtils; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.RuntimeEquality; +import java.time.Instant; +import java.util.Arrays; + +/** Standard function for {@code getSeconds}. */ +public final class GetSecondsFunction extends CelStandardFunction { + + private static final GetSecondsFunction ALL_OVERLOADS = create(GetSecondsOverload.values()); + + public static GetSecondsFunction create() { + return ALL_OVERLOADS; + } + + public static GetSecondsFunction create(GetSecondsFunction.GetSecondsOverload... overloads) { + return create(Arrays.asList(overloads)); + } + + public static GetSecondsFunction create( + Iterable overloads) { + return new GetSecondsFunction(ImmutableSet.copyOf(overloads)); + } + + /** Overloads for the standard function. */ + public enum GetSecondsOverload implements CelStandardOverload { + TIMESTAMP_TO_SECONDS( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "timestamp_to_seconds", + Instant.class, + (Instant ts) -> (long) newLocalDateTime(ts, UTC).getSecond()); + } else { + return CelFunctionBinding.from( + "timestamp_to_seconds", + Timestamp.class, + (Timestamp ts) -> (long) newLocalDateTime(ts, UTC).getSecond()); + } + }), + TIMESTAMP_TO_SECONDS_WITH_TZ( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "timestamp_to_seconds_with_tz", + Instant.class, + String.class, + (Instant ts, String tz) -> (long) newLocalDateTime(ts, tz).getSecond()); + } else { + return CelFunctionBinding.from( + "timestamp_to_seconds_with_tz", + Timestamp.class, + String.class, + (Timestamp ts, String tz) -> (long) newLocalDateTime(ts, tz).getSecond()); + } + }), + DURATION_TO_SECONDS( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "duration_to_seconds", + java.time.Duration.class, + dur -> { + long truncatedSeconds = dur.getSeconds(); + // Preserve the existing behavior from protobuf where seconds is truncated towards + // 0 when negative. + if (dur.isNegative() && dur.getNano() > 0) { + truncatedSeconds++; + } + + return truncatedSeconds; + }); + } else { + return CelFunctionBinding.from( + "duration_to_seconds", Duration.class, ProtoTimeUtils::toSeconds); + } + }), + ; + + private final CelStandardOverload standardOverload; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return standardOverload.newFunctionBinding(celOptions, runtimeEquality); + } + + GetSecondsOverload(CelStandardOverload standardOverload) { + this.standardOverload = standardOverload; + } + } + + private GetSecondsFunction(ImmutableSet overloads) { + super("getSeconds", overloads); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/standard/GreaterEqualsOperator.java b/runtime/src/main/java/dev/cel/runtime/standard/GreaterEqualsOperator.java new file mode 100644 index 000000000..c04b4398f --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/GreaterEqualsOperator.java @@ -0,0 +1,204 @@ +// 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.standard; + +import static dev.cel.common.Operator.GREATER_EQUALS; + +import com.google.common.collect.ImmutableSet; +import com.google.common.primitives.UnsignedLong; +import com.google.protobuf.ByteString; +import com.google.protobuf.Duration; +import com.google.protobuf.Timestamp; +import dev.cel.common.CelOptions; +import dev.cel.common.internal.ComparisonFunctions; +import dev.cel.common.internal.ProtoTimeUtils; +import dev.cel.common.values.CelByteString; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.RuntimeEquality; +import dev.cel.runtime.RuntimeHelpers; +import java.time.Instant; +import java.util.Arrays; + +/** Standard function for the greater equals (>=) operator. */ +public final class GreaterEqualsOperator extends CelStandardFunction { + private static final GreaterEqualsOperator ALL_OVERLOADS = create(GreaterEqualsOverload.values()); + + public static GreaterEqualsOperator create() { + return ALL_OVERLOADS; + } + + public static GreaterEqualsOperator create( + GreaterEqualsOperator.GreaterEqualsOverload... overloads) { + return create(Arrays.asList(overloads)); + } + + public static GreaterEqualsOperator create( + Iterable overloads) { + return new GreaterEqualsOperator(ImmutableSet.copyOf(overloads)); + } + + /** Overloads for the standard function. */ + public enum GreaterEqualsOverload implements CelStandardOverload { + GREATER_EQUALS_BOOL( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "greater_equals_bool", + Boolean.class, + Boolean.class, + (Boolean x, Boolean y) -> x || !y)), + GREATER_EQUALS_BYTES( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "greater_equals_bytes", + CelByteString.class, + CelByteString.class, + (CelByteString x, CelByteString y) -> + CelByteString.unsignedLexicographicalComparator().compare(x, y) >= 0); + } else { + return CelFunctionBinding.from( + "greater_equals_bytes", + ByteString.class, + ByteString.class, + (ByteString x, ByteString y) -> + ByteString.unsignedLexicographicalComparator().compare(x, y) >= 0); + } + }), + GREATER_EQUALS_DOUBLE( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "greater_equals_double", + Double.class, + Double.class, + (Double x, Double y) -> x >= y)), + GREATER_EQUALS_DURATION( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "greater_equals_duration", + java.time.Duration.class, + java.time.Duration.class, + (java.time.Duration d1, java.time.Duration d2) -> d1.compareTo(d2) >= 0); + } else { + return CelFunctionBinding.from( + "greater_equals_duration", + Duration.class, + Duration.class, + (Duration d1, Duration d2) -> ProtoTimeUtils.compare(d1, d2) >= 0); + } + }), + GREATER_EQUALS_INT64( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "greater_equals_int64", Long.class, Long.class, (Long x, Long y) -> x >= y)), + GREATER_EQUALS_STRING( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "greater_equals_string", + String.class, + String.class, + (String x, String y) -> x.compareTo(y) >= 0)), + GREATER_EQUALS_TIMESTAMP( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "greater_equals_timestamp", + Instant.class, + Instant.class, + (Instant i1, Instant i2) -> i1.compareTo(i2) >= 0); + } else { + return CelFunctionBinding.from( + "greater_equals_timestamp", + Timestamp.class, + Timestamp.class, + (Timestamp t1, Timestamp t2) -> ProtoTimeUtils.compare(t1, t2) >= 0); + } + }), + GREATER_EQUALS_UINT64( + (celOptions, runtimeEquality) -> { + if (celOptions.enableUnsignedLongs()) { + return CelFunctionBinding.from( + "greater_equals_uint64", + UnsignedLong.class, + UnsignedLong.class, + (UnsignedLong x, UnsignedLong y) -> RuntimeHelpers.uint64CompareTo(x, y) >= 0); + } else { + return CelFunctionBinding.from( + "greater_equals_uint64", + Long.class, + Long.class, + (Long x, Long y) -> RuntimeHelpers.uint64CompareTo(x, y, celOptions) >= 0); + } + }), + GREATER_EQUALS_INT64_UINT64( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "greater_equals_int64_uint64", + Long.class, + UnsignedLong.class, + (Long x, UnsignedLong y) -> ComparisonFunctions.compareIntUint(x, y) >= 0)), + GREATER_EQUALS_UINT64_INT64( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "greater_equals_uint64_int64", + UnsignedLong.class, + Long.class, + (UnsignedLong x, Long y) -> ComparisonFunctions.compareUintInt(x, y) >= 0)), + GREATER_EQUALS_INT64_DOUBLE( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "greater_equals_int64_double", + Long.class, + Double.class, + (Long x, Double y) -> ComparisonFunctions.compareIntDouble(x, y) >= 0)), + GREATER_EQUALS_DOUBLE_INT64( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "greater_equals_double_int64", + Double.class, + Long.class, + (Double x, Long y) -> ComparisonFunctions.compareDoubleInt(x, y) >= 0)), + GREATER_EQUALS_UINT64_DOUBLE( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "greater_equals_uint64_double", + UnsignedLong.class, + Double.class, + (UnsignedLong x, Double y) -> ComparisonFunctions.compareUintDouble(x, y) >= 0)), + GREATER_EQUALS_DOUBLE_UINT64( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "greater_equals_double_uint64", + Double.class, + UnsignedLong.class, + (Double x, UnsignedLong y) -> ComparisonFunctions.compareDoubleUint(x, y) >= 0)); + + private final CelStandardOverload standardOverload; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return standardOverload.newFunctionBinding(celOptions, runtimeEquality); + } + + GreaterEqualsOverload(CelStandardOverload standardOverload) { + this.standardOverload = standardOverload; + } + } + + private GreaterEqualsOperator(ImmutableSet overloads) { + super(GREATER_EQUALS.getFunction(), overloads); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/standard/GreaterOperator.java b/runtime/src/main/java/dev/cel/runtime/standard/GreaterOperator.java new file mode 100644 index 000000000..80edb8a9b --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/GreaterOperator.java @@ -0,0 +1,197 @@ +// 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.standard; + +import static dev.cel.common.Operator.GREATER; + +import com.google.common.collect.ImmutableSet; +import com.google.common.primitives.UnsignedLong; +import com.google.protobuf.ByteString; +import com.google.protobuf.Duration; +import com.google.protobuf.Timestamp; +import dev.cel.common.CelOptions; +import dev.cel.common.internal.ComparisonFunctions; +import dev.cel.common.internal.ProtoTimeUtils; +import dev.cel.common.values.CelByteString; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.RuntimeEquality; +import dev.cel.runtime.RuntimeHelpers; +import java.time.Instant; +import java.util.Arrays; + +/** Standard function for the greater (>) operator. */ +public final class GreaterOperator extends CelStandardFunction { + private static final GreaterOperator ALL_OVERLOADS = create(GreaterOverload.values()); + + public static GreaterOperator create() { + return ALL_OVERLOADS; + } + + public static GreaterOperator create(GreaterOperator.GreaterOverload... overloads) { + return create(Arrays.asList(overloads)); + } + + public static GreaterOperator create(Iterable overloads) { + return new GreaterOperator(ImmutableSet.copyOf(overloads)); + } + + /** Overloads for the standard function. */ + public enum GreaterOverload implements CelStandardOverload { + GREATER_BOOL( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "greater_bool", Boolean.class, Boolean.class, (Boolean x, Boolean y) -> x && !y)), + GREATER_BYTES( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "greater_bytes", + CelByteString.class, + CelByteString.class, + (CelByteString x, CelByteString y) -> + CelByteString.unsignedLexicographicalComparator().compare(x, y) > 0); + } else { + return CelFunctionBinding.from( + "greater_bytes", + ByteString.class, + ByteString.class, + (ByteString x, ByteString y) -> + ByteString.unsignedLexicographicalComparator().compare(x, y) > 0); + } + }), + GREATER_DOUBLE( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "greater_double", Double.class, Double.class, (Double x, Double y) -> x > y)), + GREATER_DURATION( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "greater_duration", + java.time.Duration.class, + java.time.Duration.class, + (java.time.Duration d1, java.time.Duration d2) -> d1.compareTo(d2) > 0); + } else { + return CelFunctionBinding.from( + "greater_duration", + Duration.class, + Duration.class, + (Duration d1, Duration d2) -> ProtoTimeUtils.compare(d1, d2) > 0); + } + }), + GREATER_INT64( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "greater_int64", Long.class, Long.class, (Long x, Long y) -> x > y)), + GREATER_STRING( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "greater_string", + String.class, + String.class, + (String x, String y) -> x.compareTo(y) > 0)), + GREATER_TIMESTAMP( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "greater_timestamp", + Instant.class, + Instant.class, + (Instant i1, Instant i2) -> i1.isAfter(i2)); + } else { + return CelFunctionBinding.from( + "greater_timestamp", + Timestamp.class, + Timestamp.class, + (Timestamp t1, Timestamp t2) -> ProtoTimeUtils.compare(t1, t2) > 0); + } + }), + GREATER_UINT64( + (celOptions, runtimeEquality) -> { + if (celOptions.enableUnsignedLongs()) { + return CelFunctionBinding.from( + "greater_uint64", + UnsignedLong.class, + UnsignedLong.class, + (UnsignedLong x, UnsignedLong y) -> RuntimeHelpers.uint64CompareTo(x, y) > 0); + } else { + return CelFunctionBinding.from( + "greater_uint64", + Long.class, + Long.class, + (Long x, Long y) -> RuntimeHelpers.uint64CompareTo(x, y, celOptions) > 0); + } + }), + GREATER_INT64_UINT64( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "greater_int64_uint64", + Long.class, + UnsignedLong.class, + (Long x, UnsignedLong y) -> ComparisonFunctions.compareIntUint(x, y) == 1)), + GREATER_UINT64_INT64( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "greater_uint64_int64", + UnsignedLong.class, + Long.class, + (UnsignedLong x, Long y) -> ComparisonFunctions.compareUintInt(x, y) == 1)), + GREATER_INT64_DOUBLE( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "greater_int64_double", + Long.class, + Double.class, + (Long x, Double y) -> ComparisonFunctions.compareIntDouble(x, y) == 1)), + GREATER_DOUBLE_INT64( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "greater_double_int64", + Double.class, + Long.class, + (Double x, Long y) -> ComparisonFunctions.compareDoubleInt(x, y) == 1)), + GREATER_UINT64_DOUBLE( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "greater_uint64_double", + UnsignedLong.class, + Double.class, + (UnsignedLong x, Double y) -> ComparisonFunctions.compareUintDouble(x, y) == 1)), + GREATER_DOUBLE_UINT64( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "greater_double_uint64", + Double.class, + UnsignedLong.class, + (Double x, UnsignedLong y) -> ComparisonFunctions.compareDoubleUint(x, y) == 1)), + ; + + private final CelStandardOverload standardOverload; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return standardOverload.newFunctionBinding(celOptions, runtimeEquality); + } + + GreaterOverload(CelStandardOverload standardOverload) { + this.standardOverload = standardOverload; + } + } + + private GreaterOperator(ImmutableSet overloads) { + super(GREATER.getFunction(), overloads); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/standard/InOperator.java b/runtime/src/main/java/dev/cel/runtime/standard/InOperator.java new file mode 100644 index 000000000..bf80cc6ff --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/InOperator.java @@ -0,0 +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.standard; + +import static dev.cel.common.Operator.IN; + +import com.google.common.collect.ImmutableSet; +import dev.cel.common.CelOptions; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.RuntimeEquality; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** Standard function for the ('in') operator. */ +@SuppressWarnings({"unchecked", "rawtypes"}) +public final class InOperator extends CelStandardFunction { + private static final InOperator ALL_OVERLOADS = create(InOverload.values()); + + public static InOperator create() { + return ALL_OVERLOADS; + } + + public static InOperator create(InOperator.InOverload... overloads) { + return create(Arrays.asList(overloads)); + } + + public static InOperator create(Iterable overloads) { + return new InOperator(ImmutableSet.copyOf(overloads)); + } + + /** Overloads for the standard function. */ + public enum InOverload implements CelStandardOverload { + IN_LIST( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "in_list", + Object.class, + List.class, + (Object value, List list) -> runtimeEquality.inList(list, value))), + IN_MAP( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "in_map", + Object.class, + Map.class, + (Object key, Map map) -> runtimeEquality.inMap(map, key))); + + private final CelStandardOverload standardOverload; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return standardOverload.newFunctionBinding(celOptions, runtimeEquality); + } + + InOverload(CelStandardOverload standardOverload) { + this.standardOverload = standardOverload; + } + } + + private InOperator(ImmutableSet overloads) { + super(IN.getFunction(), overloads); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/standard/IndexOperator.java b/runtime/src/main/java/dev/cel/runtime/standard/IndexOperator.java new file mode 100644 index 000000000..f48e8fbf5 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/IndexOperator.java @@ -0,0 +1,72 @@ +// 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.standard; + +import static dev.cel.common.Operator.INDEX; + +import com.google.common.collect.ImmutableSet; +import dev.cel.common.CelOptions; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.RuntimeEquality; +import dev.cel.runtime.RuntimeHelpers; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** Standard function for the indexing ({@code list[0] or map['foo']}) operator */ +public final class IndexOperator extends CelStandardFunction { + private static final IndexOperator ALL_OVERLOADS = create(IndexOverload.values()); + + public static IndexOperator create() { + return ALL_OVERLOADS; + } + + public static IndexOperator create(IndexOperator.IndexOverload... overloads) { + return create(Arrays.asList(overloads)); + } + + public static IndexOperator create(Iterable overloads) { + return new IndexOperator(ImmutableSet.copyOf(overloads)); + } + + /** Overloads for the standard function. */ + @SuppressWarnings({"unchecked"}) + public enum IndexOverload implements CelStandardOverload { + INDEX_LIST( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "index_list", List.class, Number.class, RuntimeHelpers::indexList)), + INDEX_MAP( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "index_map", Map.class, Object.class, runtimeEquality::indexMap)); + + private final CelStandardOverload standardOverload; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return standardOverload.newFunctionBinding(celOptions, runtimeEquality); + } + + IndexOverload(CelStandardOverload standardOverload) { + this.standardOverload = standardOverload; + } + } + + private IndexOperator(ImmutableSet overloads) { + super(INDEX.getFunction(), overloads); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/standard/IntFunction.java b/runtime/src/main/java/dev/cel/runtime/standard/IntFunction.java new file mode 100644 index 000000000..63959af87 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/IntFunction.java @@ -0,0 +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.standard; + +import com.google.common.collect.ImmutableSet; +import com.google.common.primitives.UnsignedLong; +import com.google.protobuf.Timestamp; +import dev.cel.common.CelOptions; +import dev.cel.common.exceptions.CelBadFormatException; +import dev.cel.common.exceptions.CelNumericOverflowException; +import dev.cel.common.internal.ProtoTimeUtils; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.RuntimeEquality; +import dev.cel.runtime.RuntimeHelpers; +import java.time.Instant; +import java.util.Arrays; + +/** Standard function for {@code int} conversion function. */ +public final class IntFunction extends CelStandardFunction { + private static final IntFunction ALL_OVERLOADS = create(IntOverload.values()); + + public static IntFunction create() { + return ALL_OVERLOADS; + } + + public static IntFunction create(IntFunction.IntOverload... overloads) { + return create(Arrays.asList(overloads)); + } + + public static IntFunction create(Iterable overloads) { + return new IntFunction(ImmutableSet.copyOf(overloads)); + } + + /** Overloads for the standard function. */ + public enum IntOverload implements CelStandardOverload { + INT64_TO_INT64( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from("int64_to_int64", Long.class, (Long x) -> x)), + UINT64_TO_INT64( + (celOptions, runtimeEquality) -> { + if (celOptions.enableUnsignedLongs()) { + return CelFunctionBinding.from( + "uint64_to_int64", + UnsignedLong.class, + (UnsignedLong arg) -> { + if (arg.compareTo(UnsignedLong.valueOf(Long.MAX_VALUE)) > 0) { + throw new CelNumericOverflowException("unsigned out of int range"); + } + return arg.longValue(); + }); + } else { + return CelFunctionBinding.from( + "uint64_to_int64", + Long.class, + (Long arg) -> { + if (celOptions.errorOnIntWrap() && arg < 0) { + throw new CelNumericOverflowException("unsigned out of int range"); + } + return arg; + }); + } + }), + DOUBLE_TO_INT64( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "double_to_int64", + Double.class, + (Double arg) -> { + if (celOptions.errorOnIntWrap()) { + return RuntimeHelpers.doubleToLongChecked(arg) + .orElseThrow( + () -> + new CelNumericOverflowException("double is out of range for int")); + } + return arg.longValue(); + })), + STRING_TO_INT64( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "string_to_int64", + String.class, + (String arg) -> { + try { + return Long.parseLong(arg); + } catch (NumberFormatException e) { + throw new CelBadFormatException(e); + } + })), + TIMESTAMP_TO_INT64( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "timestamp_to_int64", Instant.class, Instant::getEpochSecond); + } else { + return CelFunctionBinding.from( + "timestamp_to_int64", Timestamp.class, ProtoTimeUtils::toSeconds); + } + }), + ; + + private final CelStandardOverload standardOverload; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return standardOverload.newFunctionBinding(celOptions, runtimeEquality); + } + + IntOverload(CelStandardOverload standardOverload) { + this.standardOverload = standardOverload; + } + } + + private IntFunction(ImmutableSet overloads) { + super("int", overloads); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/standard/LessEqualsOperator.java b/runtime/src/main/java/dev/cel/runtime/standard/LessEqualsOperator.java new file mode 100644 index 000000000..7688acbc1 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/LessEqualsOperator.java @@ -0,0 +1,201 @@ +// 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.standard; + +import static dev.cel.common.Operator.LESS_EQUALS; + +import com.google.common.collect.ImmutableSet; +import com.google.common.primitives.UnsignedLong; +import com.google.protobuf.ByteString; +import com.google.protobuf.Duration; +import com.google.protobuf.Timestamp; +import dev.cel.common.CelOptions; +import dev.cel.common.internal.ComparisonFunctions; +import dev.cel.common.internal.ProtoTimeUtils; +import dev.cel.common.values.CelByteString; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.RuntimeEquality; +import dev.cel.runtime.RuntimeHelpers; +import java.time.Instant; +import java.util.Arrays; + +/** Standard function for the less equals (<=) operator. */ +public final class LessEqualsOperator extends CelStandardFunction { + private static final LessEqualsOperator ALL_OVERLOADS = create(LessEqualsOverload.values()); + + public static LessEqualsOperator create() { + return ALL_OVERLOADS; + } + + public static LessEqualsOperator create(LessEqualsOperator.LessEqualsOverload... overloads) { + return create(Arrays.asList(overloads)); + } + + public static LessEqualsOperator create( + Iterable overloads) { + return new LessEqualsOperator(ImmutableSet.copyOf(overloads)); + } + + /** Overloads for the standard function. */ + public enum LessEqualsOverload implements CelStandardOverload { + LESS_EQUALS_BOOL( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "less_equals_bool", + Boolean.class, + Boolean.class, + (Boolean x, Boolean y) -> !x || y)), + LESS_EQUALS_BYTES( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "less_equals_bytes", + CelByteString.class, + CelByteString.class, + (CelByteString x, CelByteString y) -> + CelByteString.unsignedLexicographicalComparator().compare(x, y) <= 0); + } else { + return CelFunctionBinding.from( + "less_equals_bytes", + ByteString.class, + ByteString.class, + (ByteString x, ByteString y) -> + ByteString.unsignedLexicographicalComparator().compare(x, y) <= 0); + } + }), + LESS_EQUALS_DOUBLE( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "less_equals_double", Double.class, Double.class, (Double x, Double y) -> x <= y)), + LESS_EQUALS_DURATION( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "less_equals_duration", + java.time.Duration.class, + java.time.Duration.class, + (java.time.Duration d1, java.time.Duration d2) -> d1.compareTo(d2) <= 0); + } else { + return CelFunctionBinding.from( + "less_equals_duration", + Duration.class, + Duration.class, + (Duration d1, Duration d2) -> ProtoTimeUtils.compare(d1, d2) <= 0); + } + }), + LESS_EQUALS_INT64( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "less_equals_int64", Long.class, Long.class, (Long x, Long y) -> x <= y)), + LESS_EQUALS_STRING( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "less_equals_string", + String.class, + String.class, + (String x, String y) -> x.compareTo(y) <= 0)), + LESS_EQUALS_TIMESTAMP( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "less_equals_timestamp", + Instant.class, + Instant.class, + (Instant i1, Instant i2) -> i1.compareTo(i2) <= 0); + } else { + return CelFunctionBinding.from( + "less_equals_timestamp", + Timestamp.class, + Timestamp.class, + (Timestamp t1, Timestamp t2) -> ProtoTimeUtils.compare(t1, t2) <= 0); + } + }), + LESS_EQUALS_UINT64( + (celOptions, runtimeEquality) -> { + if (celOptions.enableUnsignedLongs()) { + return CelFunctionBinding.from( + "less_equals_uint64", + UnsignedLong.class, + UnsignedLong.class, + (UnsignedLong x, UnsignedLong y) -> RuntimeHelpers.uint64CompareTo(x, y) <= 0); + } else { + return CelFunctionBinding.from( + "less_equals_uint64", + Long.class, + Long.class, + (Long x, Long y) -> RuntimeHelpers.uint64CompareTo(x, y, celOptions) <= 0); + } + }), + LESS_EQUALS_INT64_UINT64( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "less_equals_int64_uint64", + Long.class, + UnsignedLong.class, + (Long x, UnsignedLong y) -> ComparisonFunctions.compareIntUint(x, y) <= 0)), + LESS_EQUALS_UINT64_INT64( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "less_equals_uint64_int64", + UnsignedLong.class, + Long.class, + (UnsignedLong x, Long y) -> ComparisonFunctions.compareUintInt(x, y) <= 0)), + LESS_EQUALS_INT64_DOUBLE( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "less_equals_int64_double", + Long.class, + Double.class, + (Long x, Double y) -> ComparisonFunctions.compareIntDouble(x, y) <= 0)), + LESS_EQUALS_DOUBLE_INT64( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "less_equals_double_int64", + Double.class, + Long.class, + (Double x, Long y) -> ComparisonFunctions.compareDoubleInt(x, y) <= 0)), + LESS_EQUALS_UINT64_DOUBLE( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "less_equals_uint64_double", + UnsignedLong.class, + Double.class, + (UnsignedLong x, Double y) -> ComparisonFunctions.compareUintDouble(x, y) <= 0)), + LESS_EQUALS_DOUBLE_UINT64( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "less_equals_double_uint64", + Double.class, + UnsignedLong.class, + (Double x, UnsignedLong y) -> ComparisonFunctions.compareDoubleUint(x, y) <= 0)), + ; + + private final CelStandardOverload standardOverload; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return standardOverload.newFunctionBinding(celOptions, runtimeEquality); + } + + LessEqualsOverload(CelStandardOverload standardOverload) { + this.standardOverload = standardOverload; + } + } + + private LessEqualsOperator(ImmutableSet overloads) { + super(LESS_EQUALS.getFunction(), overloads); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/standard/LessOperator.java b/runtime/src/main/java/dev/cel/runtime/standard/LessOperator.java new file mode 100644 index 000000000..a53a13a92 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/LessOperator.java @@ -0,0 +1,197 @@ +// 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.standard; + +import static dev.cel.common.Operator.LESS; + +import com.google.common.collect.ImmutableSet; +import com.google.common.primitives.UnsignedLong; +import com.google.protobuf.ByteString; +import com.google.protobuf.Duration; +import com.google.protobuf.Timestamp; +import dev.cel.common.CelOptions; +import dev.cel.common.internal.ComparisonFunctions; +import dev.cel.common.internal.ProtoTimeUtils; +import dev.cel.common.values.CelByteString; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.RuntimeEquality; +import dev.cel.runtime.RuntimeHelpers; +import java.time.Instant; +import java.util.Arrays; + +/** Standard function for the less (<) operator. */ +public final class LessOperator extends CelStandardFunction { + private static final LessOperator ALL_OVERLOADS = create(LessOverload.values()); + + public static LessOperator create() { + return ALL_OVERLOADS; + } + + public static LessOperator create(LessOperator.LessOverload... overloads) { + return create(Arrays.asList(overloads)); + } + + public static LessOperator create(Iterable overloads) { + return new LessOperator(ImmutableSet.copyOf(overloads)); + } + + /** Overloads for the standard function. */ + public enum LessOverload implements CelStandardOverload { + LESS_BOOL( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "less_bool", Boolean.class, Boolean.class, (Boolean x, Boolean y) -> !x && y)), + LESS_INT64( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "less_int64", Long.class, Long.class, (Long x, Long y) -> x < y)), + LESS_UINT64( + (celOptions, runtimeEquality) -> { + if (celOptions.enableUnsignedLongs()) { + return CelFunctionBinding.from( + "less_uint64", + UnsignedLong.class, + UnsignedLong.class, + (UnsignedLong x, UnsignedLong y) -> RuntimeHelpers.uint64CompareTo(x, y) < 0); + } else { + return CelFunctionBinding.from( + "less_uint64", + Long.class, + Long.class, + (Long x, Long y) -> RuntimeHelpers.uint64CompareTo(x, y, celOptions) < 0); + } + }), + LESS_BYTES( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "less_bytes", + CelByteString.class, + CelByteString.class, + (CelByteString x, CelByteString y) -> + CelByteString.unsignedLexicographicalComparator().compare(x, y) < 0); + } else { + return CelFunctionBinding.from( + "less_bytes", + ByteString.class, + ByteString.class, + (ByteString x, ByteString y) -> + ByteString.unsignedLexicographicalComparator().compare(x, y) < 0); + } + }), + LESS_DOUBLE( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "less_double", Double.class, Double.class, (Double x, Double y) -> x < y)), + LESS_DOUBLE_UINT64( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "less_double_uint64", + Double.class, + UnsignedLong.class, + (Double x, UnsignedLong y) -> ComparisonFunctions.compareDoubleUint(x, y) == -1)), + LESS_INT64_UINT64( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "less_int64_uint64", + Long.class, + UnsignedLong.class, + (Long x, UnsignedLong y) -> ComparisonFunctions.compareIntUint(x, y) == -1)), + LESS_UINT64_INT64( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "less_uint64_int64", + UnsignedLong.class, + Long.class, + (UnsignedLong x, Long y) -> ComparisonFunctions.compareUintInt(x, y) == -1)), + LESS_INT64_DOUBLE( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "less_int64_double", + Long.class, + Double.class, + (Long x, Double y) -> ComparisonFunctions.compareIntDouble(x, y) == -1)), + LESS_DOUBLE_INT64( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "less_double_int64", + Double.class, + Long.class, + (Double x, Long y) -> ComparisonFunctions.compareDoubleInt(x, y) == -1)), + LESS_UINT64_DOUBLE( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "less_uint64_double", + UnsignedLong.class, + Double.class, + (UnsignedLong x, Double y) -> ComparisonFunctions.compareUintDouble(x, y) == -1)), + LESS_DURATION( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "less_duration", + java.time.Duration.class, + java.time.Duration.class, + (java.time.Duration d1, java.time.Duration d2) -> d1.compareTo(d2) < 0); + } else { + return CelFunctionBinding.from( + "less_duration", + Duration.class, + Duration.class, + (Duration d1, Duration d2) -> ProtoTimeUtils.compare(d1, d2) < 0); + } + }), + LESS_STRING( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "less_string", + String.class, + String.class, + (String x, String y) -> x.compareTo(y) < 0)), + LESS_TIMESTAMP( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "less_timestamp", + Instant.class, + Instant.class, + (Instant i1, Instant i2) -> i1.isBefore(i2)); + } else { + return CelFunctionBinding.from( + "less_timestamp", + Timestamp.class, + Timestamp.class, + (Timestamp t1, Timestamp t2) -> ProtoTimeUtils.compare(t1, t2) < 0); + } + }), + ; + + private final CelStandardOverload standardOverload; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return standardOverload.newFunctionBinding(celOptions, runtimeEquality); + } + + LessOverload(CelStandardOverload standardOverload) { + this.standardOverload = standardOverload; + } + } + + private LessOperator(ImmutableSet overloads) { + super(LESS.getFunction(), overloads); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/standard/LogicalNotOperator.java b/runtime/src/main/java/dev/cel/runtime/standard/LogicalNotOperator.java new file mode 100644 index 000000000..6c2ec8efd --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/LogicalNotOperator.java @@ -0,0 +1,64 @@ +// 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.standard; + +import static dev.cel.common.Operator.LOGICAL_NOT; + +import com.google.common.collect.ImmutableSet; +import dev.cel.common.CelOptions; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.RuntimeEquality; +import java.util.Arrays; + +/** Standard function for the logical not (!=) operator. */ +public final class LogicalNotOperator extends CelStandardFunction { + private static final LogicalNotOperator ALL_OVERLOADS = create(LogicalNotOverload.values()); + + public static LogicalNotOperator create() { + return ALL_OVERLOADS; + } + + public static LogicalNotOperator create(LogicalNotOperator.LogicalNotOverload... overloads) { + return create(Arrays.asList(overloads)); + } + + public static LogicalNotOperator create( + Iterable overloads) { + return new LogicalNotOperator(ImmutableSet.copyOf(overloads)); + } + + /** Overloads for the standard function. */ + public enum LogicalNotOverload implements CelStandardOverload { + LOGICAL_NOT( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from("logical_not", Boolean.class, (Boolean x) -> !x)); + + private final CelStandardOverload standardOverload; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return standardOverload.newFunctionBinding(celOptions, runtimeEquality); + } + + LogicalNotOverload(CelStandardOverload standardOverload) { + this.standardOverload = standardOverload; + } + } + + private LogicalNotOperator(ImmutableSet overloads) { + super(LOGICAL_NOT.getFunction(), overloads); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/standard/MatchesFunction.java b/runtime/src/main/java/dev/cel/runtime/standard/MatchesFunction.java new file mode 100644 index 000000000..a82a7d439 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/MatchesFunction.java @@ -0,0 +1,88 @@ +// 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.standard; + +import com.google.common.collect.ImmutableSet; +import dev.cel.common.CelOptions; +import dev.cel.common.exceptions.CelInvalidArgumentException; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.RuntimeEquality; +import dev.cel.runtime.RuntimeHelpers; +import java.util.Arrays; + +/** Standard function for {@code matches}. */ +public final class MatchesFunction extends CelStandardFunction { + private static final MatchesFunction ALL_OVERLOADS = create(MatchesOverload.values()); + + public static MatchesFunction create() { + return ALL_OVERLOADS; + } + + public static MatchesFunction create(MatchesFunction.MatchesOverload... overloads) { + return create(Arrays.asList(overloads)); + } + + public static MatchesFunction create(Iterable overloads) { + return new MatchesFunction(ImmutableSet.copyOf(overloads)); + } + + /** Overloads for the standard function. */ + public enum MatchesOverload implements CelStandardOverload { + MATCHES( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "matches", + String.class, + String.class, + (String string, String regexp) -> { + try { + return RuntimeHelpers.matches(string, regexp, celOptions); + } catch (RuntimeException e) { + throw new CelInvalidArgumentException(e); + } + })), + // Duplicate receiver-style matches overload. + MATCHES_STRING( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "matches_string", + String.class, + String.class, + (String string, String regexp) -> { + try { + return RuntimeHelpers.matches(string, regexp, celOptions); + } catch (RuntimeException e) { + throw new CelInvalidArgumentException(e); + } + })), + ; + + private final CelStandardOverload standardOverload; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return standardOverload.newFunctionBinding(celOptions, runtimeEquality); + } + + MatchesOverload(CelStandardOverload standardOverload) { + this.standardOverload = standardOverload; + } + } + + private MatchesFunction(ImmutableSet overloads) { + super("matches", overloads); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/standard/ModuloOperator.java b/runtime/src/main/java/dev/cel/runtime/standard/ModuloOperator.java new file mode 100644 index 000000000..baa74fe15 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/ModuloOperator.java @@ -0,0 +1,90 @@ +// 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.standard; + +import static dev.cel.common.Operator.MODULO; + +import com.google.common.collect.ImmutableSet; +import com.google.common.primitives.UnsignedLong; +import dev.cel.common.CelOptions; +import dev.cel.common.exceptions.CelDivideByZeroException; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.RuntimeEquality; +import dev.cel.runtime.RuntimeHelpers; +import java.util.Arrays; + +/** Standard function for the modulus (%) operator. */ +public final class ModuloOperator extends CelStandardFunction { + private static final ModuloOperator ALL_OVERLOADS = create(ModuloOverload.values()); + + public static ModuloOperator create() { + return ALL_OVERLOADS; + } + + public static ModuloOperator create(ModuloOperator.ModuloOverload... overloads) { + return create(Arrays.asList(overloads)); + } + + public static ModuloOperator create(Iterable overloads) { + return new ModuloOperator(ImmutableSet.copyOf(overloads)); + } + + /** Overloads for the standard function. */ + public enum ModuloOverload implements CelStandardOverload { + MODULO_INT64( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "modulo_int64", + Long.class, + Long.class, + (Long x, Long y) -> { + try { + return x % y; + } catch (ArithmeticException e) { + throw new CelDivideByZeroException(e); + } + })), + MODULO_UINT64( + (celOptions, runtimeEquality) -> { + if (celOptions.enableUnsignedLongs()) { + return CelFunctionBinding.from( + "modulo_uint64", UnsignedLong.class, UnsignedLong.class, RuntimeHelpers::uint64Mod); + } else { + return CelFunctionBinding.from( + "modulo_uint64", + Long.class, + Long.class, + (Long x, Long y) -> RuntimeHelpers.uint64Mod(x, y, celOptions)); + } + }), + ; + + private final CelStandardOverload standardOverload; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return standardOverload.newFunctionBinding(celOptions, runtimeEquality); + } + + ModuloOverload(CelStandardOverload standardOverload) { + this.standardOverload = standardOverload; + } + } + + private ModuloOperator(ImmutableSet overloads) { + super(MODULO.getFunction(), overloads); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/standard/MultiplyOperator.java b/runtime/src/main/java/dev/cel/runtime/standard/MultiplyOperator.java new file mode 100644 index 000000000..425723652 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/MultiplyOperator.java @@ -0,0 +1,108 @@ +// 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.standard; + +import static dev.cel.common.Operator.MULTIPLY; + +import com.google.common.collect.ImmutableSet; +import com.google.common.primitives.UnsignedLong; +import dev.cel.common.CelOptions; +import dev.cel.common.exceptions.CelNumericOverflowException; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.RuntimeEquality; +import dev.cel.runtime.RuntimeHelpers; +import java.util.Arrays; + +/** Standard function for the multiplication (*) operator. */ +public final class MultiplyOperator extends CelStandardFunction { + private static final MultiplyOperator ALL_OVERLOADS = create(MultiplyOverload.values()); + + public static MultiplyOperator create() { + return ALL_OVERLOADS; + } + + public static MultiplyOperator create(MultiplyOperator.MultiplyOverload... overloads) { + return create(Arrays.asList(overloads)); + } + + public static MultiplyOperator create(Iterable overloads) { + return new MultiplyOperator(ImmutableSet.copyOf(overloads)); + } + + /** Overloads for the standard function. */ + public enum MultiplyOverload implements CelStandardOverload { + MULTIPLY_INT64( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "multiply_int64", + Long.class, + Long.class, + (Long x, Long y) -> { + try { + return RuntimeHelpers.int64Multiply(x, y, celOptions); + } catch (ArithmeticException e) { + throw new CelNumericOverflowException(e); + } + })), + MULTIPLY_DOUBLE( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "multiply_double", Double.class, Double.class, (Double x, Double y) -> x * y)), + MULTIPLY_UINT64( + (celOptions, runtimeEquality) -> { + if (celOptions.enableUnsignedLongs()) { + return CelFunctionBinding.from( + "multiply_uint64", + UnsignedLong.class, + UnsignedLong.class, + (UnsignedLong x, UnsignedLong y) -> { + try { + return RuntimeHelpers.uint64Multiply(x, y); + } catch (ArithmeticException e) { + throw new CelNumericOverflowException(e); + } + }); + } else { + return CelFunctionBinding.from( + "multiply_uint64", + Long.class, + Long.class, + (Long x, Long y) -> { + try { + return RuntimeHelpers.uint64Multiply(x, y, celOptions); + } catch (ArithmeticException e) { + throw new CelNumericOverflowException(e); + } + }); + } + }); + + private final CelStandardOverload standardOverload; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return standardOverload.newFunctionBinding(celOptions, runtimeEquality); + } + + MultiplyOverload(CelStandardOverload standardOverload) { + this.standardOverload = standardOverload; + } + } + + private MultiplyOperator(ImmutableSet overloads) { + super(MULTIPLY.getFunction(), overloads); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/standard/NegateOperator.java b/runtime/src/main/java/dev/cel/runtime/standard/NegateOperator.java new file mode 100644 index 000000000..c61c395cb --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/NegateOperator.java @@ -0,0 +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.standard; + +import static dev.cel.common.Operator.NEGATE; + +import com.google.common.collect.ImmutableSet; +import dev.cel.common.CelOptions; +import dev.cel.common.exceptions.CelNumericOverflowException; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.RuntimeEquality; +import dev.cel.runtime.RuntimeHelpers; +import java.util.Arrays; + +/** Standard function for the negate (-) operator. */ +public final class NegateOperator extends CelStandardFunction { + private static final NegateOperator ALL_OVERLOADS = create(NegateOverload.values()); + + public static NegateOperator create() { + return ALL_OVERLOADS; + } + + public static NegateOperator create(NegateOperator.NegateOverload... overloads) { + return create(Arrays.asList(overloads)); + } + + public static NegateOperator create(Iterable overloads) { + return new NegateOperator(ImmutableSet.copyOf(overloads)); + } + + /** Overloads for the standard function. */ + public enum NegateOverload implements CelStandardOverload { + NEGATE_INT64( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "negate_int64", + Long.class, + (Long x) -> { + try { + return RuntimeHelpers.int64Negate(x, celOptions); + } catch (ArithmeticException e) { + throw new CelNumericOverflowException(e); + } + })), + NEGATE_DOUBLE( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from("negate_double", Double.class, (Double x) -> -x)); + + private final CelStandardOverload standardOverload; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return standardOverload.newFunctionBinding(celOptions, runtimeEquality); + } + + NegateOverload(CelStandardOverload standardOverload) { + this.standardOverload = standardOverload; + } + } + + private NegateOperator(ImmutableSet overloads) { + super(NEGATE.getFunction(), overloads); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/standard/NotEqualsOperator.java b/runtime/src/main/java/dev/cel/runtime/standard/NotEqualsOperator.java new file mode 100644 index 000000000..27b17676e --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/NotEqualsOperator.java @@ -0,0 +1,67 @@ +// 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.standard; + +import static dev.cel.common.Operator.NOT_EQUALS; + +import com.google.common.collect.ImmutableSet; +import dev.cel.common.CelOptions; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.RuntimeEquality; +import java.util.Arrays; + +/** Standard function for the not equals (!=) operator. */ +public final class NotEqualsOperator extends CelStandardFunction { + private static final NotEqualsOperator ALL_OVERLOADS = create(NotEqualsOverload.values()); + + public static NotEqualsOperator create() { + return ALL_OVERLOADS; + } + + public static NotEqualsOperator create(NotEqualsOperator.NotEqualsOverload... overloads) { + return create(Arrays.asList(overloads)); + } + + public static NotEqualsOperator create(Iterable overloads) { + return new NotEqualsOperator(ImmutableSet.copyOf(overloads)); + } + + /** Overloads for the standard function. */ + public enum NotEqualsOverload implements CelStandardOverload { + NOT_EQUALS( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "not_equals", + Object.class, + Object.class, + (Object x, Object y) -> !runtimeEquality.objectEquals(x, y))); + + private final CelStandardOverload standardOverload; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return standardOverload.newFunctionBinding(celOptions, runtimeEquality); + } + + NotEqualsOverload(CelStandardOverload standardOverload) { + this.standardOverload = standardOverload; + } + } + + private NotEqualsOperator(ImmutableSet overloads) { + super(NOT_EQUALS.getFunction(), overloads); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/standard/NotStrictlyFalseFunction.java b/runtime/src/main/java/dev/cel/runtime/standard/NotStrictlyFalseFunction.java new file mode 100644 index 000000000..8e0ceead4 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/NotStrictlyFalseFunction.java @@ -0,0 +1,70 @@ +// 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.standard; + +import static dev.cel.common.Operator.NOT_STRICTLY_FALSE; + +import com.google.common.collect.ImmutableSet; +import dev.cel.common.CelOptions; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.InternalFunctionBinder; +import dev.cel.runtime.RuntimeEquality; + +/** + * Standard function for {@code @not_strictly_false}. This is an internal function used within + * comprehensions to coerce the result into true if an evaluation yields an error or an unknown set. + */ +public final class NotStrictlyFalseFunction extends CelStandardFunction { + private static final NotStrictlyFalseFunction ALL_OVERLOADS = + new NotStrictlyFalseFunction(ImmutableSet.copyOf(NotStrictlyFalseOverload.values())); + + public static NotStrictlyFalseFunction create() { + return ALL_OVERLOADS; + } + + /** Overloads for the standard function. */ + public enum NotStrictlyFalseOverload implements CelStandardOverload { + NOT_STRICTLY_FALSE( + (celOptions, runtimeEquality) -> + InternalFunctionBinder.from( + "not_strictly_false", + Object.class, + (Object value) -> { + if (value instanceof Boolean) { + return value; + } + + return true; + }, + /* isStrict= */ false)), + ; + + private final CelStandardOverload bindingCreator; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.newFunctionBinding(celOptions, runtimeEquality); + } + + NotStrictlyFalseOverload(CelStandardOverload bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + private NotStrictlyFalseFunction(ImmutableSet overloads) { + super(NOT_STRICTLY_FALSE.getFunction(), overloads); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/standard/SizeFunction.java b/runtime/src/main/java/dev/cel/runtime/standard/SizeFunction.java new file mode 100644 index 000000000..a8f787665 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/SizeFunction.java @@ -0,0 +1,103 @@ +// 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.standard; + +import com.google.common.collect.ImmutableSet; +import com.google.protobuf.ByteString; +import dev.cel.common.CelOptions; +import dev.cel.common.values.CelByteString; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.RuntimeEquality; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** Standard function for {@code size}. */ +public final class SizeFunction extends CelStandardFunction { + private static final SizeFunction ALL_OVERLOADS = create(SizeOverload.values()); + + public static SizeFunction create() { + return ALL_OVERLOADS; + } + + public static SizeFunction create(SizeFunction.SizeOverload... overloads) { + return create(Arrays.asList(overloads)); + } + + public static SizeFunction create(Iterable overloads) { + return new SizeFunction(ImmutableSet.copyOf(overloads)); + } + + /** Overloads for the standard function. */ + @SuppressWarnings("rawtypes") + public enum SizeOverload implements CelStandardOverload { + SIZE_BYTES( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "size_bytes", CelByteString.class, (CelByteString bytes) -> (long) bytes.size()); + } else { + return CelFunctionBinding.from( + "size_bytes", ByteString.class, (ByteString bytes) -> (long) bytes.size()); + } + }), + BYTES_SIZE( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "bytes_size", CelByteString.class, (CelByteString bytes) -> (long) bytes.size()); + } else { + return CelFunctionBinding.from( + "bytes_size", ByteString.class, (ByteString bytes) -> (long) bytes.size()); + } + }), + SIZE_LIST( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from("size_list", List.class, (List list1) -> (long) list1.size())), + LIST_SIZE( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from("list_size", List.class, (List list1) -> (long) list1.size())), + SIZE_STRING( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "size_string", String.class, (String s) -> (long) s.codePointCount(0, s.length()))), + STRING_SIZE( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "string_size", String.class, (String s) -> (long) s.codePointCount(0, s.length()))), + SIZE_MAP( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from("size_map", Map.class, (Map map1) -> (long) map1.size())), + MAP_SIZE( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from("map_size", Map.class, (Map map1) -> (long) map1.size())); + + private final CelStandardOverload standardOverload; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return standardOverload.newFunctionBinding(celOptions, runtimeEquality); + } + + SizeOverload(CelStandardOverload standardOverload) { + this.standardOverload = standardOverload; + } + } + + private SizeFunction(ImmutableSet overloads) { + super("size", overloads); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/standard/StartsWithFunction.java b/runtime/src/main/java/dev/cel/runtime/standard/StartsWithFunction.java new file mode 100644 index 000000000..457dd3cf1 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/StartsWithFunction.java @@ -0,0 +1,63 @@ +// 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.standard; + +import com.google.common.collect.ImmutableSet; +import dev.cel.common.CelOptions; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.RuntimeEquality; +import java.util.Arrays; + +/** Standard function for {@code startsWith}. */ +public final class StartsWithFunction extends CelStandardFunction { + private static final StartsWithFunction ALL_OVERLOADS = create(StartsWithOverload.values()); + + public static StartsWithFunction create() { + return ALL_OVERLOADS; + } + + public static StartsWithFunction create(StartsWithFunction.StartsWithOverload... overloads) { + return create(Arrays.asList(overloads)); + } + + public static StartsWithFunction create( + Iterable overloads) { + return new StartsWithFunction(ImmutableSet.copyOf(overloads)); + } + + /** Overloads for the standard function. */ + public enum StartsWithOverload implements CelStandardOverload { + STARTS_WITH_STRING( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "starts_with_string", String.class, String.class, String::startsWith)); + + private final CelStandardOverload standardOverload; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return standardOverload.newFunctionBinding(celOptions, runtimeEquality); + } + + StartsWithOverload(CelStandardOverload standardOverload) { + this.standardOverload = standardOverload; + } + } + + private StartsWithFunction(ImmutableSet overloads) { + super("startsWith", overloads); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/standard/StringFunction.java b/runtime/src/main/java/dev/cel/runtime/standard/StringFunction.java new file mode 100644 index 000000000..8fbc8add7 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/StringFunction.java @@ -0,0 +1,134 @@ +// 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.standard; + +import com.google.common.collect.ImmutableSet; +import com.google.common.primitives.UnsignedLong; +import com.google.common.primitives.UnsignedLongs; +import com.google.protobuf.ByteString; +import com.google.protobuf.Duration; +import com.google.protobuf.Timestamp; +import dev.cel.common.CelOptions; +import dev.cel.common.exceptions.CelBadFormatException; +import dev.cel.common.internal.DateTimeHelpers; +import dev.cel.common.internal.ProtoTimeUtils; +import dev.cel.common.values.CelByteString; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.RuntimeEquality; +import java.time.Instant; +import java.util.Arrays; + +/** Standard function for {@code string} conversion function. */ +public final class StringFunction extends CelStandardFunction { + private static final StringFunction ALL_OVERLOADS = create(StringOverload.values()); + + public static StringFunction create() { + return ALL_OVERLOADS; + } + + public static StringFunction create(StringFunction.StringOverload... overloads) { + return create(Arrays.asList(overloads)); + } + + public static StringFunction create(Iterable overloads) { + return new StringFunction(ImmutableSet.copyOf(overloads)); + } + + /** Overloads for the standard function. */ + public enum StringOverload implements CelStandardOverload { + STRING_TO_STRING( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from("string_to_string", String.class, (String x) -> x)), + INT64_TO_STRING( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from("int64_to_string", Long.class, Object::toString)), + DOUBLE_TO_STRING( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from("double_to_string", Double.class, Object::toString)), + BOOL_TO_STRING( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from("bool_to_string", Boolean.class, Object::toString)), + BYTES_TO_STRING( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "bytes_to_string", + CelByteString.class, + (byteStr) -> { + if (!byteStr.isValidUtf8()) { + throw new CelBadFormatException( + "invalid UTF-8 in bytes, cannot convert to string"); + } + return byteStr.toStringUtf8(); + }); + } else { + return CelFunctionBinding.from( + "bytes_to_string", + ByteString.class, + (byteStr) -> { + if (!byteStr.isValidUtf8()) { + throw new CelBadFormatException( + "invalid UTF-8 in bytes, cannot convert to string"); + } + return byteStr.toStringUtf8(); + }); + } + }), + TIMESTAMP_TO_STRING( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from("timestamp_to_string", Instant.class, Instant::toString); + } else { + return CelFunctionBinding.from( + "timestamp_to_string", Timestamp.class, ProtoTimeUtils::toString); + } + }), + DURATION_TO_STRING( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "duration_to_string", java.time.Duration.class, DateTimeHelpers::toString); + } else { + return CelFunctionBinding.from( + "duration_to_string", Duration.class, ProtoTimeUtils::toString); + } + }), + UINT64_TO_STRING( + (celOptions, runtimeEquality) -> { + if (celOptions.enableUnsignedLongs()) { + return CelFunctionBinding.from( + "uint64_to_string", UnsignedLong.class, UnsignedLong::toString); + } else { + return CelFunctionBinding.from("uint64_to_string", Long.class, UnsignedLongs::toString); + } + }); + + private final CelStandardOverload standardOverload; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return standardOverload.newFunctionBinding(celOptions, runtimeEquality); + } + + StringOverload(CelStandardOverload standardOverload) { + this.standardOverload = standardOverload; + } + } + + private StringFunction(ImmutableSet overloads) { + super("string", overloads); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/standard/SubtractOperator.java b/runtime/src/main/java/dev/cel/runtime/standard/SubtractOperator.java new file mode 100644 index 000000000..784c46825 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/SubtractOperator.java @@ -0,0 +1,162 @@ +// 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.standard; + +import static dev.cel.common.Operator.SUBTRACT; + +import com.google.common.collect.ImmutableSet; +import com.google.common.primitives.UnsignedLong; +import com.google.protobuf.Duration; +import com.google.protobuf.Timestamp; +import dev.cel.common.CelOptions; +import dev.cel.common.exceptions.CelNumericOverflowException; +import dev.cel.common.internal.DateTimeHelpers; +import dev.cel.common.internal.ProtoTimeUtils; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.RuntimeEquality; +import dev.cel.runtime.RuntimeHelpers; +import java.time.Instant; +import java.util.Arrays; + +/** Standard function for the subtraction (-) operator. */ +public final class SubtractOperator extends CelStandardFunction { + private static final SubtractOperator ALL_OVERLOADS = create(SubtractOverload.values()); + + public static SubtractOperator create() { + return ALL_OVERLOADS; + } + + public static SubtractOperator create(SubtractOverload... overloads) { + return create(Arrays.asList(overloads)); + } + + public static SubtractOperator create(Iterable overloads) { + return new SubtractOperator(ImmutableSet.copyOf(overloads)); + } + + /** Overloads for the standard function. */ + public enum SubtractOverload implements CelStandardOverload { + SUBTRACT_INT64( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "subtract_int64", + Long.class, + Long.class, + (Long x, Long y) -> { + try { + return RuntimeHelpers.int64Subtract(x, y, celOptions); + } catch (ArithmeticException e) { + throw new CelNumericOverflowException(e); + } + })), + SUBTRACT_TIMESTAMP_TIMESTAMP( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "subtract_timestamp_timestamp", + Instant.class, + Instant.class, + (Instant i1, Instant i2) -> java.time.Duration.between(i2, i1)); + } else { + return CelFunctionBinding.from( + "subtract_timestamp_timestamp", + Timestamp.class, + Timestamp.class, + (Timestamp t1, Timestamp t2) -> ProtoTimeUtils.between(t2, t1)); + } + }), + SUBTRACT_TIMESTAMP_DURATION( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "subtract_timestamp_duration", + Instant.class, + java.time.Duration.class, + DateTimeHelpers::subtract); + } else { + return CelFunctionBinding.from( + "subtract_timestamp_duration", + Timestamp.class, + Duration.class, + ProtoTimeUtils::subtract); + } + }), + SUBTRACT_UINT64( + (celOptions, runtimeEquality) -> { + if (celOptions.enableUnsignedLongs()) { + return CelFunctionBinding.from( + "subtract_uint64", + UnsignedLong.class, + UnsignedLong.class, + (UnsignedLong x, UnsignedLong y) -> { + try { + return RuntimeHelpers.uint64Subtract(x, y); + } catch (ArithmeticException e) { + throw new CelNumericOverflowException(e); + } + }); + } else { + return CelFunctionBinding.from( + "subtract_uint64", + Long.class, + Long.class, + (Long x, Long y) -> { + try { + return RuntimeHelpers.uint64Subtract(x, y, celOptions); + } catch (ArithmeticException e) { + throw new CelNumericOverflowException(e); + } + }); + } + }), + SUBTRACT_DOUBLE( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "subtract_double", Double.class, Double.class, (Double x, Double y) -> x - y)), + SUBTRACT_DURATION_DURATION( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "subtract_duration_duration", + java.time.Duration.class, + java.time.Duration.class, + DateTimeHelpers::subtract); + } else { + return CelFunctionBinding.from( + "subtract_duration_duration", + Duration.class, + Duration.class, + ProtoTimeUtils::subtract); + } + }), + ; + + private final CelStandardOverload standardOverload; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return standardOverload.newFunctionBinding(celOptions, runtimeEquality); + } + + SubtractOverload(CelStandardOverload standardOverload) { + this.standardOverload = standardOverload; + } + } + + private SubtractOperator(ImmutableSet overloads) { + super(SUBTRACT.getFunction(), overloads); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/standard/TimestampFunction.java b/runtime/src/main/java/dev/cel/runtime/standard/TimestampFunction.java new file mode 100644 index 000000000..00a4ed43e --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/TimestampFunction.java @@ -0,0 +1,111 @@ +// 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.standard; + +import com.google.common.collect.ImmutableSet; +import com.google.protobuf.Timestamp; +import dev.cel.common.CelOptions; +import dev.cel.common.exceptions.CelBadFormatException; +import dev.cel.common.internal.DateTimeHelpers; +import dev.cel.common.internal.ProtoTimeUtils; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.RuntimeEquality; +import java.text.ParseException; +import java.time.Instant; +import java.time.format.DateTimeParseException; +import java.util.Arrays; + +/** Standard function for {@code timestamp} conversion function. */ +public final class TimestampFunction extends CelStandardFunction { + private static final TimestampFunction ALL_OVERLOADS = create(TimestampOverload.values()); + + public static TimestampFunction create() { + return ALL_OVERLOADS; + } + + public static TimestampFunction create(TimestampFunction.TimestampOverload... overloads) { + return create(Arrays.asList(overloads)); + } + + public static TimestampFunction create(Iterable overloads) { + return new TimestampFunction(ImmutableSet.copyOf(overloads)); + } + + /** Overloads for the standard function. */ + public enum TimestampOverload implements CelStandardOverload { + STRING_TO_TIMESTAMP( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "string_to_timestamp", + String.class, + (String ts) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + try { + return DateTimeHelpers.parse(ts); + } catch (DateTimeParseException e) { + throw new CelBadFormatException(e); + } + + } else { + try { + return ProtoTimeUtils.parse(ts); + } catch (ParseException e) { + throw new CelBadFormatException(e); + } + } + })), + TIMESTAMP_TO_TIMESTAMP( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "timestamp_to_timestamp", Instant.class, (Instant x) -> x); + } else { + return CelFunctionBinding.from( + "timestamp_to_timestamp", Timestamp.class, (Timestamp x) -> x); + } + }), + INT64_TO_TIMESTAMP( + (celOptions, runtimeEquality) -> { + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return CelFunctionBinding.from( + "int64_to_timestamp", Long.class, epochSecond -> { + Instant instant = Instant.ofEpochSecond(epochSecond); + DateTimeHelpers.checkValid(instant); + return instant; + }); + } else { + return CelFunctionBinding.from( + "int64_to_timestamp", Long.class, ProtoTimeUtils::fromSecondsToTimestamp); + } + }), + ; + + private final CelStandardOverload standardOverload; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return standardOverload.newFunctionBinding(celOptions, runtimeEquality); + } + + TimestampOverload(CelStandardOverload standardOverload) { + this.standardOverload = standardOverload; + } + } + + private TimestampFunction(ImmutableSet overloads) { + super("timestamp", overloads); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/standard/TypeFunction.java b/runtime/src/main/java/dev/cel/runtime/standard/TypeFunction.java new file mode 100644 index 000000000..b325c0e9c --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/TypeFunction.java @@ -0,0 +1,71 @@ +// 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.standard; + +import com.google.common.collect.ImmutableSet; +import dev.cel.common.CelOptions; +import dev.cel.common.types.SimpleType; +import dev.cel.common.types.TypeType; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.RuntimeEquality; +import dev.cel.runtime.TypeResolver; + +/** + * Standard function for the {@code type} function. + * + *

The {@code type} function returns the CEL type of its argument. It accepts a + * {@link TypeResolver} so that different runtimes can supply the appropriate resolver (e.g. a + * descriptor-based resolver for full proto, or a base resolver for lite proto). + */ +public final class TypeFunction extends CelStandardFunction { + + private final TypeResolver typeResolver; + + public static TypeFunction create(TypeResolver typeResolver) { + return new TypeFunction(typeResolver); + } + + /** Overloads for the standard {@code type} function. */ + public enum TypeOverload implements CelStandardOverload { + TYPE; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + // This overload is not used directly. The binding is created in TypeFunction via the + // TypeResolver instance. + // TODO: Instantiate from CelStandardFunctions. + throw new UnsupportedOperationException( + "TypeOverload bindings must be created through TypeFunction.create(TypeResolver)"); + } + } + + @Override + public ImmutableSet newFunctionBindings( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + CelFunctionBinding binding = + CelFunctionBinding.from( + "type", + Object.class, + arg -> typeResolver.resolveObjectType(arg, TypeType.create(SimpleType.DYN))); + + return CelFunctionBinding.fromOverloads("type", ImmutableSet.of(binding)); + } + + private TypeFunction(TypeResolver typeResolver) { + super("type", ImmutableSet.copyOf(TypeOverload.values())); + this.typeResolver = typeResolver; + } +} \ No newline at end of file diff --git a/runtime/src/main/java/dev/cel/runtime/standard/UintFunction.java b/runtime/src/main/java/dev/cel/runtime/standard/UintFunction.java new file mode 100644 index 000000000..be2217dd9 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/UintFunction.java @@ -0,0 +1,155 @@ +// 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.standard; + +import com.google.common.collect.ImmutableSet; +import com.google.common.primitives.UnsignedLong; +import com.google.common.primitives.UnsignedLongs; +import dev.cel.common.CelOptions; +import dev.cel.common.exceptions.CelBadFormatException; +import dev.cel.common.exceptions.CelNumericOverflowException; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.RuntimeEquality; +import dev.cel.runtime.RuntimeHelpers; +import java.math.BigDecimal; +import java.util.Arrays; + +/** Standard function for {@code uint} conversion function. */ +public final class UintFunction extends CelStandardFunction { + private static final UintFunction ALL_OVERLOADS = create(UintOverload.values()); + + public static UintFunction create() { + return ALL_OVERLOADS; + } + + public static UintFunction create(UintFunction.UintOverload... overloads) { + return create(Arrays.asList(overloads)); + } + + public static UintFunction create(Iterable overloads) { + return new UintFunction(ImmutableSet.copyOf(overloads)); + } + + /** Overloads for the standard function. */ + public enum UintOverload implements CelStandardOverload { + UINT64_TO_UINT64( + (celOptions, runtimeEquality) -> { + if (celOptions.enableUnsignedLongs()) { + return CelFunctionBinding.from( + "uint64_to_uint64", UnsignedLong.class, (UnsignedLong x) -> x); + } else { + return CelFunctionBinding.from("uint64_to_uint64", Long.class, (Long x) -> x); + } + }), + INT64_TO_UINT64( + (celOptions, runtimeEquality) -> { + if (celOptions.enableUnsignedLongs()) { + return CelFunctionBinding.from( + "int64_to_uint64", + Long.class, + (Long arg) -> { + if (celOptions.errorOnIntWrap() && arg < 0) { + throw new CelNumericOverflowException("int out of uint range"); + } + return UnsignedLong.valueOf(arg); + }); + } else { + return CelFunctionBinding.from( + "int64_to_uint64", + Long.class, + (Long arg) -> { + if (celOptions.errorOnIntWrap() && arg < 0) { + throw new CelNumericOverflowException("int out of uint range"); + } + return arg; + }); + } + }), + DOUBLE_TO_UINT64( + (celOptions, runtimeEquality) -> { + if (celOptions.enableUnsignedLongs()) { + return CelFunctionBinding.from( + "double_to_uint64", + Double.class, + (Double arg) -> { + if (celOptions.errorOnIntWrap()) { + return RuntimeHelpers.doubleToUnsignedChecked(arg) + .orElseThrow( + () -> new CelNumericOverflowException("double out of uint range")); + } + return UnsignedLong.valueOf(BigDecimal.valueOf(arg).toBigInteger()); + }); + } else { + return CelFunctionBinding.from( + "double_to_uint64", + Double.class, + (Double arg) -> { + if (celOptions.errorOnIntWrap()) { + return RuntimeHelpers.doubleToUnsignedChecked(arg) + .map(UnsignedLong::longValue) + .orElseThrow( + () -> + new CelNumericOverflowException( + "double out of uint range")); + } + return arg.longValue(); + }); + } + }), + STRING_TO_UINT64( + (celOptions, runtimeEquality) -> { + if (celOptions.enableUnsignedLongs()) { + return CelFunctionBinding.from( + "string_to_uint64", + String.class, + (String arg) -> { + try { + return UnsignedLong.valueOf(arg); + } catch (NumberFormatException e) { + throw new CelBadFormatException(e); + } + }); + } else { + return CelFunctionBinding.from( + "string_to_uint64", + String.class, + (String arg) -> { + try { + return UnsignedLongs.parseUnsignedLong(arg); + } catch (NumberFormatException e) { + throw new CelBadFormatException(e); + } + }); + } + }), + ; + + private final CelStandardOverload standardOverload; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return standardOverload.newFunctionBinding(celOptions, runtimeEquality); + } + + UintOverload(CelStandardOverload standardOverload) { + this.standardOverload = standardOverload; + } + } + + private UintFunction(ImmutableSet overloads) { + super("uint", overloads); + } +} diff --git a/runtime/src/test/java/dev/cel/runtime/ActivationTest.java b/runtime/src/test/java/dev/cel/runtime/ActivationTest.java index 5eb10737c..fc435c848 100644 --- a/runtime/src/test/java/dev/cel/runtime/ActivationTest.java +++ b/runtime/src/test/java/dev/cel/runtime/ActivationTest.java @@ -17,11 +17,11 @@ import static com.google.common.truth.Truth.assertThat; import com.google.common.primitives.UnsignedLong; -import com.google.protobuf.NullValue; import dev.cel.common.CelOptions; -import dev.cel.testing.testdata.proto3.TestAllTypesProto.NestedTestAllTypes; -import dev.cel.testing.testdata.proto3.TestAllTypesProto.TestAllTypes; -import dev.cel.testing.testdata.proto3.TestAllTypesProto.TestAllTypes.NestedMessage; +import dev.cel.common.values.NullValue; +import dev.cel.expr.conformance.proto3.NestedTestAllTypes; +import dev.cel.expr.conformance.proto3.TestAllTypes; +import dev.cel.expr.conformance.proto3.TestAllTypes.NestedMessage; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -33,7 +33,13 @@ 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() + .enableUnsignedLongs(true) + .fromProtoUnsetFieldOption(CelOptions.ProtoUnsetFieldOptions.SKIP) + .build(); @Test public void copyOf_success_withNullEntries() { @@ -49,27 +55,37 @@ public void copyOf_success_withNullEntries() { @Test public void fromProto() { NestedMessage nestedMessage = NestedMessage.newBuilder().setBb(1).build(); - Activation activation = Activation.fromProto(nestedMessage, TEST_OPTIONS); + Activation activation = ProtoMessageActivationFactory.fromProto(nestedMessage, TEST_OPTIONS); assertThat(activation.resolve("bb")).isEqualTo(1); TestAllTypes testMessage = TestAllTypes.newBuilder().setSingleNestedMessage(nestedMessage).build(); - activation = Activation.fromProto(testMessage, TEST_OPTIONS); + activation = ProtoMessageActivationFactory.fromProto(testMessage, TEST_OPTIONS); assertThat(activation.resolve("single_nested_message")).isEqualTo(nestedMessage); } @Test public void fromProto_unsetScalarField() { - Activation activation = Activation.fromProto(NestedMessage.getDefaultInstance(), TEST_OPTIONS); + Activation activation = + ProtoMessageActivationFactory.fromProto(NestedMessage.getDefaultInstance(), TEST_OPTIONS); assertThat(activation.resolve("bb")).isEqualTo(0); } + @Test + public void fromProto_unsetScalarField_skipUnsetFields() { + Activation activation = + ProtoMessageActivationFactory.fromProto( + NestedMessage.getDefaultInstance(), TEST_OPTIONS_SKIP_UNSET_FIELDS); + assertThat(activation.resolve("bb")).isNull(); + } + @Test public void fromProto_unsetAnyField() { // An unset Any field is the only field which cannot be accurately published into an Activation, // and is instead published as an error value which should fit nicely with the CEL evaluation // semantics. - Activation activation = Activation.fromProto(TestAllTypes.getDefaultInstance(), TEST_OPTIONS); + Activation activation = + ProtoMessageActivationFactory.fromProto(TestAllTypes.getDefaultInstance(), TEST_OPTIONS); assertThat(activation.resolve("single_any")).isInstanceOf(Throwable.class); assertThat((Throwable) activation.resolve("single_any")) .hasMessageThat() @@ -81,20 +97,35 @@ public void fromProto_unsetAnyField() { @Test public void fromProto_unsetValueField() { - Activation activation = Activation.fromProto(TestAllTypes.getDefaultInstance(), TEST_OPTIONS); + Activation activation = + ProtoMessageActivationFactory.fromProto(TestAllTypes.getDefaultInstance(), TEST_OPTIONS); assertThat(activation.resolve("single_value")).isEqualTo(NullValue.NULL_VALUE); } @Test public void fromProto_unsetMessageField() { Activation activation = - Activation.fromProto(NestedTestAllTypes.getDefaultInstance(), TEST_OPTIONS); + ProtoMessageActivationFactory.fromProto( + NestedTestAllTypes.getDefaultInstance(), TEST_OPTIONS); assertThat(activation.resolve("payload")).isEqualTo(TestAllTypes.getDefaultInstance()); } @Test public void fromProto_unsetRepeatedField() { - Activation activation = Activation.fromProto(TestAllTypes.getDefaultInstance(), TEST_OPTIONS); + Activation activation = + ProtoMessageActivationFactory.fromProto(TestAllTypes.getDefaultInstance(), TEST_OPTIONS); + assertThat(activation.resolve("repeated_int64")).isInstanceOf(List.class); + assertThat((List) activation.resolve("repeated_int64")).isEmpty(); + + assertThat(activation.resolve("repeated_nested_message")).isInstanceOf(List.class); + assertThat((List) activation.resolve("repeated_nested_message")).isEmpty(); + } + + @Test + public void fromProto_unsetRepeatedField_skipUnsetFields() { + Activation activation = + ProtoMessageActivationFactory.fromProto( + TestAllTypes.getDefaultInstance(), TEST_OPTIONS_SKIP_UNSET_FIELDS); assertThat(activation.resolve("repeated_int64")).isInstanceOf(List.class); assertThat((List) activation.resolve("repeated_int64")).isEmpty(); @@ -104,7 +135,17 @@ public void fromProto_unsetRepeatedField() { @Test public void fromProto_unsetMapField() { - Activation activation = Activation.fromProto(TestAllTypes.getDefaultInstance(), TEST_OPTIONS); + Activation activation = + ProtoMessageActivationFactory.fromProto(TestAllTypes.getDefaultInstance(), TEST_OPTIONS); + assertThat(activation.resolve("map_int32_int64")).isInstanceOf(Map.class); + assertThat((Map) activation.resolve("map_int32_int64")).isEmpty(); + } + + @Test + public void fromProto_unsetMapField_skipUnsetFields() { + Activation activation = + ProtoMessageActivationFactory.fromProto( + TestAllTypes.getDefaultInstance(), TEST_OPTIONS_SKIP_UNSET_FIELDS); assertThat(activation.resolve("map_int32_int64")).isInstanceOf(Map.class); assertThat((Map) activation.resolve("map_int32_int64")).isEmpty(); } @@ -112,7 +153,7 @@ public void fromProto_unsetMapField() { @Test public void fromProto_unsignedLongField_unsignedResult() { Activation activation = - Activation.fromProto( + ProtoMessageActivationFactory.fromProto( TestAllTypes.newBuilder() .setSingleUint32(1) .setSingleUint64(UnsignedLong.MAX_VALUE.longValue()) @@ -122,17 +163,4 @@ public void fromProto_unsignedLongField_unsignedResult() { assertThat((UnsignedLong) activation.resolve("single_uint64")) .isEqualTo(UnsignedLong.MAX_VALUE); } - - @Test - public void fromProto_unsignedLongField_signedResult() { - // Test disables the unsigned long support. - Activation activation = - Activation.fromProto( - TestAllTypes.newBuilder() - .setSingleUint32(1) - .setSingleUint64(UnsignedLong.MAX_VALUE.longValue()) - .build()); - assertThat((Long) activation.resolve("single_uint32")).isEqualTo(1L); - assertThat((Long) activation.resolve("single_uint64")).isEqualTo(-1L); - } } diff --git a/runtime/src/test/java/dev/cel/runtime/BUILD.bazel b/runtime/src/test/java/dev/cel/runtime/BUILD.bazel index fea8d6e43..e886c3d8a 100644 --- a/runtime/src/test/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/test/java/dev/cel/runtime/BUILD.bazel @@ -1,50 +1,107 @@ +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"]) +# Invalidate cache after file removal +package( + default_applicable_licenses = ["//:license"], + default_testonly = True, +) + +ANDROID_TESTS = [ + "CelLiteRuntimeAndroidTest.java", +] java_library( name = "tests", testonly = 1, srcs = glob( ["*.java"], + # keep sorted exclude = [ - "CelValueInterpreterTest.java", + "CelLiteInterpreterTest.java", "InterpreterTest.java", - ], + "PlannerInterpreterTest.java", + ] + ANDROID_TESTS, ), deps = [ "//:auto_value", "//:java_truth", "//bundle:cel", - "//common", + "//common:cel_ast", + "//common:cel_descriptor_util", + "//common:cel_descriptors", + "//common:cel_exception", + "//common:cel_source", + "//common:compiler_common", + "//common:container", "//common:error_codes", "//common:options", "//common:proto_v1alpha1_ast", - "//common:runtime_exception", "//common/ast", + "//common/exceptions:bad_format", + "//common/exceptions:divide_by_zero", + "//common/exceptions:numeric_overflow", + "//common/exceptions:runtime_exception", "//common/internal:cel_descriptor_pools", "//common/internal:converter", "//common/internal:default_message_factory", "//common/internal:dynamic_proto", "//common/internal:proto_message_factory", + "//common/internal:proto_time_utils", "//common/internal:well_known_proto", - "//common/resources/testdata/proto2:messages_extensions_proto2_java_proto", - "//common/resources/testdata/proto2:messages_proto2_java_proto", - "//common/resources/testdata/proto3:test_all_types_java_proto", "//common/types", "//common/types:cel_v1alpha1_types", + "//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", + "//extensions", + "//extensions:optional_library", "//parser:macro", "//parser:unparser", "//runtime", + "//runtime:activation", + "//runtime:dispatcher", + "//runtime:evaluation_exception_builder", "//runtime:evaluation_listener", + "//runtime:function_binding", + "//runtime:interpretable", "//runtime:interpreter", - "//runtime:runtime_helper", + "//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", + "//runtime:resolved_overload", + "//runtime:runtime_equality", + "//runtime:runtime_helpers", + "//runtime:standard_functions", + "//runtime:type_resolver", "//runtime:unknown_attributes", "//runtime:unknown_options", + "//runtime/standard:add", + "//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", + "//testing/protos:multi_file_java_proto", + "//testing/protos:single_file_java_proto", + "//testing/protos:test_all_types_cel_java_proto2", + "//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", "@com_google_googleapis//google/api/expr/v1alpha1:expr_java_proto", - "@maven//:com_google_api_grpc_proto_google_common_protos", + "@com_google_googleapis//google/rpc/context:attribute_context_java_proto", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", "@maven//:com_google_protobuf_protobuf_java_util", @@ -52,6 +109,7 @@ java_library( "@maven//:com_google_truth_extensions_truth_proto_extension", "@maven//:junit_junit", "@maven//:org_jspecify_jspecify", + "@maven_android//:com_google_protobuf_protobuf_javalite", ], ) @@ -63,26 +121,93 @@ java_library( ], deps = [ # "//java/com/google/testing/testsize:annotations", + "//testing:base_interpreter_test", + "@maven//:junit_junit", + "@maven//:com_google_testparameterinjector_test_parameter_injector", + ], +) + +java_library( + name = "planner_interpreter_test", + testonly = 1, + srcs = [ + "PlannerInterpreterTest.java", + ], + resources = [ + "//runtime/testdata", + ], + deps = [ + "//common:cel_ast", + "//common:compiler_common", + "//common:container", "//common:options", + "//common/types", + "//common/types:type_providers", + "//extensions", + "//runtime", + "//runtime:function_binding", + "//runtime:unknown_attributes", "//testing:base_interpreter_test", - "//testing:eval", - "//testing:sync", + "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + "@maven//:com_google_testparameterinjector_test_parameter_injector", "@maven//:junit_junit", ], ) +cel_android_local_test( + name = "android_tests", + srcs = ANDROID_TESTS, + test_class = "dev.cel.runtime.CelLiteRuntimeAndroidTest", + deps = [ + "//:java_truth", + "//common:cel_ast_android", + "//common:options", + "//common/internal:proto_time_utils_android", + "//common/values:cel_byte_string", + "//common/values:cel_value_provider_android", + "//common/values:proto_message_lite_value_provider_android", + "//extensions:lite_extensions_android", + "//extensions:sets_function", + "//runtime:evaluation_exception", + "//runtime:function_binding_android", + "//runtime:late_function_binding_android", + "//runtime:lite_runtime_android", + "//runtime:lite_runtime_factory_android", + "//runtime:lite_runtime_impl_android", + "//runtime:standard_functions_android", + "//runtime:unknown_attributes_android", + "//runtime/src/main/java/dev/cel/runtime:program_android", + "//runtime/standard:equals_android", + "//runtime/standard:int_android", + "//testing/protos:test_all_types_cel_java_proto2_lite", + "//testing/protos:test_all_types_cel_java_proto3_lite", + "//testing/src/main/java/dev/cel/testing/compiled:compiled_expr_utils_android", + "@cel_spec//proto/cel/expr:checked_java_proto_lite", + "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto_lite", + "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto_lite", + "@maven//:com_google_testparameterinjector_test_parameter_injector", + "@maven_android//:com_google_guava_guava", + "@maven_android//:com_google_protobuf_protobuf_javalite", + ], +) + java_library( - name = "cel_value_interpreter_test", + name = "cel_lite_interpreter_test", testonly = 1, srcs = [ - "CelValueInterpreterTest.java", + "CelLiteInterpreterTest.java", ], deps = [ - # "//java/com/google/testing/testsize:annotations", "//common:options", + "//common/values:proto_message_lite_value_provider", + "//extensions:optional_library", + "//runtime", "//testing:base_interpreter_test", - "//testing:cel_value_sync", - "//testing:eval", + "//testing/protos:test_all_types_cel_java_proto2", + "//testing/protos:test_all_types_cel_java_proto3", + "@maven//:com_google_testparameterinjector_test_parameter_injector", "@maven//:junit_junit", ], ) @@ -96,8 +221,9 @@ junit4_test_suites( ], src_dir = "src/test/java", deps = [ - ":cel_value_interpreter_test", + ":cel_lite_interpreter_test", ":interpreter_test", + ":planner_interpreter_test", ":tests", ], ) diff --git a/runtime/src/test/java/dev/cel/runtime/CelAttributeParserTest.java b/runtime/src/test/java/dev/cel/runtime/CelAttributeParserTest.java index 58e5f2735..e2d463555 100644 --- a/runtime/src/test/java/dev/cel/runtime/CelAttributeParserTest.java +++ b/runtime/src/test/java/dev/cel/runtime/CelAttributeParserTest.java @@ -101,19 +101,19 @@ public void parse_unsupportedExprKindThrows() { Assert.assertThrows( IllegalArgumentException.class, () -> CelAttributeParser.parse("1 / 2")); - assertThat(iae).hasMessageThat().contains("_/_(CONST_EXPR, CONST_EXPR)"); + assertThat(iae).hasMessageThat().contains("_/_(CONSTANT, CONSTANT)"); iae = Assert.assertThrows( IllegalArgumentException.class, () -> CelAttributeParser.parse("123.field")); - assertThat(iae).hasMessageThat().contains("CONST_EXPR"); + assertThat(iae).hasMessageThat().contains("CelConstant"); iae = Assert.assertThrows( IllegalArgumentException.class, () -> CelAttributeParser.parse("a && b")); - assertThat(iae).hasMessageThat().contains("_&&_(IDENT_EXPR, IDENT_EXPR)"); + assertThat(iae).hasMessageThat().contains("_&&_(IDENT, IDENT)"); } @Test diff --git a/runtime/src/test/java/dev/cel/runtime/CelEvaluationExceptionBuilderTest.java b/runtime/src/test/java/dev/cel/runtime/CelEvaluationExceptionBuilderTest.java new file mode 100644 index 000000000..c073a8443 --- /dev/null +++ b/runtime/src/test/java/dev/cel/runtime/CelEvaluationExceptionBuilderTest.java @@ -0,0 +1,101 @@ +// 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 static com.google.common.truth.Truth.assertThat; + +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.common.CelErrorCode; +import dev.cel.common.exceptions.CelBadFormatException; +import dev.cel.common.exceptions.CelRuntimeException; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public class CelEvaluationExceptionBuilderTest { + + @Test + public void builder_default() { + CelEvaluationExceptionBuilder builder = CelEvaluationExceptionBuilder.newBuilder("foo"); + + CelEvaluationException e = builder.build(); + + assertThat(e).hasMessageThat().isEqualTo("evaluation error: foo"); + assertThat(e).hasCauseThat().isNull(); + assertThat(e.getErrorCode()).isEqualTo(CelErrorCode.INTERNAL_ERROR); + } + + @Test + public void builder_withoutMetadata() { + IllegalStateException cause = new IllegalStateException("Cause"); + CelEvaluationExceptionBuilder builder = + CelEvaluationExceptionBuilder.newBuilder("foo") + .setCause(cause) + .setErrorCode(CelErrorCode.ATTRIBUTE_NOT_FOUND); + + CelEvaluationException e = builder.build(); + + assertThat(e).hasMessageThat().isEqualTo("evaluation error: foo"); + assertThat(e).hasCauseThat().isEqualTo(cause); + assertThat(e.getErrorCode()).isEqualTo(CelErrorCode.ATTRIBUTE_NOT_FOUND); + } + + @Test + public void builder_allPropertiesSet() { + IllegalStateException cause = new IllegalStateException("Cause"); + CelEvaluationExceptionBuilder builder = + CelEvaluationExceptionBuilder.newBuilder("foo") + .setCause(cause) + .setErrorCode(CelErrorCode.BAD_FORMAT) + .setMetadata( + new Metadata() { + @Override + public String getLocation() { + return "location.txt"; + } + + @Override + public int getPosition(long exprId) { + return 10; + } + + @Override + public boolean hasPosition(long exprId) { + return true; + } + }, + 0); + + CelEvaluationException e = builder.build(); + + assertThat(e).hasMessageThat().isEqualTo("evaluation error at location.txt:10: foo"); + assertThat(e).hasCauseThat().isEqualTo(cause); + assertThat(e.getErrorCode()).isEqualTo(CelErrorCode.BAD_FORMAT); + } + + @Test + public void builder_fromCelRuntimeException() { + IllegalStateException cause = new IllegalStateException("cause error message"); + CelRuntimeException celRuntimeException = new CelBadFormatException(cause); + CelEvaluationExceptionBuilder builder = + CelEvaluationExceptionBuilder.newBuilder(celRuntimeException); + + CelEvaluationException e = builder.build(); + + assertThat(e).hasMessageThat().isEqualTo("evaluation error: cause error message"); + assertThat(e).hasCauseThat().isEqualTo(celRuntimeException); + assertThat(e.getErrorCode()).isEqualTo(CelErrorCode.BAD_FORMAT); + } +} diff --git a/runtime/src/test/java/dev/cel/runtime/CelLateFunctionBindingsTest.java b/runtime/src/test/java/dev/cel/runtime/CelLateFunctionBindingsTest.java new file mode 100644 index 000000000..819b99665 --- /dev/null +++ b/runtime/src/test/java/dev/cel/runtime/CelLateFunctionBindingsTest.java @@ -0,0 +1,142 @@ +// 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. + +package dev.cel.runtime; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableList; +import com.google.common.primitives.UnsignedLong; +import dev.cel.common.CelErrorCode; +import dev.cel.expr.conformance.proto3.TestAllTypes; +import java.util.Optional; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link CelLateFunctionBindings}. */ +@RunWith(JUnit4.class) +public final class CelLateFunctionBindingsTest { + + @Test + public void findOverload_singleMatchingFunction_isPresent() throws Exception { + CelLateFunctionBindings bindings = + CelLateFunctionBindings.from( + CelFunctionBinding.from("increment_int", Long.class, (arg) -> arg + 1), + CelFunctionBinding.from( + "increment_uint", UnsignedLong.class, (arg) -> arg.plus(UnsignedLong.ONE))); + Optional overload = + bindings.findOverloadMatchingArgs( + "increment", ImmutableList.of("increment_int", "increment_uint"), new Object[] {1L}); + assertThat(overload).isPresent(); + assertThat(overload.get().getOverloadId()).isEqualTo("increment_int"); + assertThat(overload.get().getParameterTypes()).containsExactly(Long.class); + assertThat(overload.get().getDefinition().apply(new Object[] {1L})).isEqualTo(2L); + } + + @Test + public void findOverload_noMatchingFunctionSameArgCount_isEmpty() throws Exception { + CelLateFunctionBindings bindings = + CelLateFunctionBindings.from( + CelFunctionBinding.from("increment_int", Long.class, (arg) -> arg + 1), + CelFunctionBinding.from( + "increment_uint", UnsignedLong.class, (arg) -> arg.plus(UnsignedLong.ONE))); + Optional overload = + bindings.findOverloadMatchingArgs( + "increment", ImmutableList.of("increment_int", "increment_uint"), new Object[] {1.0}); + assertThat(overload).isEmpty(); + } + + @Test + public void findOverload_noMatchingFunctionDifferentArgCount_isEmpty() throws Exception { + CelLateFunctionBindings bindings = + CelLateFunctionBindings.from( + CelFunctionBinding.from("increment_int", Long.class, (arg) -> arg + 1), + CelFunctionBinding.from( + "increment_uint", UnsignedLong.class, (arg) -> arg.plus(UnsignedLong.ONE))); + Optional overload = + bindings.findOverloadMatchingArgs( + "increment", + ImmutableList.of("increment_int", "increment_uint"), + new Object[] {1.0, 1.0}); + assertThat(overload).isEmpty(); + } + + @Test + public void findOverload_badInput_throwsException() throws Exception { + CelLateFunctionBindings bindings = + CelLateFunctionBindings.from( + CelFunctionBinding.from( + "increment_uint", + UnsignedLong.class, + (arg) -> { + if (arg.equals(UnsignedLong.MAX_VALUE)) { + throw new CelEvaluationException( + "numeric overflow", null, CelErrorCode.NUMERIC_OVERFLOW); + } + return arg.plus(UnsignedLong.ONE); + })); + Optional overload = + bindings.findOverloadMatchingArgs( + "increment", ImmutableList.of("increment_uint"), new Object[] {UnsignedLong.MAX_VALUE}); + assertThat(overload).isPresent(); + assertThat(overload.get().getOverloadId()).isEqualTo("increment_uint"); + assertThat(overload.get().getParameterTypes()).containsExactly(UnsignedLong.class); + CelEvaluationException e = + Assert.assertThrows( + CelEvaluationException.class, + () -> overload.get().getDefinition().apply(new Object[] {UnsignedLong.MAX_VALUE})); + assertThat(e.getErrorCode()).isEqualTo(CelErrorCode.NUMERIC_OVERFLOW); + } + + @Test + public void findOverload_multipleMatchingFunctions_throwsException() throws Exception { + CelLateFunctionBindings bindings = + CelLateFunctionBindings.from( + CelFunctionBinding.from("increment_int", Long.class, (arg) -> arg + 1), + CelFunctionBinding.from("increment_uint", Long.class, (arg) -> arg + 2)); + CelEvaluationException e = + Assert.assertThrows( + CelEvaluationException.class, + () -> + bindings.findOverloadMatchingArgs( + "increment", + ImmutableList.of("increment_int", "increment_uint"), + new Object[] {1L})); + assertThat(e).hasMessageThat().contains("Ambiguous overloads for function 'increment'"); + } + + @Test + public void findOverload_nullPrimitiveArg_isEmpty() throws Exception { + CelLateFunctionBindings bindings = + CelLateFunctionBindings.from( + CelFunctionBinding.from("identity_int", Long.class, (arg) -> arg)); + Optional overload = + bindings.findOverloadMatchingArgs( + "identity", ImmutableList.of("identity_int"), new Object[] {null}); + assertThat(overload).isEmpty(); + } + + @Test + public void findOverload_nullMessageArg_isEmpty() throws Exception { + CelLateFunctionBindings bindings = + CelLateFunctionBindings.from( + CelFunctionBinding.from("identity_msg", TestAllTypes.class, (arg) -> arg)); + Optional overload = + bindings.findOverloadMatchingArgs( + "identity", ImmutableList.of("identity_msg"), new Object[] {null}); + assertThat(overload).isEmpty(); + } +} diff --git a/runtime/src/test/java/dev/cel/runtime/CelLiteInterpreterTest.java b/runtime/src/test/java/dev/cel/runtime/CelLiteInterpreterTest.java new file mode 100644 index 000000000..1d1a316c0 --- /dev/null +++ b/runtime/src/test/java/dev/cel/runtime/CelLiteInterpreterTest.java @@ -0,0 +1,112 @@ +// 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.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.common.CelOptions; +import dev.cel.common.values.ProtoMessageLiteValueProvider; +import dev.cel.expr.conformance.proto3.TestAllTypesCelDescriptor; +import dev.cel.extensions.CelOptionalLibrary; +import dev.cel.testing.BaseInterpreterTest; +import org.junit.runner.RunWith; + +/** + * Exercises a suite of interpreter tests defined in {@link BaseInterpreterTest} using {@link + * ProtoMessageLiteValueProvider} and full version of protobuf messages. + */ +@RunWith(TestParameterInjector.class) +public class CelLiteInterpreterTest extends BaseInterpreterTest { + + @Override + protected CelRuntimeBuilder newBaseRuntimeBuilder(CelOptions celOptions) { + return CelRuntimeFactory.standardCelRuntimeBuilder() + .setValueProvider( + ProtoMessageLiteValueProvider.newInstance( + dev.cel.expr.conformance.proto2.TestAllTypesCelDescriptor.getDescriptor(), + TestAllTypesCelDescriptor.getDescriptor())) + .addLibraries(CelOptionalLibrary.INSTANCE) + .setOptions(celOptions.toBuilder().enableCelValue(true).build()); + } + + @Override + public void dynamicMessage_adapted() throws Exception { + // Dynamic message is not supported in Protolite + skipBaselineVerification(); + } + + @Override + public void dynamicMessage_dynamicDescriptor() throws Exception { + // Dynamic message is not supported in Protolite + skipBaselineVerification(); + } + + // 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(); + } + + @Override + public void jsonConversions() { + skipBaselineVerification(); + } + + @Override + public void nestedEnums() { + skipBaselineVerification(); + } + + @Override + public void messages() throws Exception { + skipBaselineVerification(); + } + + @Override + public void packUnpackAny() { + skipBaselineVerification(); + } + + @Override + public void lists() throws Exception { + skipBaselineVerification(); + } + + @Override + public void maps() throws Exception { + skipBaselineVerification(); + } + + @Override + public void jsonValueTypes() { + skipBaselineVerification(); + } + + @Override + public void messages_error() { + skipBaselineVerification(); + } + + @Override + public void jsonFieldNames() { + // json_name field option is not yet supported in lite runtime + skipBaselineVerification(); + } +} diff --git a/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeAndroidTest.java b/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeAndroidTest.java new file mode 100644 index 000000000..73492d126 --- /dev/null +++ b/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeAndroidTest.java @@ -0,0 +1,731 @@ +// 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 static com.google.common.truth.Truth.assertThat; +import static dev.cel.testing.compiled.CompiledExprUtils.readCheckedExpr; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.primitives.UnsignedLong; +import com.google.common.truth.Correspondence; +import com.google.protobuf.BoolValue; +import com.google.protobuf.ByteString; +import com.google.protobuf.BytesValue; +import com.google.protobuf.DoubleValue; +import com.google.protobuf.FloatValue; +import com.google.protobuf.Int32Value; +import com.google.protobuf.Int64Value; +import com.google.protobuf.StringValue; +import com.google.protobuf.UInt32Value; +import com.google.protobuf.UInt64Value; +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.common.CelOptions; +import dev.cel.common.internal.ProtoTimeUtils; +import dev.cel.common.values.CelByteString; +import dev.cel.common.values.CelValueProvider; +import dev.cel.common.values.ProtoMessageLiteValueProvider; +import dev.cel.expr.conformance.proto3.NestedTestAllTypes; +import dev.cel.expr.conformance.proto3.NestedTestAllTypesCelLiteDescriptor; +import dev.cel.expr.conformance.proto3.TestAllTypes; +import dev.cel.expr.conformance.proto3.TestAllTypesCelLiteDescriptor; +import dev.cel.extensions.CelLiteExtensions; +import dev.cel.extensions.SetsFunction; +import dev.cel.runtime.standard.EqualsOperator; +import dev.cel.runtime.standard.IntFunction; +import dev.cel.runtime.standard.IntFunction.IntOverload; +import java.time.Duration; +import java.time.Instant; +import java.util.List; +import java.util.Map; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public class CelLiteRuntimeAndroidTest { + private static final double DOUBLE_TOLERANCE = 0.00001d; + private static final Correspondence, List> LIST_WITH_DOUBLE_TOLERANCE = + Correspondence.from( + (actualList, expectedList) -> { + if (actualList == null + || expectedList == null + || actualList.size() != expectedList.size()) { + return false; + } + for (int i = 0; i < actualList.size(); i++) { + Object actual = actualList.get(i); + Object expected = expectedList.get(i); + + if (actual instanceof Double && expected instanceof Double) { + return Math.abs((Double) actual - (Double) expected) <= DOUBLE_TOLERANCE; + } else if (!actual.equals(expected)) { + return false; + } + } + return true; + }, + String.format( + "has elements that are equal (with tolerance of %f for doubles)", DOUBLE_TOLERANCE)); + + private static final Correspondence, Map> MAP_WITH_DOUBLE_TOLERANCE = + Correspondence.from( + (actualMap, expectedMap) -> { + if (actualMap == null + || expectedMap == null + || actualMap.size() != expectedMap.size()) { + return false; + } + + for (Map.Entry actualEntry : actualMap.entrySet()) { + if (!expectedMap.containsKey(actualEntry.getKey())) { + return false; + } + + Object actualEntryValue = actualEntry.getValue(); + Object expectedEntryValue = expectedMap.get(actualEntry.getKey()); + if (actualEntryValue instanceof Double && expectedEntryValue instanceof Double) { + return Math.abs((Double) actualEntryValue - (Double) expectedEntryValue) + <= DOUBLE_TOLERANCE; + } else if (!actualEntryValue.equals(expectedEntryValue)) { + return false; + } + } + + return true; + }, + String.format( + "has elements that are equal (with tolerance of %f for doubles)", DOUBLE_TOLERANCE)); + + @Test + public void toRuntimeBuilder_isNewInstance() { + CelLiteRuntimeBuilder runtimeBuilder = CelLiteRuntimeFactory.newLiteRuntimeBuilder(); + CelLiteRuntime runtime = runtimeBuilder.build(); + + CelLiteRuntimeBuilder newRuntimeBuilder = runtime.toRuntimeBuilder(); + + assertThat(newRuntimeBuilder).isNotEqualTo(runtimeBuilder); + } + + @Test + public void toRuntimeBuilder_propertiesCopied() { + CelOptions celOptions = CelOptions.current().enableCelValue(true).build(); + CelLiteRuntimeLibrary runtimeExtension = + CelLiteExtensions.sets(celOptions, SetsFunction.INTERSECTS); + CelValueProvider celValueProvider = ProtoMessageLiteValueProvider.newInstance(); + IntFunction intFunction = IntFunction.create(IntOverload.INT64_TO_INT64); + EqualsOperator equalsOperator = EqualsOperator.create(); + CelLiteRuntimeBuilder runtimeBuilder = + CelLiteRuntimeFactory.newLiteRuntimeBuilder() + .setOptions(celOptions) + .setStandardFunctions(intFunction, equalsOperator) + .addFunctionBindings( + CelFunctionBinding.from("string_isEmpty", String.class, String::isEmpty)) + .setValueProvider(celValueProvider) + .addLibraries(runtimeExtension); + CelLiteRuntime runtime = runtimeBuilder.build(); + + LiteRuntimeImpl.Builder newRuntimeBuilder = + (LiteRuntimeImpl.Builder) runtime.toRuntimeBuilder(); + + assertThat(newRuntimeBuilder.celOptions).isEqualTo(celOptions); + assertThat(newRuntimeBuilder.celValueProvider).isSameInstanceAs(celValueProvider); + assertThat(newRuntimeBuilder.runtimeLibrariesBuilder.build()).containsExactly(runtimeExtension); + assertThat(newRuntimeBuilder.standardFunctionBuilder.build()) + .containsExactly(intFunction, equalsOperator) + .inOrder(); + 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 + public void setCelOptions_unallowedOptionsSet_throws(@TestParameter CelOptionsTestCase testCase) { + assertThrows( + IllegalArgumentException.class, + () -> + CelLiteRuntimeFactory.newLiteRuntimeBuilder().setOptions(testCase.celOptions).build()); + } + + @Test + public void standardEnvironment_disabledByDefault() throws Exception { + CelLiteRuntime runtime = CelLiteRuntimeFactory.newLiteRuntimeBuilder().build(); + // Expr: 1 + 2 + CelAbstractSyntaxTree ast = readCheckedExpr("compiled_one_plus_two"); + + CelEvaluationException e = + assertThrows(CelEvaluationException.class, () -> runtime.createProgram(ast).eval()); + assertThat(e) + .hasMessageThat() + .contains( + "evaluation error at :2: No matching overload for function '_+_'. Overload" + + " candidates: add_int64"); + } + + @Test + public void eval_add() throws Exception { + CelLiteRuntime runtime = + CelLiteRuntimeFactory.newLiteRuntimeBuilder() + .setStandardFunctions(CelStandardFunctions.ALL_STANDARD_FUNCTIONS) + .build(); + // Expr: 1 + 2 + CelAbstractSyntaxTree ast = readCheckedExpr("compiled_one_plus_two"); + + assertThat(runtime.createProgram(ast).eval()).isEqualTo(3L); + } + + @Test + public void eval_stringLiteral() throws Exception { + CelLiteRuntime runtime = CelLiteRuntimeFactory.newLiteRuntimeBuilder().build(); + // Expr: 'hello world' + CelAbstractSyntaxTree ast = readCheckedExpr("compiled_hello_world"); + Program program = runtime.createProgram(ast); + + String result = (String) program.eval(); + + assertThat(result).isEqualTo("hello world"); + } + + @Test + @SuppressWarnings("unchecked") + public void eval_listLiteral() throws Exception { + CelLiteRuntime runtime = CelLiteRuntimeFactory.newLiteRuntimeBuilder().build(); + // Expr: ['a', 1, 2u, 3.5] + CelAbstractSyntaxTree ast = readCheckedExpr("compiled_list_literal"); + Program program = runtime.createProgram(ast); + + List result = (List) program.eval(); + + assertThat(result).containsExactly("a", 1L, UnsignedLong.valueOf(2L), 3.5d).inOrder(); + } + + @Test + public void eval_comprehensionExists() throws Exception { + CelLiteRuntime runtime = + CelLiteRuntimeFactory.newLiteRuntimeBuilder() + .setStandardFunctions(CelStandardFunctions.ALL_STANDARD_FUNCTIONS) + .build(); + // Expr: [1,2,3].exists(x, x == 3) + CelAbstractSyntaxTree ast = readCheckedExpr("compiled_comprehension_exists"); + Program program = runtime.createProgram(ast); + + boolean result = (boolean) program.eval(); + + assertThat(result).isTrue(); + } + + @Test + public void eval_primitiveVariables() throws Exception { + CelLiteRuntime runtime = + CelLiteRuntimeFactory.newLiteRuntimeBuilder() + .setStandardFunctions(CelStandardFunctions.ALL_STANDARD_FUNCTIONS) + .build(); + // Expr: bool_var && bytes_var == b'abc' && double_var == 1.0 && int_var == 42 && uint_var == + // 42u && str_var == 'foo' + CelAbstractSyntaxTree ast = readCheckedExpr("compiled_primitive_variables"); + Program program = runtime.createProgram(ast); + + boolean result = + (boolean) + program.eval( + ImmutableMap.of( + "bool_var", + true, + "bytes_var", + CelByteString.copyFromUtf8("abc"), + "double_var", + 1.0, + "int_var", + 42L, + "uint_var", + UnsignedLong.valueOf(42L), + "str_var", + "foo")); + + assertThat(result).isTrue(); + } + + @Test + @SuppressWarnings("rawtypes") + public void eval_customFunctions() throws Exception { + CelLiteRuntime runtime = + CelLiteRuntimeFactory.newLiteRuntimeBuilder() + .addFunctionBindings( + CelFunctionBinding.from("string_isEmpty", String.class, String::isEmpty), + CelFunctionBinding.from("list_isEmpty", List.class, List::isEmpty)) + .setStandardFunctions(CelStandardFunctions.ALL_STANDARD_FUNCTIONS) + .build(); + // Expr: ''.isEmpty() && [].isEmpty() + CelAbstractSyntaxTree ast = readCheckedExpr("compiled_custom_functions"); + Program program = runtime.createProgram(ast); + + boolean result = (boolean) program.eval(); + + assertThat(result).isTrue(); + } + + @Test + @SuppressWarnings("rawtypes") + public void eval_customFunctions_asLateBoundFunctions() throws Exception { + CelLiteRuntime runtime = + CelLiteRuntimeFactory.newLiteRuntimeBuilder() + .addFunctionBindings(CelFunctionBinding.from("list_isEmpty", List.class, List::isEmpty)) + .setStandardFunctions(CelStandardFunctions.ALL_STANDARD_FUNCTIONS) + .build(); + // Expr: ''.isEmpty() && [].isEmpty() + CelAbstractSyntaxTree ast = readCheckedExpr("compiled_custom_functions"); + Program program = runtime.createProgram(ast); + + boolean result = + (boolean) + program.eval( + ImmutableMap.of(), + CelLateFunctionBindings.from( + CelFunctionBinding.from("string_isEmpty", String.class, String::isEmpty), + CelFunctionBinding.from("list_isEmpty", List.class, List::isEmpty))); + + assertThat(result).isTrue(); + } + + @Test + @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(); + CelAbstractSyntaxTree ast = readCheckedExpr(checkedExpr); + Program program = runtime.createProgram(ast); + + CelUnknownSet result = (CelUnknownSet) program.eval(); + + assertThat(result.unknownExprIds()).hasSize(15); + } + + @Test + @TestParameters("{checkedExpr: 'compiled_proto2_select_primitives_all_ored'}") + @TestParameters("{checkedExpr: 'compiled_proto3_select_primitives_all_ored'}") + public void eval_protoMessage_primitiveWithDefaults(String checkedExpr) throws Exception { + CelLiteRuntime runtime = + CelLiteRuntimeFactory.newLiteRuntimeBuilder() + .setStandardFunctions(CelStandardFunctions.ALL_STANDARD_FUNCTIONS) + .setValueProvider( + ProtoMessageLiteValueProvider.newInstance( + dev.cel.expr.conformance.proto2.TestAllTypesCelLiteDescriptor.getDescriptor(), + dev.cel.expr.conformance.proto2.NestedTestAllTypesCelLiteDescriptor + .getDescriptor(), + TestAllTypesCelLiteDescriptor.getDescriptor(), + NestedTestAllTypesCelLiteDescriptor.getDescriptor())) + .build(); + // Ensures that all branches of the OR conditions are evaluated, and that appropriate defaults + // are returned for primitives. + CelAbstractSyntaxTree ast = readCheckedExpr(checkedExpr); + Program program = runtime.createProgram(ast); + + boolean result = + (boolean) + program.eval( + ImmutableMap.of( + "proto2", dev.cel.expr.conformance.proto2.TestAllTypes.getDefaultInstance(), + "proto3", TestAllTypes.getDefaultInstance())); + + assertThat(result).isFalse(); // False should be returned to avoid short circuiting. + } + + @Test + @TestParameters("{checkedExpr: 'compiled_proto2_select_primitives'}") + @TestParameters("{checkedExpr: 'compiled_proto3_select_primitives'}") + public void eval_protoMessage_primitives(String checkedExpr) throws Exception { + CelLiteRuntime runtime = + CelLiteRuntimeFactory.newLiteRuntimeBuilder() + .setStandardFunctions(CelStandardFunctions.ALL_STANDARD_FUNCTIONS) + .setValueProvider( + ProtoMessageLiteValueProvider.newInstance( + dev.cel.expr.conformance.proto2.TestAllTypesCelLiteDescriptor.getDescriptor(), + TestAllTypesCelLiteDescriptor.getDescriptor())) + .build(); + CelAbstractSyntaxTree ast = readCheckedExpr(checkedExpr); + Program program = runtime.createProgram(ast); + + boolean result = + (boolean) + program.eval( + ImmutableMap.of( + "proto2", + dev.cel.expr.conformance.proto2.TestAllTypes.newBuilder() + .setSingleInt32(1) + .setSingleInt64(2L) + .setSingleUint32(3) + .setSingleUint64(4L) + .setSingleSint32(5) + .setSingleSint64(6L) + .setSingleFixed32(7) + .setSingleFixed64(8L) + .setSingleSfixed32(9) + .setSingleSfixed64(10L) + .setSingleFloat(1.5f) + .setSingleDouble(2.5d) + .setSingleBool(true) + .setSingleString("hello world") + .setSingleBytes(ByteString.copyFromUtf8("abc")) + .build(), + "proto3", + TestAllTypes.newBuilder() + .setSingleInt32(1) + .setSingleInt64(2L) + .setSingleUint32(3) + .setSingleUint64(4L) + .setSingleSint32(5) + .setSingleSint64(6L) + .setSingleFixed32(7) + .setSingleFixed64(8L) + .setSingleSfixed32(9) + .setSingleSfixed64(10L) + .setSingleFloat(1.5f) + .setSingleDouble(2.5d) + .setSingleBool(true) + .setSingleString("hello world") + .setSingleBytes(ByteString.copyFromUtf8("abc")) + .build())); + + assertThat(result).isTrue(); + } + + @Test + @TestParameters("{checkedExpr: 'compiled_proto2_select_wrappers'}") + @TestParameters("{checkedExpr: 'compiled_proto3_select_wrappers'}") + public void eval_protoMessage_wrappers(String checkedExpr) throws Exception { + CelLiteRuntime runtime = + CelLiteRuntimeFactory.newLiteRuntimeBuilder() + .setStandardFunctions(CelStandardFunctions.ALL_STANDARD_FUNCTIONS) + .setValueProvider( + ProtoMessageLiteValueProvider.newInstance( + dev.cel.expr.conformance.proto2.TestAllTypesCelLiteDescriptor.getDescriptor(), + TestAllTypesCelLiteDescriptor.getDescriptor())) + .build(); + CelAbstractSyntaxTree ast = readCheckedExpr(checkedExpr); + Program program = runtime.createProgram(ast); + + boolean result = + (boolean) + program.eval( + ImmutableMap.of( + "proto2", + dev.cel.expr.conformance.proto2.TestAllTypes.newBuilder() + .setSingleInt32Wrapper(Int32Value.of(1)) + .setSingleInt64Wrapper(Int64Value.of(2L)) + .setSingleUint32Wrapper(UInt32Value.of(3)) + .setSingleUint64Wrapper(UInt64Value.of(4L)) + .setSingleFloatWrapper(FloatValue.of(1.5f)) + .setSingleDoubleWrapper(DoubleValue.of(2.5d)) + .setSingleBoolWrapper(BoolValue.of(true)) + .setSingleStringWrapper(StringValue.of("hello world")) + .setSingleBytesWrapper(BytesValue.of(ByteString.copyFromUtf8("abc"))) + .build(), + "proto3", + TestAllTypes.newBuilder() + .setSingleInt32Wrapper(Int32Value.of(1)) + .setSingleInt64Wrapper(Int64Value.of(2L)) + .setSingleUint32Wrapper(UInt32Value.of(3)) + .setSingleUint64Wrapper(UInt64Value.of(4L)) + .setSingleFloatWrapper(FloatValue.of(1.5f)) + .setSingleDoubleWrapper(DoubleValue.of(2.5d)) + .setSingleBoolWrapper(BoolValue.of(true)) + .setSingleStringWrapper(StringValue.of("hello world")) + .setSingleBytesWrapper(BytesValue.of(ByteString.copyFromUtf8("abc"))) + .build())); + + assertThat(result).isTrue(); + } + + @Test + @SuppressWarnings("unchecked") + @TestParameters("{checkedExpr: 'compiled_proto2_deep_traversal'}") + @TestParameters("{checkedExpr: 'compiled_proto3_deep_traversal'}") + public void eval_protoMessage_safeTraversal(String checkedExpr) throws Exception { + CelLiteRuntime runtime = + CelLiteRuntimeFactory.newLiteRuntimeBuilder() + .setStandardFunctions(CelStandardFunctions.ALL_STANDARD_FUNCTIONS) + .setValueProvider( + ProtoMessageLiteValueProvider.newInstance( + dev.cel.expr.conformance.proto2.TestAllTypesCelLiteDescriptor.getDescriptor(), + dev.cel.expr.conformance.proto2.NestedTestAllTypesCelLiteDescriptor + .getDescriptor(), + TestAllTypesCelLiteDescriptor.getDescriptor(), + NestedTestAllTypesCelLiteDescriptor.getDescriptor())) + .build(); + // Expr: proto2.oneof_type.payload.repeated_string + CelAbstractSyntaxTree ast = readCheckedExpr(checkedExpr); + + Program program = runtime.createProgram(ast); + + List result = + (List) + program.eval( + ImmutableMap.of( + "proto2", dev.cel.expr.conformance.proto2.TestAllTypes.getDefaultInstance(), + "proto3", TestAllTypes.getDefaultInstance())); + + assertThat(result).isEmpty(); + } + + @Test + @SuppressWarnings("unchecked") + @TestParameters("{checkedExpr: 'compiled_proto2_deep_traversal'}") + @TestParameters("{checkedExpr: 'compiled_proto3_deep_traversal'}") + public void eval_protoMessage_deepTraversalReturnsRepeatedStrings(String checkedExpr) + throws Exception { + CelLiteRuntime runtime = + CelLiteRuntimeFactory.newLiteRuntimeBuilder() + .setStandardFunctions(CelStandardFunctions.ALL_STANDARD_FUNCTIONS) + .setValueProvider( + ProtoMessageLiteValueProvider.newInstance( + dev.cel.expr.conformance.proto2.TestAllTypesCelLiteDescriptor.getDescriptor(), + dev.cel.expr.conformance.proto2.NestedTestAllTypesCelLiteDescriptor + .getDescriptor(), + TestAllTypesCelLiteDescriptor.getDescriptor(), + NestedTestAllTypesCelLiteDescriptor.getDescriptor())) + .build(); + // Expr: proto2.oneof_type.payload.repeated_string + CelAbstractSyntaxTree ast = readCheckedExpr(checkedExpr); + Program program = runtime.createProgram(ast); + ImmutableList data = ImmutableList.of("hello", "world"); + + List result = + (List) + program.eval( + ImmutableMap.of( + "proto2", + dev.cel.expr.conformance.proto2.TestAllTypes.newBuilder() + .setOneofType( + dev.cel.expr.conformance.proto2.NestedTestAllTypes.newBuilder() + .setPayload( + dev.cel.expr.conformance.proto2.TestAllTypes.newBuilder() + .addAllRepeatedString(data) + .build())), + "proto3", + TestAllTypes.newBuilder() + .setOneofType( + NestedTestAllTypes.newBuilder() + .setPayload( + TestAllTypes.newBuilder() + .addAllRepeatedString(data) + .build())))); + + assertThat(result).isEqualTo(data); + } + + @Test + @SuppressWarnings("unchecked") + @TestParameters("{checkedExpr: 'compiled_proto2_select_repeated_fields'}") + @TestParameters("{checkedExpr: 'compiled_proto3_select_repeated_fields'}") + public void eval_protoMessage_repeatedFields(String checkedExpr) throws Exception { + CelLiteRuntime runtime = + CelLiteRuntimeFactory.newLiteRuntimeBuilder() + .setStandardFunctions(CelStandardFunctions.ALL_STANDARD_FUNCTIONS) + .setValueProvider( + ProtoMessageLiteValueProvider.newInstance( + dev.cel.expr.conformance.proto2.TestAllTypesCelLiteDescriptor.getDescriptor(), + TestAllTypesCelLiteDescriptor.getDescriptor())) + .build(); + dev.cel.expr.conformance.proto2.TestAllTypes proto2TestMsg = + dev.cel.expr.conformance.proto2.TestAllTypes.newBuilder() + .addAllRepeatedInt32(ImmutableList.of(1, 2)) + .addAllRepeatedInt64(ImmutableList.of(3L, 4L)) + .addAllRepeatedUint32(ImmutableList.of(5, 6)) + .addAllRepeatedUint64(ImmutableList.of(7L, 8L)) + .addAllRepeatedSint32(ImmutableList.of(9, 10)) + .addAllRepeatedSint64(ImmutableList.of(11L, 12L)) + .addAllRepeatedFixed32(ImmutableList.of(13, 14)) + .addAllRepeatedFixed64(ImmutableList.of(15L, 16L)) + .addAllRepeatedSfixed32(ImmutableList.of(17, 18)) + .addAllRepeatedSfixed64(ImmutableList.of(19L, 20L)) + .addAllRepeatedFloat(ImmutableList.of(21.1f, 22.2f)) + .addAllRepeatedDouble(ImmutableList.of(23.3, 24.4)) + .addAllRepeatedBool(ImmutableList.of(true, false)) + .addAllRepeatedString(ImmutableList.of("alpha", "beta")) + .addAllRepeatedBytes( + ImmutableList.of( + ByteString.copyFromUtf8("gamma"), ByteString.copyFromUtf8("delta"))) + .build(); + TestAllTypes proto3TestMsg = + TestAllTypes.newBuilder() + .addAllRepeatedInt32(ImmutableList.of(1, 2)) + .addAllRepeatedInt64(ImmutableList.of(3L, 4L)) + .addAllRepeatedUint32(ImmutableList.of(5, 6)) + .addAllRepeatedUint64(ImmutableList.of(7L, 8L)) + .addAllRepeatedSint32(ImmutableList.of(9, 10)) + .addAllRepeatedSint64(ImmutableList.of(11L, 12L)) + .addAllRepeatedFixed32(ImmutableList.of(13, 14)) + .addAllRepeatedFixed64(ImmutableList.of(15L, 16L)) + .addAllRepeatedSfixed32(ImmutableList.of(17, 18)) + .addAllRepeatedSfixed64(ImmutableList.of(19L, 20L)) + .addAllRepeatedFloat(ImmutableList.of(21.1f, 22.2f)) + .addAllRepeatedDouble(ImmutableList.of(23.3, 24.4)) + .addAllRepeatedBool(ImmutableList.of(true, false)) + .addAllRepeatedString(ImmutableList.of("alpha", "beta")) + .addAllRepeatedBytes( + ImmutableList.of( + ByteString.copyFromUtf8("gamma"), ByteString.copyFromUtf8("delta"))) + .build(); + CelAbstractSyntaxTree ast = readCheckedExpr(checkedExpr); + Program program = runtime.createProgram(ast); + + List result = + (List) + program.eval(ImmutableMap.of("proto2", proto2TestMsg, "proto3", proto3TestMsg)); + + assertThat(result) + .comparingElementsUsing(LIST_WITH_DOUBLE_TOLERANCE) + .containsExactly( + ImmutableList.of(1L, 2L), + ImmutableList.of(3L, 4L), + ImmutableList.of(UnsignedLong.valueOf(5L), UnsignedLong.valueOf(6L)), + ImmutableList.of(UnsignedLong.valueOf(7L), UnsignedLong.valueOf(8L)), + ImmutableList.of(9L, 10L), + ImmutableList.of(11L, 12L), + ImmutableList.of(13L, 14L), + ImmutableList.of(15L, 16L), + ImmutableList.of(17L, 18L), + ImmutableList.of(19L, 20L), + ImmutableList.of(21.1d, 22.2d), + ImmutableList.of(23.3d, 24.4d), + ImmutableList.of(true, false), + ImmutableList.of("alpha", "beta"), + ImmutableList.of( + CelByteString.copyFromUtf8("gamma"), CelByteString.copyFromUtf8("delta"))) + .inOrder(); + } + + @Test + // leave proto2.TestAllTypes qualification as is for clarity + @SuppressWarnings({"UnnecessarilyFullyQualified", "unchecked"}) + @TestParameters("{checkedExpr: 'compiled_proto2_select_map_fields'}") + @TestParameters("{checkedExpr: 'compiled_proto3_select_map_fields'}") + public void eval_protoMessage_mapFields(String checkedExpr) throws Exception { + CelLiteRuntime runtime = + CelLiteRuntimeFactory.newLiteRuntimeBuilder() + .setStandardFunctions(CelStandardFunctions.ALL_STANDARD_FUNCTIONS) + .setValueProvider( + ProtoMessageLiteValueProvider.newInstance( + dev.cel.expr.conformance.proto2.TestAllTypesCelLiteDescriptor.getDescriptor(), + TestAllTypesCelLiteDescriptor.getDescriptor())) + .build(); + dev.cel.expr.conformance.proto2.TestAllTypes proto2TestMsg = + dev.cel.expr.conformance.proto2.TestAllTypes.newBuilder() + .putAllMapBoolBool(ImmutableMap.of(true, false, false, true)) + .putAllMapBoolString(ImmutableMap.of(true, "foo", false, "bar")) + .putAllMapBoolBytes( + ImmutableMap.of( + true, ByteString.copyFromUtf8("baz"), false, ByteString.copyFromUtf8("qux"))) + .putAllMapBoolInt32(ImmutableMap.of(true, 1, false, 2)) + .putAllMapBoolInt64(ImmutableMap.of(true, 3L, false, 4L)) + .putAllMapBoolUint32(ImmutableMap.of(true, 5, false, 6)) + .putAllMapBoolUint64(ImmutableMap.of(true, 7L, false, 8L)) + .putAllMapBoolFloat(ImmutableMap.of(true, 9.1f, false, 10.2f)) + .putAllMapBoolDouble(ImmutableMap.of(true, 11.3, false, 12.4)) + .putAllMapBoolEnum( + ImmutableMap.of( + true, + dev.cel.expr.conformance.proto2.TestAllTypes.NestedEnum.BAR, + false, + dev.cel.expr.conformance.proto2.TestAllTypes.NestedEnum.BAZ)) + .putAllMapBoolDuration( + ImmutableMap.of( + true, + ProtoTimeUtils.fromSecondsToDuration(15), + false, + ProtoTimeUtils.fromSecondsToDuration(16))) + .putAllMapBoolTimestamp( + ImmutableMap.of( + true, + ProtoTimeUtils.fromSecondsToTimestamp(17), + false, + ProtoTimeUtils.fromSecondsToTimestamp(18))) + .build(); + TestAllTypes proto3TestMsg = + TestAllTypes.newBuilder() + .putAllMapBoolBool(ImmutableMap.of(true, false, false, true)) + .putAllMapBoolString(ImmutableMap.of(true, "foo", false, "bar")) + .putAllMapBoolBytes( + ImmutableMap.of( + true, ByteString.copyFromUtf8("baz"), false, ByteString.copyFromUtf8("qux"))) + .putAllMapBoolInt32(ImmutableMap.of(true, 1, false, 2)) + .putAllMapBoolInt64(ImmutableMap.of(true, 3L, false, 4L)) + .putAllMapBoolUint32(ImmutableMap.of(true, 5, false, 6)) + .putAllMapBoolUint64(ImmutableMap.of(true, 7L, false, 8L)) + .putAllMapBoolFloat(ImmutableMap.of(true, 9.1f, false, 10.2f)) + .putAllMapBoolDouble(ImmutableMap.of(true, 11.3, false, 12.4)) + .putAllMapBoolEnum( + ImmutableMap.of( + true, TestAllTypes.NestedEnum.BAR, false, TestAllTypes.NestedEnum.BAZ)) + .putAllMapBoolDuration( + ImmutableMap.of( + true, + ProtoTimeUtils.fromSecondsToDuration(15), + false, + ProtoTimeUtils.fromSecondsToDuration(16))) + .putAllMapBoolTimestamp( + ImmutableMap.of( + true, + ProtoTimeUtils.fromSecondsToTimestamp(17), + false, + ProtoTimeUtils.fromSecondsToTimestamp(18))) + .build(); + CelAbstractSyntaxTree ast = readCheckedExpr(checkedExpr); + Program program = runtime.createProgram(ast); + + List result = + (List) + program.eval(ImmutableMap.of("proto2", proto2TestMsg, "proto3", proto3TestMsg)); + + assertThat(result) + .comparingElementsUsing(MAP_WITH_DOUBLE_TOLERANCE) + .containsExactly( + ImmutableMap.of(true, false, false, true), + ImmutableMap.of(true, "foo", false, "bar"), + ImmutableMap.of( + true, CelByteString.copyFromUtf8("baz"), false, CelByteString.copyFromUtf8("qux")), + ImmutableMap.of(true, 1L, false, 2L), + ImmutableMap.of(true, 3L, false, 4L), + ImmutableMap.of(true, UnsignedLong.valueOf(5), false, UnsignedLong.valueOf(6)), + ImmutableMap.of(true, UnsignedLong.valueOf(7L), false, UnsignedLong.valueOf(8L)), + ImmutableMap.of(true, 9.1d, false, 10.2d), + ImmutableMap.of(true, 11.3d, false, 12.4d), + ImmutableMap.of(true, 1L, false, 2L), // Note: Enums are converted into integers + ImmutableMap.of(true, Duration.ofSeconds(15), false, Duration.ofSeconds(16)), + ImmutableMap.of(true, Instant.ofEpochSecond(17), false, Instant.ofEpochSecond(18))) + .inOrder(); + } + + private enum CelOptionsTestCase { + CEL_VALUE_DISABLED(newBaseTestOptions().enableCelValue(false).build()), + UNSIGNED_LONG_DISABLED(newBaseTestOptions().enableUnsignedLongs(false).build()), + UNWRAP_WKT_DISABLED(newBaseTestOptions().unwrapWellKnownTypesOnFunctionDispatch(false).build()), + ; + + private final CelOptions celOptions; + + private static CelOptions.Builder newBaseTestOptions() { + return CelOptions.current().enableCelValue(true); + } + + CelOptionsTestCase(CelOptions celOptions) { + this.celOptions = celOptions; + } + } +} diff --git a/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeTest.java b/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeTest.java new file mode 100644 index 000000000..0ce7bd184 --- /dev/null +++ b/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeTest.java @@ -0,0 +1,696 @@ +// 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 static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.primitives.UnsignedLong; +import com.google.protobuf.BoolValue; +import com.google.protobuf.ByteString; +import com.google.protobuf.BytesValue; +import com.google.protobuf.DoubleValue; +import com.google.protobuf.FloatValue; +import com.google.protobuf.Int32Value; +import com.google.protobuf.Int64Value; +import com.google.protobuf.ListValue; +import com.google.protobuf.StringValue; +import com.google.protobuf.Struct; +import com.google.protobuf.UInt32Value; +import com.google.protobuf.UInt64Value; +import com.google.protobuf.util.Values; +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.common.CelContainer; +import dev.cel.common.CelFunctionDecl; +import dev.cel.common.CelOverloadDecl; +import dev.cel.common.internal.ProtoTimeUtils; +import dev.cel.common.types.SimpleType; +import dev.cel.common.types.StructTypeReference; +import dev.cel.common.values.CelByteString; +import dev.cel.common.values.NullValue; +import dev.cel.common.values.ProtoMessageLiteValueProvider; +import dev.cel.compiler.CelCompiler; +import dev.cel.compiler.CelCompilerFactory; +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.expr.conformance.proto3.TestAllTypesCelDescriptor; +import dev.cel.parser.CelStandardMacro; +import dev.cel.testing.testdata.MessageWithEnum; +import dev.cel.testing.testdata.MessageWithEnumCelDescriptor; +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 java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Exercises tests for CelLiteRuntime using full version of protobuf messages. */ +@RunWith(TestParameterInjector.class) +public class CelLiteRuntimeTest { + 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")) + .build(); + + private static final CelLiteRuntime CEL_RUNTIME = + CelLiteRuntimeFactory.newLiteRuntimeBuilder() + .setStandardFunctions(CelStandardFunctions.ALL_STANDARD_FUNCTIONS) + .setValueProvider( + ProtoMessageLiteValueProvider.newInstance( + dev.cel.expr.conformance.proto2.TestAllTypesCelDescriptor.getDescriptor(), + TestAllTypesCelDescriptor.getDescriptor())) + .build(); + + @Test + public void messageCreation_emptyMessage() throws Exception { + CelAbstractSyntaxTree ast = CEL_COMPILER.compile("TestAllTypes{}").getAst(); + + TestAllTypes simpleTest = (TestAllTypes) CEL_RUNTIME.createProgram(ast).eval(); + + assertThat(simpleTest).isEqualToDefaultInstance(); + } + + @Test + public void messageCreation_fieldsPopulated() throws Exception { + CelAbstractSyntaxTree ast = CEL_COMPILER.compile("TestAllTypes{single_int32: 4}").getAst(); + + CelEvaluationException e = + assertThrows(CelEvaluationException.class, () -> CEL_RUNTIME.createProgram(ast).eval()); + + assertThat(e) + .hasMessageThat() + .contains("Message creation with prepopulated fields is not supported yet."); + } + + @Test + @TestParameters("{expression: 'msg.single_int32 == 1'}") + @TestParameters("{expression: 'msg.single_int64 == 2'}") + @TestParameters("{expression: 'msg.single_uint32 == 3u'}") + @TestParameters("{expression: 'msg.single_uint64 == 4u'}") + @TestParameters("{expression: 'msg.single_sint32 == 5'}") + @TestParameters("{expression: 'msg.single_sint64 == 6'}") + @TestParameters("{expression: 'msg.single_fixed32 == 7u'}") + @TestParameters("{expression: 'msg.single_fixed64 == 8u'}") + @TestParameters("{expression: 'msg.single_sfixed32 == 9'}") + @TestParameters("{expression: 'msg.single_sfixed64 == 10'}") + @TestParameters("{expression: 'msg.single_float == 1.5'}") + @TestParameters("{expression: 'msg.single_double == 2.5'}") + @TestParameters("{expression: 'msg.single_bool == true'}") + @TestParameters("{expression: 'msg.single_string == \"foo\"'}") + @TestParameters("{expression: 'msg.single_bytes == b\"abc\"'}") + @TestParameters("{expression: 'msg.optional_bool == true'}") + public void fieldSelection_literals(String expression) throws Exception { + CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expression).getAst(); + TestAllTypes msg = + TestAllTypes.newBuilder() + .setSingleInt32(1) + .setSingleInt64(2L) + .setSingleUint32(3) + .setSingleUint64(4L) + .setSingleSint32(5) + .setSingleSint64(6L) + .setSingleFixed32(7) + .setSingleFixed64(8L) + .setSingleSfixed32(9) + .setSingleSfixed64(10L) + .setSingleFloat(1.5f) + .setSingleDouble(2.5d) + .setSingleBool(true) + .setSingleString("foo") + .setSingleBytes(ByteString.copyFromUtf8("abc")) + .setOptionalBool(true) + .build(); + + boolean result = (boolean) CEL_RUNTIME.createProgram(ast).eval(ImmutableMap.of("msg", msg)); + + assertThat(result).isTrue(); + } + + @Test + @TestParameters("{expression: 'msg.single_uint32'}") + @TestParameters("{expression: 'msg.single_uint64'}") + public void fieldSelection_unsigned(String expression) throws Exception { + CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expression).getAst(); + TestAllTypes msg = TestAllTypes.newBuilder().setSingleUint32(4).setSingleUint64(4L).build(); + + Object result = CEL_RUNTIME.createProgram(ast).eval(ImmutableMap.of("msg", msg)); + + assertThat(result).isEqualTo(UnsignedLong.valueOf(4L)); + } + + @Test + @TestParameters("{expression: 'msg.repeated_int32'}") + @TestParameters("{expression: 'msg.repeated_int64'}") + @SuppressWarnings("unchecked") + public void fieldSelection_packedRepeatedInts(String expression) throws Exception { + // Note: non-LEN delimited primitives such as ints are packed by default in proto3 + CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expression).getAst(); + TestAllTypes msg = + TestAllTypes.newBuilder() + .addRepeatedInt32(1) + .addRepeatedInt32(2) + .addRepeatedInt64(1L) + .addRepeatedInt64(2L) + .build(); + + List result = + (List) CEL_RUNTIME.createProgram(ast).eval(ImmutableMap.of("msg", msg)); + + assertThat(result).containsExactly(1L, 2L).inOrder(); + } + + @Test + @SuppressWarnings("unchecked") + public void fieldSelection_repeatedStrings() throws Exception { + // Note: len-delimited fields, such as string and messages are not packed. + CelAbstractSyntaxTree ast = CEL_COMPILER.compile("msg.repeated_string").getAst(); + TestAllTypes msg = + TestAllTypes.newBuilder().addRepeatedString("hello").addRepeatedString("world").build(); + + List result = + (List) CEL_RUNTIME.createProgram(ast).eval(ImmutableMap.of("msg", msg)); + + assertThat(result).containsExactly("hello", "world").inOrder(); + } + + @Test + @SuppressWarnings("unchecked") + public void fieldSelection_repeatedBoolWrappers() throws Exception { + CelAbstractSyntaxTree ast = CEL_COMPILER.compile("msg.repeated_bool_wrapper").getAst(); + TestAllTypes msg = + TestAllTypes.newBuilder() + .addRepeatedBoolWrapper(BoolValue.of(true)) + .addRepeatedBoolWrapper(BoolValue.of(false)) + .addRepeatedBoolWrapper(BoolValue.of(true)) + .build(); + + List result = + (List) CEL_RUNTIME.createProgram(ast).eval(ImmutableMap.of("msg", msg)); + + assertThat(result).containsExactly(true, false, true).inOrder(); + } + + @Test + @TestParameters("{expression: 'msg.map_string_int32'}") + @TestParameters("{expression: 'msg.map_string_int64'}") + @SuppressWarnings("unchecked") + public void fieldSelection_map(String expression) throws Exception { + CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expression).getAst(); + TestAllTypes msg = + TestAllTypes.newBuilder() + .putMapStringInt32("a", 1) + .putMapStringInt32("b", 2) + .putMapStringInt64("a", 1L) + .putMapStringInt64("b", 2L) + .build(); + + Map result = + (Map) CEL_RUNTIME.createProgram(ast).eval(ImmutableMap.of("msg", msg)); + + assertThat(result).containsExactly("a", 1L, "b", 2L); + } + + @Test + @TestParameters("{expression: 'msg.single_int32_wrapper == 1'}") + @TestParameters("{expression: 'msg.single_int64_wrapper == 2'}") + @TestParameters("{expression: 'msg.single_uint32_wrapper == 3u'}") + @TestParameters("{expression: 'msg.single_uint64_wrapper == 4u'}") + @TestParameters("{expression: 'msg.single_float_wrapper == 1.5'}") + @TestParameters("{expression: 'msg.single_double_wrapper == 2.5'}") + @TestParameters("{expression: 'msg.single_bool_wrapper == true'}") + @TestParameters("{expression: 'msg.single_string_wrapper == \"foo\"'}") + @TestParameters("{expression: 'msg.single_bytes_wrapper == b\"abc\"'}") + public void fieldSelection_wrappers(String expression) throws Exception { + CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expression).getAst(); + TestAllTypes msg = + TestAllTypes.newBuilder() + .setSingleInt32Wrapper(Int32Value.of(1)) + .setSingleInt64Wrapper(Int64Value.of(2L)) + .setSingleUint32Wrapper(UInt32Value.of(3)) + .setSingleUint64Wrapper(UInt64Value.of(4L)) + .setSingleFloatWrapper(FloatValue.of(1.5f)) + .setSingleDoubleWrapper(DoubleValue.of(2.5d)) + .setSingleBoolWrapper(BoolValue.of(true)) + .setSingleStringWrapper(StringValue.of("foo")) + .setSingleBytesWrapper(BytesValue.of(ByteString.copyFromUtf8("abc"))) + .build(); + + boolean result = (boolean) CEL_RUNTIME.createProgram(ast).eval(ImmutableMap.of("msg", msg)); + + assertThat(result).isTrue(); + } + + @Test + @TestParameters("{expression: 'msg.single_int32_wrapper'}") + @TestParameters("{expression: 'msg.single_int64_wrapper'}") + @TestParameters("{expression: 'msg.single_uint32_wrapper'}") + @TestParameters("{expression: 'msg.single_uint64_wrapper'}") + @TestParameters("{expression: 'msg.single_float_wrapper'}") + @TestParameters("{expression: 'msg.single_double_wrapper'}") + @TestParameters("{expression: 'msg.single_bool_wrapper'}") + @TestParameters("{expression: 'msg.single_string_wrapper'}") + @TestParameters("{expression: 'msg.single_bytes_wrapper'}") + public void fieldSelection_wrappersNullability(String expression) throws Exception { + CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expression).getAst(); + TestAllTypes msg = TestAllTypes.getDefaultInstance(); + + Object result = CEL_RUNTIME.createProgram(ast).eval(ImmutableMap.of("msg", msg)); + + assertThat(result).isEqualTo(NullValue.NULL_VALUE); + } + + @Test + public void fieldSelection_duration() throws Exception { + String expression = "msg.single_duration"; + CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expression).getAst(); + TestAllTypes msg = + TestAllTypes.newBuilder() + .setSingleDuration(ProtoTimeUtils.fromSecondsToDuration(600)) + .build(); + + Duration result = (Duration) CEL_RUNTIME.createProgram(ast).eval(ImmutableMap.of("msg", msg)); + + assertThat(result).isEqualTo(Duration.ofMinutes(10)); + } + + @Test + public void fieldSelection_timestamp() throws Exception { + String expression = "msg.single_timestamp"; + CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expression).getAst(); + TestAllTypes msg = + TestAllTypes.newBuilder() + .setSingleTimestamp(ProtoTimeUtils.fromSecondsToTimestamp(50)) + .build(); + + Instant result = (Instant) CEL_RUNTIME.createProgram(ast).eval(ImmutableMap.of("msg", msg)); + + assertThat(result).isEqualTo(Instant.ofEpochSecond(50)); + } + + @Test + @SuppressWarnings("unchecked") + public void fieldSelection_jsonStruct() throws Exception { + String expression = "msg.single_struct"; + CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expression).getAst(); + TestAllTypes msg = + TestAllTypes.newBuilder() + .setSingleStruct( + Struct.newBuilder() + .putFields("one", Values.of(1)) + .putFields("two", Values.of(true))) + .build(); + + Map result = + (Map) CEL_RUNTIME.createProgram(ast).eval(ImmutableMap.of("msg", msg)); + + assertThat(result).containsExactly("one", 1.0d, "two", true).inOrder(); + } + + @Test + public void fieldSelection_jsonValue() throws Exception { + String expression = "msg.single_value"; + CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expression).getAst(); + TestAllTypes msg = TestAllTypes.newBuilder().setSingleValue(Values.of("foo")).build(); + + String result = (String) CEL_RUNTIME.createProgram(ast).eval(ImmutableMap.of("msg", msg)); + + assertThat(result).isEqualTo("foo"); + } + + @Test + @SuppressWarnings("unchecked") + public void fieldSelection_jsonListValue() throws Exception { + String expression = "msg.list_value"; + CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expression).getAst(); + TestAllTypes msg = + TestAllTypes.newBuilder() + .setListValue( + ListValue.newBuilder().addValues(Values.of(true)).addValues(Values.of("foo"))) + .build(); + + List result = + (List) CEL_RUNTIME.createProgram(ast).eval(ImmutableMap.of("msg", msg)); + + assertThat(result).containsExactly(true, "foo").inOrder(); + } + + @Test + @TestParameters("{expression: 'has(msg.single_int32)'}") + @TestParameters("{expression: 'has(msg.single_int64)'}") + @TestParameters("{expression: 'has(msg.single_int32_wrapper)'}") + @TestParameters("{expression: 'has(msg.single_int64_wrapper)'}") + @TestParameters("{expression: 'has(msg.repeated_int32)'}") + @TestParameters("{expression: 'has(msg.repeated_int64)'}") + @TestParameters("{expression: 'has(msg.repeated_int32_wrapper)'}") + @TestParameters("{expression: 'has(msg.repeated_int64_wrapper)'}") + @TestParameters("{expression: 'has(msg.map_string_int32)'}") + @TestParameters("{expression: 'has(msg.map_string_int64)'}") + @TestParameters("{expression: 'has(msg.map_bool_int32_wrapper)'}") + @TestParameters("{expression: 'has(msg.map_bool_int64_wrapper)'}") + public void presenceTest_proto2_evaluatesToFalse(String expression) throws Exception { + CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expression).getAst(); + dev.cel.expr.conformance.proto2.TestAllTypes msg = + dev.cel.expr.conformance.proto2.TestAllTypes.newBuilder() + .addAllRepeatedInt32(ImmutableList.of()) + .addAllRepeatedInt32Wrapper(ImmutableList.of()) + .putAllMapBoolInt32(ImmutableMap.of()) + .putAllMapBoolInt32Wrapper(ImmutableMap.of()) + .build(); + + boolean result = (boolean) CEL_RUNTIME.createProgram(ast).eval(ImmutableMap.of("msg", msg)); + + assertThat(result).isFalse(); + } + + @Test + @TestParameters("{expression: 'has(msg.single_int32)'}") + @TestParameters("{expression: 'has(msg.single_int64)'}") + @TestParameters("{expression: 'has(msg.single_int32_wrapper)'}") + @TestParameters("{expression: 'has(msg.single_int64_wrapper)'}") + @TestParameters("{expression: 'has(msg.repeated_int32)'}") + @TestParameters("{expression: 'has(msg.repeated_int64)'}") + @TestParameters("{expression: 'has(msg.repeated_int32_wrapper)'}") + @TestParameters("{expression: 'has(msg.repeated_int64_wrapper)'}") + @TestParameters("{expression: 'has(msg.map_string_int32)'}") + @TestParameters("{expression: 'has(msg.map_string_int64)'}") + @TestParameters("{expression: 'has(msg.map_string_int32_wrapper)'}") + @TestParameters("{expression: 'has(msg.map_string_int64_wrapper)'}") + public void presenceTest_proto2_evaluatesToTrue(String expression) throws Exception { + CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expression).getAst(); + dev.cel.expr.conformance.proto2.TestAllTypes msg = + dev.cel.expr.conformance.proto2.TestAllTypes.newBuilder() + .setSingleInt32(0) + .setSingleInt64(0) + .setSingleInt32Wrapper(Int32Value.of(0)) + .setSingleInt64Wrapper(Int64Value.of(0)) + .addAllRepeatedInt32(ImmutableList.of(1)) + .addAllRepeatedInt64(ImmutableList.of(2L)) + .addAllRepeatedInt32Wrapper(ImmutableList.of(Int32Value.of(0))) + .addAllRepeatedInt64Wrapper(ImmutableList.of(Int64Value.of(0L))) + .putAllMapStringInt32Wrapper(ImmutableMap.of("a", Int32Value.of(1))) + .putAllMapStringInt64Wrapper(ImmutableMap.of("b", Int64Value.of(2L))) + .putMapStringInt32("a", 1) + .putMapStringInt64("b", 2) + .build(); + + boolean result = (boolean) CEL_RUNTIME.createProgram(ast).eval(ImmutableMap.of("msg", msg)); + + assertThat(result).isTrue(); + } + + @Test + @TestParameters("{expression: 'has(msg.single_int32)'}") + @TestParameters("{expression: 'has(msg.single_int64)'}") + @TestParameters("{expression: 'has(msg.single_int32_wrapper)'}") + @TestParameters("{expression: 'has(msg.single_int64_wrapper)'}") + @TestParameters("{expression: 'has(msg.repeated_int32)'}") + @TestParameters("{expression: 'has(msg.repeated_int64)'}") + @TestParameters("{expression: 'has(msg.repeated_int32_wrapper)'}") + @TestParameters("{expression: 'has(msg.repeated_int64_wrapper)'}") + @TestParameters("{expression: 'has(msg.map_string_int32)'}") + @TestParameters("{expression: 'has(msg.map_string_int64)'}") + @TestParameters("{expression: 'has(msg.map_bool_int32_wrapper)'}") + @TestParameters("{expression: 'has(msg.map_bool_int64_wrapper)'}") + public void presenceTest_proto3_evaluatesToFalse(String expression) throws Exception { + CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expression).getAst(); + TestAllTypes msg = + TestAllTypes.newBuilder() + .setSingleInt32(0) + .addAllRepeatedInt32(ImmutableList.of()) + .addAllRepeatedInt32Wrapper(ImmutableList.of()) + .putAllMapBoolInt32(ImmutableMap.of()) + .putAllMapBoolInt32Wrapper(ImmutableMap.of()) + .build(); + + boolean result = (boolean) CEL_RUNTIME.createProgram(ast).eval(ImmutableMap.of("msg", msg)); + + assertThat(result).isFalse(); + } + + @Test + @TestParameters("{expression: 'has(msg.single_int32)'}") + @TestParameters("{expression: 'has(msg.single_int64)'}") + @TestParameters("{expression: 'has(msg.single_int32_wrapper)'}") + @TestParameters("{expression: 'has(msg.single_int64_wrapper)'}") + @TestParameters("{expression: 'has(msg.repeated_int32)'}") + @TestParameters("{expression: 'has(msg.repeated_int64)'}") + @TestParameters("{expression: 'has(msg.repeated_int32_wrapper)'}") + @TestParameters("{expression: 'has(msg.repeated_int64_wrapper)'}") + @TestParameters("{expression: 'has(msg.map_string_int32)'}") + @TestParameters("{expression: 'has(msg.map_string_int64)'}") + @TestParameters("{expression: 'has(msg.map_string_int32_wrapper)'}") + @TestParameters("{expression: 'has(msg.map_string_int64_wrapper)'}") + public void presenceTest_proto3_evaluatesToTrue(String expression) throws Exception { + CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expression).getAst(); + TestAllTypes msg = + TestAllTypes.newBuilder() + .setSingleInt32(1) + .setSingleInt64(2) + .setSingleInt32Wrapper(Int32Value.of(0)) + .setSingleInt64Wrapper(Int64Value.of(0)) + .addAllRepeatedInt32(ImmutableList.of(1)) + .addAllRepeatedInt64(ImmutableList.of(2L)) + .addAllRepeatedInt32Wrapper(ImmutableList.of(Int32Value.of(0))) + .addAllRepeatedInt64Wrapper(ImmutableList.of(Int64Value.of(0L))) + .putAllMapStringInt32Wrapper(ImmutableMap.of("a", Int32Value.of(1))) + .putAllMapStringInt64Wrapper(ImmutableMap.of("b", Int64Value.of(2L))) + .putMapStringInt32("a", 1) + .putMapStringInt64("b", 2) + .build(); + + boolean result = (boolean) CEL_RUNTIME.createProgram(ast).eval(ImmutableMap.of("msg", msg)); + + assertThat(result).isTrue(); + } + + @Test + public void nestedMessage_traversalThroughSetField() throws Exception { + CelAbstractSyntaxTree ast = + CEL_COMPILER + .compile("msg.single_nested_message.bb == 43 && has(msg.single_nested_message)") + .getAst(); + TestAllTypes nestedMessage = + TestAllTypes.newBuilder() + .setSingleNestedMessage(NestedMessage.newBuilder().setBb(43)) + .build(); + + boolean result = + (boolean) CEL_RUNTIME.createProgram(ast).eval(ImmutableMap.of("msg", nestedMessage)); + + assertThat(result).isTrue(); + } + + @Test + public void nestedMessage_safeTraversal() throws Exception { + CelAbstractSyntaxTree ast = CEL_COMPILER.compile("msg.single_nested_message.bb == 43").getAst(); + TestAllTypes nestedMessage = + TestAllTypes.newBuilder() + .setSingleNestedMessage(NestedMessage.getDefaultInstance()) + .build(); + + boolean result = + (boolean) CEL_RUNTIME.createProgram(ast).eval(ImmutableMap.of("msg", nestedMessage)); + + assertThat(result).isFalse(); + } + + @Test + public void enumSelection() throws Exception { + CelAbstractSyntaxTree ast = CEL_COMPILER.compile("msg.single_nested_enum").getAst(); + TestAllTypes nestedMessage = + TestAllTypes.newBuilder().setSingleNestedEnum(NestedEnum.BAR).build(); + Long result = (Long) CEL_RUNTIME.createProgram(ast).eval(ImmutableMap.of("msg", nestedMessage)); + + assertThat(result).isEqualTo(NestedEnum.BAR.getNumber()); + } + + @SuppressWarnings("ImmutableEnumChecker") // Test only + private enum DefaultValueTestCase { + INT32("msg.single_int32", 0L), + INT64("msg.single_int64", 0L), + UINT32("msg.single_uint32", UnsignedLong.ZERO), + UINT64("msg.single_uint64", UnsignedLong.ZERO), + SINT32("msg.single_sint32", 0L), + SINT64("msg.single_sint64", 0L), + FIXED32("msg.single_fixed32", 0L), + FIXED64("msg.single_fixed64", 0L), + SFIXED32("msg.single_sfixed32", 0L), + SFIXED64("msg.single_sfixed64", 0L), + FLOAT("msg.single_float", 0.0d), + DOUBLE("msg.single_double", 0.0d), + BOOL("msg.single_bool", false), + STRING("msg.single_string", ""), + BYTES("msg.single_bytes", CelByteString.EMPTY), + ENUM("msg.standalone_enum", 0L), + NESTED_MESSAGE("msg.single_nested_message", NestedMessage.getDefaultInstance()), + OPTIONAL_BOOL("msg.optional_bool", false), + REPEATED_STRING("msg.repeated_string", Collections.unmodifiableList(new ArrayList<>())), + MAP_INT32_BOOL("msg.map_int32_bool", Collections.unmodifiableMap(new HashMap<>())), + ; + + private final String expression; + private final Object expectedValue; + + DefaultValueTestCase(String expression, Object expectedValue) { + this.expression = expression; + this.expectedValue = expectedValue; + } + } + + @Test + public void unsetField_defaultValue(@TestParameter DefaultValueTestCase testCase) + throws Exception { + CelAbstractSyntaxTree ast = CEL_COMPILER.compile(testCase.expression).getAst(); + + Object result = + CEL_RUNTIME + .createProgram(ast) + .eval(ImmutableMap.of("msg", TestAllTypes.getDefaultInstance())); + + assertThat(result).isEqualTo(testCase.expectedValue); + } + + @Test + public void nestedMessage_fromImportedProto() throws Exception { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addVar( + "multiFile", StructTypeReference.create(MultiFile.getDescriptor().getFullName())) + .addMessageTypes(MultiFile.getDescriptor()) + .build(); + CelLiteRuntime celRuntime = + CelLiteRuntimeFactory.newLiteRuntimeBuilder() + .setStandardFunctions(CelStandardFunctions.ALL_STANDARD_FUNCTIONS) + .setValueProvider( + ProtoMessageLiteValueProvider.newInstance( + SingleFileCelDescriptor.getDescriptor(), + MultiFileCelDescriptor.getDescriptor())) + .build(); + + CelAbstractSyntaxTree ast = celCompiler.compile("multiFile.nested_single_file.name").getAst(); + + String result = + (String) + celRuntime + .createProgram(ast) + .eval( + ImmutableMap.of( + "multiFile", + MultiFile.newBuilder() + .setNestedSingleFile(SingleFile.newBuilder().setName("foo").build()))); + + assertThat(result).isEqualTo("foo"); + } + + @Test + public void eval_withLateBoundFunction() throws Exception { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "lateBoundFunc", + CelOverloadDecl.newGlobalOverload( + "lateBoundFunc_string", SimpleType.STRING, SimpleType.STRING))) + .build(); + CelLiteRuntime celRuntime = CelLiteRuntimeFactory.newLiteRuntimeBuilder().build(); + CelAbstractSyntaxTree ast = celCompiler.compile("lateBoundFunc('hello')").getAst(); + + String result = + (String) + celRuntime + .createProgram(ast) + .eval( + ImmutableMap.of(), + CelLateFunctionBindings.from( + CelFunctionBinding.from( + "lateBoundFunc_string", String.class, arg -> arg + " world"))); + + assertThat(result).isEqualTo("hello world"); + } + + @Test + public void eval_dynFunctionReturnsProto() throws Exception { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "func", CelOverloadDecl.newGlobalOverload("func_identity", SimpleType.DYN))) + .build(); + CelLiteRuntime celRuntime = + CelLiteRuntimeFactory.newLiteRuntimeBuilder() + .setValueProvider( + ProtoMessageLiteValueProvider.newInstance( + TestAllTypesCelDescriptor.getDescriptor())) + .addFunctionBindings( + CelFunctionBinding.from( + "func_identity", + ImmutableList.of(), + unused -> TestAllTypes.getDefaultInstance())) + .build(); + + CelAbstractSyntaxTree ast = celCompiler.compile("func()").getAst(); + + TestAllTypes result = (TestAllTypes) celRuntime.createProgram(ast).eval(); + + assertThat(result).isEqualToDefaultInstance(); + } + + @Test + public void eval_withEnumField() throws Exception { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addVar( + "msg", StructTypeReference.create(MessageWithEnum.getDescriptor().getFullName())) + .addMessageTypes(MessageWithEnum.getDescriptor()) + .build(); + CelLiteRuntime celLiteRuntime = + CelLiteRuntimeFactory.newLiteRuntimeBuilder() + .setStandardFunctions(CelStandardFunctions.ALL_STANDARD_FUNCTIONS) + .setValueProvider( + ProtoMessageLiteValueProvider.newInstance( + MessageWithEnumCelDescriptor.getDescriptor())) + .build(); + CelAbstractSyntaxTree ast = celCompiler.compile("msg.simple_enum").getAst(); + + Long result = + (Long) + celLiteRuntime + .createProgram(ast) + .eval( + ImmutableMap.of( + "msg", MessageWithEnum.newBuilder().setSimpleEnum(SimpleEnum.BAR))); + + 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 new file mode 100644 index 000000000..471282117 --- /dev/null +++ b/runtime/src/test/java/dev/cel/runtime/CelResolvedOverloadTest.java @@ -0,0 +1,114 @@ +// 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. + +package dev.cel.runtime; + +import static com.google.common.truth.Truth.assertThat; + +import dev.cel.expr.conformance.proto3.TestAllTypes; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link CelResolvedOverload}. */ +@RunWith(JUnit4.class) +public final class CelResolvedOverloadTest { + + CelResolvedOverload getIncrementIntOverload() { + return CelResolvedOverload.of( + /* functionName= */ "increment_int", + /* overloadId= */ "increment_int_overload", + (CelFunctionOverload) + (args) -> { + Long arg = (Long) args[0]; + return arg + 1; + }, + /* isStrict= */ true, + Long.class); + } + + @Test + public void canHandle_matchingTypes_returnsTrue() { + assertThat(getIncrementIntOverload().canHandle(new Object[] {1L})).isTrue(); + } + + @Test + public void canHandle_nullMessageType_returnsFalse() { + CelResolvedOverload overload = + CelResolvedOverload.of( + /* 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( + /* functionName= */ "identity", + /* overloadId= */ "identity_overload", + (CelFunctionOverload) (args) -> args[0], + /* isStrict= */ true, + Long.class); + assertThat(overload.canHandle(new Object[] {null})).isFalse(); + } + + @Test + public void canHandle_nonMatchingTypes_returnsFalse() { + assertThat(getIncrementIntOverload().canHandle(new Object[] {1.0})).isFalse(); + } + + @Test + public void canHandle_nonMatchingArgCount_returnsFalse() { + assertThat(getIncrementIntOverload().canHandle(new Object[] {1L, 2L})).isFalse(); + } + + @Test + public void canHandle_nonStrictOverload_returnsTrue() { + CelResolvedOverload nonStrictOverload = + CelResolvedOverload.of( + /* functionName= */ "non_strict", + /* overloadId= */ "non_strict_overload", + (CelFunctionOverload) + (args) -> { + return false; + }, + /* isStrict= */ false, + Long.class, + Long.class); + assertThat( + nonStrictOverload.canHandle( + new Object[] {new RuntimeException(), CelUnknownSet.create()})) + .isTrue(); + } + + @Test + public void canHandle_nonStrictOverload_returnsFalse() { + CelResolvedOverload nonStrictOverload = + CelResolvedOverload.of( + /* functionName= */ "non_strict", + /* overloadId= */ "non_strict_overload", + (CelFunctionOverload) + (args) -> { + return false; + }, + /* isStrict= */ false, + Long.class, + Long.class); + assertThat(nonStrictOverload.canHandle(new Object[] {new RuntimeException(), "Foo"})).isFalse(); + } +} diff --git a/runtime/src/test/java/dev/cel/runtime/CelRuntimeLegacyImplTest.java b/runtime/src/test/java/dev/cel/runtime/CelRuntimeLegacyImplTest.java index 2ce5e4d0b..fa3b5f4ae 100644 --- a/runtime/src/test/java/dev/cel/runtime/CelRuntimeLegacyImplTest.java +++ b/runtime/src/test/java/dev/cel/runtime/CelRuntimeLegacyImplTest.java @@ -16,11 +16,16 @@ import static com.google.common.truth.Truth.assertThat; +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.runtime.CelRuntime.CelFunctionBinding; -import dev.cel.testing.testdata.proto3.TestAllTypesProto.TestAllTypes; +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; @@ -35,7 +40,7 @@ public void evalException() throws CelException { CelRuntime runtime = CelRuntimeFactory.standardCelRuntimeBuilder().build(); CelRuntime.Program program = runtime.createProgram(compiler.compile("1/0").getAst()); CelEvaluationException e = Assert.assertThrows(CelEvaluationException.class, program::eval); - assertThat(e).hasCauseThat().isInstanceOf(ArithmeticException.class); + assertThat(e).hasCauseThat().isInstanceOf(CelDivideByZeroException.class); } @Test @@ -58,7 +63,7 @@ public void toRuntimeBuilder_isImmutable() { CelRuntimeLegacyImpl.Builder newRuntimeBuilder = (CelRuntimeLegacyImpl.Builder) celRuntime.toRuntimeBuilder(); - assertThat(newRuntimeBuilder.getRuntimeLibraries().build()).isEmpty(); + assertThat(newRuntimeBuilder.celRuntimeLibraries.build()).isEmpty(); } @Test @@ -68,14 +73,16 @@ public void toRuntimeBuilder_collectionProperties_copied() { celRuntimeBuilder.addFileTypes(TestAllTypes.getDescriptor().getFile()); celRuntimeBuilder.addFunctionBindings(CelFunctionBinding.from("test", Integer.class, arg -> 1)); celRuntimeBuilder.addLibraries(runtimeBuilder -> {}); + int originalFileTypesSize = + ((CelRuntimeLegacyImpl.Builder) celRuntimeBuilder).fileTypes.build().size(); CelRuntimeLegacyImpl celRuntime = (CelRuntimeLegacyImpl) celRuntimeBuilder.build(); CelRuntimeLegacyImpl.Builder newRuntimeBuilder = (CelRuntimeLegacyImpl.Builder) celRuntime.toRuntimeBuilder(); - assertThat(newRuntimeBuilder.getFunctionBindings()).hasSize(1); - assertThat(newRuntimeBuilder.getRuntimeLibraries().build()).hasSize(1); - assertThat(newRuntimeBuilder.getFileTypes().build()).hasSize(6); + assertThat(newRuntimeBuilder.customFunctionBindings).hasSize(1); + assertThat(newRuntimeBuilder.celRuntimeLibraries.build()).hasSize(1); + assertThat(newRuntimeBuilder.fileTypes.build()).hasSize(originalFileTypesSize); } @Test @@ -91,8 +98,31 @@ public void toRuntimeBuilder_collectionProperties_areImmutable() { celRuntimeBuilder.addFunctionBindings(CelFunctionBinding.from("test", Integer.class, arg -> 1)); celRuntimeBuilder.addLibraries(runtimeBuilder -> {}); - assertThat(newRuntimeBuilder.getFunctionBindings()).isEmpty(); - assertThat(newRuntimeBuilder.getRuntimeLibraries().build()).isEmpty(); - assertThat(newRuntimeBuilder.getFileTypes().build()).isEmpty(); + assertThat(newRuntimeBuilder.customFunctionBindings).isEmpty(); + assertThat(newRuntimeBuilder.celRuntimeLibraries.build()).isEmpty(); + assertThat(newRuntimeBuilder.fileTypes.build()).isEmpty(); + } + + @Test + 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); + CelRuntime celRuntime = celRuntimeBuilder.build(); + + CelRuntimeLegacyImpl.Builder newRuntimeBuilder = + (CelRuntimeLegacyImpl.Builder) celRuntime.toRuntimeBuilder(); + + assertThat(newRuntimeBuilder.customTypeFactory).isEqualTo(customTypeFactory); + assertThat(newRuntimeBuilder.overriddenStandardFunctions) + .isEqualTo(overriddenStandardFunctions); + assertThat(newRuntimeBuilder.celValueProvider).isEqualTo(noOpValueProvider); } } diff --git a/runtime/src/test/java/dev/cel/runtime/CelRuntimeTest.java b/runtime/src/test/java/dev/cel/runtime/CelRuntimeTest.java index 61794aea0..13d5dd550 100644 --- a/runtime/src/test/java/dev/cel/runtime/CelRuntimeTest.java +++ b/runtime/src/test/java/dev/cel/runtime/CelRuntimeTest.java @@ -2,7 +2,7 @@ // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. -// You may obtain a copy of the License aj +// You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // @@ -15,10 +15,13 @@ package dev.cel.runtime; 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; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.protobuf.Any; import com.google.protobuf.BoolValue; @@ -26,34 +29,45 @@ import com.google.protobuf.DescriptorProtos.FileDescriptorSet; import com.google.protobuf.DynamicMessage; 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.bundle.Cel; 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; import dev.cel.common.ast.CelExpr; import dev.cel.common.ast.CelExpr.ExprKind.Kind; import dev.cel.common.types.CelV1AlphaTypes; +import dev.cel.common.types.ListType; 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.extensions.CelExtensions; import dev.cel.parser.CelStandardMacro; import dev.cel.parser.CelUnparserFactory; -import dev.cel.testing.testdata.proto3.TestAllTypesProto.TestAllTypes; +import dev.cel.testing.CelRuntimeFlavor; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.atomic.AtomicReference; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; -@RunWith(JUnit4.class) +@RunWith(TestParameterInjector.class) public class CelRuntimeTest { + @TestParameter private CelRuntimeFlavor runtimeFlavor; + @Test public void evaluate_anyPackedEqualityUsingProtoDifferencer_success() throws Exception { Cel cel = @@ -93,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) @@ -112,6 +126,50 @@ public void evaluate_v1alpha1CheckedExpr() throws Exception { assertThat(evaluatedResult).isEqualTo("Hello world!"); } + @Test + // Lazy evaluation result cache doesn't allow references to mutate the cached instance. + @TestParameters( + "{expression: 'cel.bind(x, unknown_attr, (unknown_attr > 0) || [0, 1, 2, 3, 4, 5, 6, 7, 8, 9," + + " 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20].exists(i, x + x > 0))'}") + @TestParameters( + "{expression: 'cel.bind(x, unknown_attr, x + x + x + x + x + x + x + x + x + x + x + x + x +" + + " x + x + x + x + x + x + x + x + x + x + x + x + x + x + x + x + x + x + x + x)'}") + // A new unknown is created per 'x' reference. + @TestParameters( + "{expression: '(my_list.exists(x, (x + x + x + x + x + x + x + x + x + x + x + x + x + x + x" + + " + x + x + x + x + x + x + x + x + x + x + x + x + x + x + x + x + x + x) > 100) &&" + + " false) || unknown_attr > 0'}") + public void advanceEvaluation_withUnknownTracking_noSelfReferenceInMerge(String expression) + throws Exception { + Cel cel = + CelFactory.standardCelBuilder() + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addCompilerLibraries(CelExtensions.bindings()) + .setContainer(CelContainer.ofName("cel.expr.conformance.test")) + .addVar("unknown_attr", SimpleType.INT) + .addVar("my_list", ListType.create(SimpleType.INT)) + .setOptions(CelOptions.current().enableUnknownTracking(true).build()) + .build(); + + CelUnknownSet result = + (CelUnknownSet) + cel.createProgram(cel.compile(expression).getAst()) + .advanceEvaluation( + UnknownContext.create( + (String name) -> { + if (name.equals("my_list")) { + return Optional.of(ImmutableList.of(1)); + } + return Optional.empty(); + }, + ImmutableList.of( + CelAttributePattern.create("unknown_attr"), + CelAttributePattern.create("my_list") + .qualify(CelAttribute.Qualifier.ofInt(0))))); + + assertThat(result.attributes()).containsExactly(CelAttribute.create("unknown_attr")); + } + @Test public void newWellKnownTypeMessage_withDifferentDescriptorInstance() throws Exception { CelCompiler celCompiler = @@ -120,6 +178,7 @@ public void newWellKnownTypeMessage_withDifferentDescriptorInstance() throws Exc .build(); CelRuntime celRuntime = CelRuntimeFactory.standardCelRuntimeBuilder() + // CEL-Internal-2 .addFileTypes( FileDescriptorSet.newBuilder() .addFile( @@ -141,6 +200,7 @@ public void newWellKnownTypeMessage_inDynamicMessage_withSetTypeFactory() throws .build(); CelRuntime celRuntime = CelRuntimeFactory.standardCelRuntimeBuilder() + // CEL-Internal-2 .setTypeFactory( (typeName) -> typeName.equals("google.protobuf.BoolValue") @@ -167,6 +227,8 @@ public void newWellKnownTypeMessage_inAnyMessage_withDifferentDescriptorInstance CelCompilerFactory.standardCelCompilerBuilder().addFileTypes(fds).build(); CelRuntime celRuntime = CelRuntimeFactory.standardCelRuntimeBuilder() + // CEL-Internal-2 + .addFileTypes(fds) .build(); CelAbstractSyntaxTree ast = @@ -190,6 +252,8 @@ public void newWellKnownTypeMessage_inAnyMessage_withSetTypeFactory() throws Exc CelCompilerFactory.standardCelCompilerBuilder().addFileTypes(fds).build(); CelRuntime celRuntime = CelRuntimeFactory.standardCelRuntimeBuilder() + // CEL-Internal-2 + .addFileTypes(fds) .setTypeFactory( (typeName) -> typeName.equals("google.protobuf.Any") @@ -216,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) @@ -240,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); @@ -255,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); @@ -273,9 +338,10 @@ public void trace_select() throws Exception { } }; Cel cel = - CelFactory.standardCelBuilder() + runtimeFlavor + .builder() .addMessageTypes(TestAllTypes.getDescriptor()) - .setContainer("dev.cel.testing.testdata.proto3") + .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) .build(); CelAbstractSyntaxTree ast = cel.compile("TestAllTypes{single_int64: 3}.single_int64").getAst(); @@ -285,16 +351,18 @@ public void trace_select() throws Exception { } @Test - public void trace_createStruct() throws Exception { + public void trace_struct() throws Exception { CelEvaluationListener listener = (expr, res) -> { assertThat(res).isEqualTo(TestAllTypes.getDefaultInstance()); - assertThat(expr.createStruct().messageName()).isEqualTo("TestAllTypes"); + assertThat(expr.struct().messageName()) + .isEqualTo("cel.expr.conformance.proto3.TestAllTypes"); }; Cel cel = - CelFactory.standardCelBuilder() + runtimeFlavor + .builder() .addMessageTypes(TestAllTypes.getDescriptor()) - .setContainer("dev.cel.testing.testdata.proto3") + .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) .build(); CelAbstractSyntaxTree ast = cel.compile("TestAllTypes{}").getAst(); @@ -305,15 +373,15 @@ public void trace_createStruct() throws Exception { @Test @SuppressWarnings("unchecked") // Test only - public void trace_createList() throws Exception { + public void trace_list() throws Exception { CelEvaluationListener listener = (expr, res) -> { - if (expr.exprKind().getKind().equals(Kind.CREATE_LIST)) { + if (expr.exprKind().getKind().equals(Kind.LIST)) { assertThat((List) res).containsExactly(1L, 2L, 3L); - assertThat(expr.createList().elements()).hasSize(3); + 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); @@ -323,15 +391,15 @@ public void trace_createList() throws Exception { @Test @SuppressWarnings("unchecked") // Test only - public void trace_createMap() throws Exception { + public void trace_map() throws Exception { CelEvaluationListener listener = (expr, res) -> { - if (expr.exprKind().getKind().equals(Kind.CREATE_MAP)) { + if (expr.exprKind().getKind().equals(Kind.MAP)) { assertThat((Map) res).containsExactly(1L, "a"); - assertThat(expr.createMap().entries()).hasSize(1); + 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); @@ -347,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); @@ -364,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(); @@ -386,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(); @@ -398,4 +467,481 @@ public void trace_withVariableResolver() throws Exception { assertThat(result).isEqualTo("hello"); } + + @Test + public void trace_shortCircuitingDisabled_logicalAndAllBranchesVisited( + @TestParameter boolean first, @TestParameter boolean second, @TestParameter boolean third) + throws Exception { + String expression = String.format("%s && %s && %s", first, second, third); + ImmutableList.Builder branchResults = ImmutableList.builder(); + CelEvaluationListener listener = + (expr, res) -> { + if (expr.constantOrDefault().getKind().equals(CelConstant.Kind.BOOLEAN_VALUE)) { + branchResults.add((Boolean) res); + } + }; + Cel celWithShortCircuit = + runtimeFlavor + .builder() + .setOptions( + CelOptions.current() + .enableShortCircuiting(true) + .enableHeterogeneousNumericComparisons(true) + .build()) + .build(); + Cel cel = + runtimeFlavor + .builder() + .setOptions( + CelOptions.current() + .enableShortCircuiting(false) + .enableHeterogeneousNumericComparisons(true) + .build()) + .build(); + CelAbstractSyntaxTree ast = cel.compile(expression).getAst(); + + boolean result = (boolean) cel.createProgram(ast).trace(listener); + boolean shortCircuitedResult = + (boolean) + celWithShortCircuit + .createProgram(celWithShortCircuit.compile(expression).getAst()) + .eval(); + + assertThat(result).isEqualTo(shortCircuitedResult); + assertThat(branchResults.build()).containsExactly(first, second, third).inOrder(); + } + + @Test + @TestParameters("{source: 'false && false && x'}") + @TestParameters("{source: 'false && x && false'}") + @TestParameters("{source: 'x && false && false'}") + public void trace_shortCircuitingDisabledWithUnknownsAndedToFalse_returnsFalse(String source) + throws Exception { + ImmutableList.Builder branchResults = ImmutableList.builder(); + CelEvaluationListener listener = + (expr, res) -> { + if (expr.constantOrDefault().getKind().equals(CelConstant.Kind.BOOLEAN_VALUE) + || expr.identOrDefault().name().equals("x")) { + if (InterpreterUtil.isUnknown(res)) { + branchResults.add("x"); // Swap unknown result with a sentinel value for testing + } else { + branchResults.add(res); + } + } + }; + Cel cel = + runtimeFlavor + .builder() + .addVar("x", SimpleType.BOOL) + .setOptions( + CelOptions.current() + .enableShortCircuiting(false) + .enableHeterogeneousNumericComparisons(true) + .build()) + .build(); + CelAbstractSyntaxTree ast = cel.compile(source).getAst(); + + 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"); + } + + @Test + @TestParameters("{source: 'true && true && x'}") + @TestParameters("{source: 'true && x && true'}") + @TestParameters("{source: 'x && true && true'}") + public void trace_shortCircuitingDisabledWithUnknownAndedToTrue_returnsUnknown(String source) + throws Exception { + ImmutableList.Builder branchResults = ImmutableList.builder(); + CelEvaluationListener listener = + (expr, res) -> { + if (expr.constantOrDefault().getKind().equals(CelConstant.Kind.BOOLEAN_VALUE) + || expr.identOrDefault().name().equals("x")) { + branchResults.add(res); + } + }; + Cel cel = + runtimeFlavor + .builder() + .addVar("x", SimpleType.BOOL) + .setOptions( + CelOptions.current() + .enableShortCircuiting(false) + .enableHeterogeneousNumericComparisons(true) + .build()) + .build(); + CelAbstractSyntaxTree ast = cel.compile(source).getAst(); + + PartialVars partialVars = PartialVars.of(CelAttributePattern.create("x")); + Object unknownResult = cel.createProgram(ast).trace(partialVars, listener); + + assertThat(InterpreterUtil.isUnknown(unknownResult)).isTrue(); + assertThat(branchResults.build()).containsExactly(true, true, unknownResult); + } + + @Test + public void trace_shortCircuitingDisabled_logicalOrAllBranchesVisited( + @TestParameter boolean first, @TestParameter boolean second, @TestParameter boolean third) + throws Exception { + String expression = String.format("%s || %s || %s", first, second, third); + ImmutableList.Builder branchResults = ImmutableList.builder(); + CelEvaluationListener listener = + (expr, res) -> { + if (expr.constantOrDefault().getKind().equals(CelConstant.Kind.BOOLEAN_VALUE)) { + branchResults.add((Boolean) res); + } + }; + Cel celWithShortCircuit = + runtimeFlavor + .builder() + .setOptions( + CelOptions.current() + .enableShortCircuiting(true) + .enableHeterogeneousNumericComparisons(true) + .build()) + .build(); + Cel cel = + runtimeFlavor + .builder() + .setOptions( + CelOptions.current() + .enableShortCircuiting(false) + .enableHeterogeneousNumericComparisons(true) + .build()) + .build(); + CelAbstractSyntaxTree ast = cel.compile(expression).getAst(); + + boolean result = (boolean) cel.createProgram(ast).trace(listener); + boolean shortCircuitedResult = + (boolean) + celWithShortCircuit + .createProgram(celWithShortCircuit.compile(expression).getAst()) + .eval(); + + assertThat(result).isEqualTo(shortCircuitedResult); + assertThat(branchResults.build()).containsExactly(first, second, third).inOrder(); + } + + @Test + @TestParameters("{source: 'false || false || x'}") + @TestParameters("{source: 'false || x || false'}") + @TestParameters("{source: 'x || false || false'}") + public void trace_shortCircuitingDisabledWithUnknownsOredToFalse_returnsUnknown(String source) + throws Exception { + ImmutableList.Builder branchResults = ImmutableList.builder(); + CelEvaluationListener listener = + (expr, res) -> { + if (expr.constantOrDefault().getKind().equals(CelConstant.Kind.BOOLEAN_VALUE) + || expr.identOrDefault().name().equals("x")) { + branchResults.add(res); + } + }; + Cel cel = + runtimeFlavor + .builder() + .addVar("x", SimpleType.BOOL) + .setOptions( + CelOptions.current() + .enableShortCircuiting(false) + .enableHeterogeneousNumericComparisons(true) + .build()) + .build(); + CelAbstractSyntaxTree ast = cel.compile(source).getAst(); + + PartialVars partialVars = PartialVars.of(CelAttributePattern.create("x")); + Object unknownResult = cel.createProgram(ast).trace(partialVars, listener); + + assertThat(InterpreterUtil.isUnknown(unknownResult)).isTrue(); + assertThat(branchResults.build()).containsExactly(false, false, unknownResult); + } + + @Test + @TestParameters("{source: 'true || true || x'}") + @TestParameters("{source: 'true || x || true'}") + @TestParameters("{source: 'x || true || true'}") + public void trace_shortCircuitingDisabledWithUnknownOredToTrue_returnsTrue(String source) + throws Exception { + ImmutableList.Builder branchResults = ImmutableList.builder(); + CelEvaluationListener listener = + (expr, res) -> { + if (expr.constantOrDefault().getKind().equals(CelConstant.Kind.BOOLEAN_VALUE) + || expr.identOrDefault().name().equals("x")) { + if (InterpreterUtil.isUnknown(res)) { + branchResults.add("x"); // Swap unknown result with a sentinel value for testing + } else { + branchResults.add(res); + } + } + }; + Cel cel = + runtimeFlavor + .builder() + .addVar("x", SimpleType.BOOL) + .setOptions( + CelOptions.current() + .enableShortCircuiting(false) + .enableHeterogeneousNumericComparisons(true) + .build()) + .build(); + CelAbstractSyntaxTree ast = cel.compile(source).getAst(); + + 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"); + } + + @Test + public void trace_shortCircuitingDisabled_ternaryAllBranchesVisited() throws Exception { + ImmutableList.Builder branchResults = ImmutableList.builder(); + CelEvaluationListener listener = + (expr, res) -> { + if (expr.constantOrDefault().getKind().equals(CelConstant.Kind.BOOLEAN_VALUE)) { + branchResults.add((Boolean) res); + } + }; + Cel cel = + runtimeFlavor + .builder() + .setOptions( + CelOptions.current() + .enableShortCircuiting(false) + .enableHeterogeneousNumericComparisons(true) + .build()) + .build(); + CelAbstractSyntaxTree ast = cel.compile("true ? false : true").getAst(); + + boolean result = (boolean) cel.createProgram(ast).trace(listener); + + assertThat(result).isFalse(); + assertThat(branchResults.build()).containsExactly(true, false, true); + } + + @Test + @TestParameters("{source: 'false ? true : x'}") + @TestParameters("{source: 'true ? x : false'}") + @TestParameters("{source: 'x ? true : false'}") + public void trace_shortCircuitingDisabled_ternaryWithUnknowns(String source) throws Exception { + ImmutableList.Builder branchResults = ImmutableList.builder(); + CelEvaluationListener listener = + (expr, res) -> { + if (expr.constantOrDefault().getKind().equals(CelConstant.Kind.BOOLEAN_VALUE) + || expr.identOrDefault().name().equals("x")) { + branchResults.add(res); + } + }; + Cel cel = + runtimeFlavor + .builder() + .addVar("x", SimpleType.BOOL) + .setOptions( + CelOptions.current() + .enableShortCircuiting(false) + .enableHeterogeneousNumericComparisons(true) + .build()) + .build(); + CelAbstractSyntaxTree ast = cel.compile(source).getAst(); + + PartialVars partialVars = PartialVars.of(CelAttributePattern.create("x")); + Object unknownResult = cel.createProgram(ast).trace(partialVars, listener); + + assertThat(InterpreterUtil.isUnknown(unknownResult)).isTrue(); + assertThat(branchResults.build()).containsExactly(false, unknownResult, true); + } + + @Test + @TestParameters( + "{expression: 'false ? (1 / 0) > 2 : false', firstVisited: false, secondVisited: false}") + @TestParameters( + "{expression: 'false ? (1 / 0) > 2 : true', firstVisited: false, secondVisited: true}") + @TestParameters( + "{expression: 'true ? false : (1 / 0) > 2', firstVisited: true, secondVisited: false}") + @TestParameters( + "{expression: 'true ? true : (1 / 0) > 2', firstVisited: true, secondVisited: true}") + public void trace_shortCircuitingDisabled_ternaryWithError( + String expression, boolean firstVisited, boolean secondVisited) throws Exception { + ImmutableList.Builder branchResults = ImmutableList.builder(); + CelEvaluationListener listener = + (expr, res) -> { + if (expr.constantOrDefault().getKind().equals(CelConstant.Kind.BOOLEAN_VALUE)) { + branchResults.add(res); + } + }; + Cel celWithShortCircuit = + runtimeFlavor + .builder() + .setOptions( + CelOptions.current() + .enableShortCircuiting(true) + .enableHeterogeneousNumericComparisons(true) + .build()) + .build(); + Cel cel = + runtimeFlavor + .builder() + .setOptions( + CelOptions.current() + .enableShortCircuiting(false) + .enableHeterogeneousNumericComparisons(true) + .build()) + .build(); + CelAbstractSyntaxTree ast = cel.compile(expression).getAst(); + + boolean result = (boolean) cel.createProgram(ast).trace(listener); + boolean shortCircuitedResult = + (boolean) + celWithShortCircuit + .createProgram(celWithShortCircuit.compile(expression).getAst()) + .eval(); + + assertThat(result).isEqualTo(shortCircuitedResult); + 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 = + CelCompilerFactory.standardCelCompilerBuilder().setStandardEnvironmentEnabled(true).build(); + 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, () -> 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"); + } } diff --git a/runtime/src/test/java/dev/cel/runtime/CelStandardFunctionsTest.java b/runtime/src/test/java/dev/cel/runtime/CelStandardFunctionsTest.java new file mode 100644 index 000000000..c5f5572a7 --- /dev/null +++ b/runtime/src/test/java/dev/cel/runtime/CelStandardFunctionsTest.java @@ -0,0 +1,245 @@ +// 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. + +package dev.cel.runtime; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableSet; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import com.google.testing.junit.testparameterinjector.TestParameters; +import dev.cel.common.CelOptions; +import dev.cel.compiler.CelCompiler; +import dev.cel.compiler.CelCompilerFactory; +import dev.cel.runtime.CelStandardFunctions.StandardFunction; +import dev.cel.runtime.standard.AddOperator.AddOverload; +import dev.cel.runtime.standard.CelStandardOverload; +import dev.cel.runtime.standard.SubtractOperator.SubtractOverload; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public class CelStandardFunctionsTest { + + @Test + @TestParameters("{includeFunction: true, excludeFunction: true, filterFunction: true}") + @TestParameters("{includeFunction: true, excludeFunction: true, filterFunction: false}") + @TestParameters("{includeFunction: true, excludeFunction: false, filterFunction: true}") + @TestParameters("{includeFunction: false, excludeFunction: true, filterFunction: true}") + public void standardFunction_moreThanOneFunctionFilterSet_throws( + boolean includeFunction, boolean excludeFunction, boolean filterFunction) { + CelStandardFunctions.Builder builder = CelStandardFunctions.newBuilder(); + if (includeFunction) { + builder.includeFunctions(StandardFunction.ADD); + } + if (excludeFunction) { + builder.excludeFunctions(StandardFunction.SUBTRACT); + } + if (filterFunction) { + builder.filterFunctions((func, over) -> true); + } + + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, builder::build); + assertThat(e) + .hasMessageThat() + .contains( + "You may only populate one of the following builder methods: includeFunctions," + + " excludeFunctions or filterFunctions"); + } + + @Test + public void runtime_standardEnvironmentEnabled_throwsWhenOverridingFunctions() { + IllegalArgumentException e = + assertThrows( + IllegalArgumentException.class, + () -> + CelRuntimeFactory.standardCelRuntimeBuilder() + .setStandardEnvironmentEnabled(true) + .setStandardFunctions( + CelStandardFunctions.newBuilder() + .includeFunctions(StandardFunction.ADD, StandardFunction.SUBTRACT) + .build()) + .build()); + + assertThat(e) + .hasMessageThat() + .contains( + "setStandardEnvironmentEnabled must be set to false to override standard" + + " function bindings"); + } + + @Test + public void standardFunctions_includeFunctions() { + CelStandardFunctions celStandardFunctions = + CelStandardFunctions.newBuilder() + .includeFunctions( + CelStandardFunctions.StandardFunction.ADD, + CelStandardFunctions.StandardFunction.SUBTRACT) + .build(); + + assertThat(celStandardFunctions.getOverloads()) + .containsExactlyElementsIn( + ImmutableSet.builder() + .addAll(CelStandardFunctions.StandardFunction.ADD.getOverloads()) + .addAll(CelStandardFunctions.StandardFunction.SUBTRACT.getOverloads()) + .build()); + } + + @Test + public void standardFunctions_excludeFunctions() { + CelStandardFunctions celStandardFunction = + CelStandardFunctions.newBuilder() + .excludeFunctions( + CelStandardFunctions.StandardFunction.ADD, + CelStandardFunctions.StandardFunction.SUBTRACT) + .build(); + + assertThat(celStandardFunction.getOverloads()) + .doesNotContain(CelStandardFunctions.StandardFunction.ADD.getOverloads()); + assertThat(celStandardFunction.getOverloads()) + .doesNotContain(CelStandardFunctions.StandardFunction.SUBTRACT.getOverloads()); + } + + @Test + public void standardFunctions_filterFunctions() { + CelStandardFunctions celStandardFunction = + CelStandardFunctions.newBuilder() + .filterFunctions( + (func, over) -> { + if (func.equals(CelStandardFunctions.StandardFunction.ADD) + && over.equals(AddOverload.ADD_INT64)) { + return true; + } + + if (func.equals(CelStandardFunctions.StandardFunction.SUBTRACT) + && over.equals(SubtractOverload.SUBTRACT_INT64)) { + return true; + } + + return false; + }) + .build(); + + assertThat(celStandardFunction.getOverloads()) + .containsExactly(AddOverload.ADD_INT64, SubtractOverload.SUBTRACT_INT64); + } + + @Test + public void standardEnvironment_subsetEnvironment() throws Exception { + CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder().build(); + CelRuntime celRuntime = + CelRuntimeFactory.standardCelRuntimeBuilder() + .setStandardEnvironmentEnabled(false) + .setStandardFunctions( + CelStandardFunctions.newBuilder() + .includeFunctions(StandardFunction.ADD, StandardFunction.SUBTRACT) + .build()) + .build(); + assertThat(celRuntime.createProgram(celCompiler.compile("1 + 2 - 3").getAst()).eval()) + .isEqualTo(0); + CelEvaluationException e = + assertThrows( + CelEvaluationException.class, + () -> celRuntime.createProgram(celCompiler.compile("1 * 2 / 3").getAst()).eval()); + assertThat(e) + .hasMessageThat() + .contains("No matching overload for function '_*_'. Overload candidates: multiply_int64"); + } + + @Test + @TestParameters("{expression: '1 > 2.0'}") + @TestParameters("{expression: '2.0 > 1'}") + @TestParameters("{expression: '1 > 2u'}") + @TestParameters("{expression: '2u > 1'}") + @TestParameters("{expression: '2u > 1.0'}") + @TestParameters("{expression: '1.0 > 2u'}") + @TestParameters("{expression: '1 >= 2.0'}") + @TestParameters("{expression: '2.0 >= 1'}") + @TestParameters("{expression: '1 >= 2u'}") + @TestParameters("{expression: '2u >= 1'}") + @TestParameters("{expression: '2u >= 1.0'}") + @TestParameters("{expression: '1.0 >= 2u'}") + @TestParameters("{expression: '1 < 2.0'}") + @TestParameters("{expression: '2.0 < 1'}") + @TestParameters("{expression: '1 < 2u'}") + @TestParameters("{expression: '2u < 1'}") + @TestParameters("{expression: '2u < 1.0'}") + @TestParameters("{expression: '1.0 < 2u'}") + @TestParameters("{expression: '1 <= 2.0'}") + @TestParameters("{expression: '2.0 <= 1'}") + @TestParameters("{expression: '1 <= 2u'}") + @TestParameters("{expression: '2u <= 1'}") + @TestParameters("{expression: '2u <= 1.0'}") + @TestParameters("{expression: '1.0 <= 2u'}") + public void heterogeneousEqualityDisabled_mixedTypeComparisons_throws(String expression) { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .setOptions(CelOptions.current().enableHeterogeneousNumericComparisons(true).build()) + .build(); + CelRuntime celRuntime = + CelRuntimeFactory.standardCelRuntimeBuilder() + .setOptions(CelOptions.current().enableHeterogeneousNumericComparisons(false).build()) + .build(); + + CelEvaluationException e = + assertThrows( + CelEvaluationException.class, + () -> celRuntime.createProgram(celCompiler.compile(expression).getAst()).eval()); + assertThat(e).hasMessageThat().contains("No matching overload for function"); + } + + @Test + public void unsignedLongsDisabled_int64Identity_throws() { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .setOptions(CelOptions.current().enableUnsignedLongs(true).build()) + .build(); + CelRuntime celRuntime = + CelRuntimeFactory.standardCelRuntimeBuilder() + .setOptions(CelOptions.current().enableUnsignedLongs(false).build()) + .build(); + + CelEvaluationException e = + assertThrows( + CelEvaluationException.class, + () -> celRuntime.createProgram(celCompiler.compile("int(1)").getAst()).eval()); + assertThat(e) + .hasMessageThat() + .contains("No matching overload for function 'int'. Overload candidates: int64_to_int64"); + } + + @Test + public void timestampEpochDisabled_int64Identity_throws() { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .setOptions(CelOptions.current().build()) + .build(); + CelRuntime celRuntime = + CelRuntimeFactory.standardCelRuntimeBuilder() + .setOptions(CelOptions.current().enableTimestampEpoch(false).build()) + .build(); + + CelEvaluationException e = + assertThrows( + CelEvaluationException.class, + () -> + celRuntime.createProgram(celCompiler.compile("timestamp(10000)").getAst()).eval()); + assertThat(e) + .hasMessageThat() + .contains( + "No matching overload for function 'timestamp'. Overload candidates:" + + " int64_to_timestamp"); + } +} diff --git a/runtime/src/test/java/dev/cel/runtime/CelValueInterpreterTest.java b/runtime/src/test/java/dev/cel/runtime/CelValueInterpreterTest.java deleted file mode 100644 index 4c657a0c8..000000000 --- a/runtime/src/test/java/dev/cel/runtime/CelValueInterpreterTest.java +++ /dev/null @@ -1,88 +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 com.google.testing.testsize.MediumTest; -import dev.cel.common.CelOptions; -import dev.cel.testing.BaseInterpreterTest; -import dev.cel.testing.Eval; -import dev.cel.testing.EvalCelValueSync; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; - -/** Tests for {@link Interpreter} and related functionality using {@code CelValue}. */ -// @MediumTest -@RunWith(Parameterized.class) -public class CelValueInterpreterTest extends BaseInterpreterTest { - - private static final CelOptions SIGNED_UINT_TEST_OPTIONS = - CelOptions.current() - .enableTimestampEpoch(true) - .enableHeterogeneousNumericComparisons(true) - .enableCelValue(true) - .comprehensionMaxIterations(1_000) - .build(); - - public CelValueInterpreterTest(boolean declareWithCelType, Eval eval) { - super(declareWithCelType, eval); - } - - /** Test relies on PartialMessage, which is deprecated and not supported for CelValue. */ - @Override - @Test - public void unknownField() { - skipBaselineVerification(); - } - - /** Test relies on PartialMessage, which is deprecated and not supported for CelValue. */ - @Override - @Test - public void unknownResultSet() { - skipBaselineVerification(); - } - - @Parameters - public static List testData() { - return new ArrayList<>( - Arrays.asList( - new Object[][] { - // SYNC_PROTO_TYPE - { - /* declareWithCelType= */ false, - new EvalCelValueSync(TEST_FILE_DESCRIPTORS, TEST_OPTIONS) - }, - // SYNC_PROTO_TYPE_SIGNED_UINT - { - /* declareWithCelType= */ false, - new EvalCelValueSync(TEST_FILE_DESCRIPTORS, SIGNED_UINT_TEST_OPTIONS) - }, - // SYNC_CEL_TYPE - { - /* declareWithCelType= */ true, - new EvalCelValueSync(TEST_FILE_DESCRIPTORS, TEST_OPTIONS) - }, - // SYNC_CEL_TYPE_SIGNED_UINT - { - /* declareWithCelType= */ true, - new EvalCelValueSync(TEST_FILE_DESCRIPTORS, SIGNED_UINT_TEST_OPTIONS) - }, - })); - } -} diff --git a/runtime/src/test/java/dev/cel/runtime/DefaultDispatcherTest.java b/runtime/src/test/java/dev/cel/runtime/DefaultDispatcherTest.java new file mode 100644 index 000000000..d862ddb33 --- /dev/null +++ b/runtime/src/test/java/dev/cel/runtime/DefaultDispatcherTest.java @@ -0,0 +1,68 @@ +// 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. + +package dev.cel.runtime; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableList; +import java.util.HashMap; +import java.util.Map; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link DefaultDispatcher}. */ +@RunWith(JUnit4.class) +public final class DefaultDispatcherTest { + + private Map overloads; + + @Before + public void setup() { + overloads = new HashMap<>(); + overloads.put( + "overload_1", + CelResolvedOverload.of( + /* functionName= */ "overload_1", + /* overloadId= */ "overload_1", + args -> (Long) args[0] + 1, + /* isStrict= */ true, + Long.class)); + overloads.put( + "overload_2", + CelResolvedOverload.of( + /* functionName= */ "overload_2", + /* overloadId= */ "overload_2", + args -> (Long) args[0] + 2, + /* isStrict= */ true, + Long.class)); + } + + @Test + public void findOverload_multipleMatches_throwsException() { + CelEvaluationException e = + Assert.assertThrows( + CelEvaluationException.class, + () -> + DefaultDispatcher.findOverloadMatchingArgs( + "overloads", + ImmutableList.of("overload_1", "overload_2"), + overloads, + new Object[] {1L})); + assertThat(e).hasMessageThat().contains("Matching candidates: overload_1, overload_2"); + } +} diff --git a/runtime/src/test/java/dev/cel/runtime/DefaultInterpreterTest.java b/runtime/src/test/java/dev/cel/runtime/DefaultInterpreterTest.java new file mode 100644 index 000000000..a23e3b2fb --- /dev/null +++ b/runtime/src/test/java/dev/cel/runtime/DefaultInterpreterTest.java @@ -0,0 +1,114 @@ +// 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 static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableList; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +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.values.CelValueConverter; +import dev.cel.compiler.CelCompiler; +import dev.cel.compiler.CelCompilerFactory; +import dev.cel.parser.CelStandardMacro; +import dev.cel.runtime.DefaultInterpreter.DefaultInterpretable; +import dev.cel.runtime.DefaultInterpreter.ExecutionFrame; +import dev.cel.runtime.standard.NotStrictlyFalseFunction.NotStrictlyFalseOverload; +import java.util.Map; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Exercises tests for the internals of the {@link DefaultInterpreter}. Tests should only be added + * here if we absolutely must add coverage for internal state of the interpreter that's otherwise + * impossible with the CEL public APIs. + */ +@RunWith(TestParameterInjector.class) +public class DefaultInterpreterTest { + + @Test + public void nestedComprehensions_accuVarContainsErrors_scopeLevelInvariantNotViolated() + throws Exception { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "error", CelOverloadDecl.newGlobalOverload("error_overload", SimpleType.DYN))) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .build(); + RuntimeTypeProvider emptyProvider = + new RuntimeTypeProvider() { + @Override + public Object createMessage(String messageName, Map values) { + return null; + } + + @Override + public Object selectField(Object message, String fieldName) { + return null; + } + + @Override + public Object hasField(Object message, String fieldName) { + return null; + } + + @Override + public Object adapt(String messageName, Object message) { + return message; + } + }; + CelAbstractSyntaxTree ast = celCompiler.compile("[1].all(x, [2].all(y, error()))").getAst(); + DefaultDispatcher.Builder dispatcherBuilder = DefaultDispatcher.newBuilder(); + dispatcherBuilder.addOverload( + /* 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( + TypeResolver.create(CelValueConverter.getDefaultInstance()), + emptyProvider, + dispatcherBuilder.build(), + CelOptions.DEFAULT); + DefaultInterpretable interpretable = + (DefaultInterpretable) defaultInterpreter.createInterpretable(ast); + + ExecutionFrame frame = interpretable.newTestExecutionFrame(GlobalResolver.EMPTY); + + assertThrows(CelEvaluationException.class, () -> interpretable.populateExecutionFrame(frame)); + assertThat(frame.scopeLevel).isEqualTo(0); + } +} diff --git a/runtime/src/test/java/dev/cel/runtime/DescriptorMessageProviderTest.java b/runtime/src/test/java/dev/cel/runtime/DescriptorMessageProviderTest.java index 2421d2daf..02c8f2b51 100644 --- a/runtime/src/test/java/dev/cel/runtime/DescriptorMessageProviderTest.java +++ b/runtime/src/test/java/dev/cel/runtime/DescriptorMessageProviderTest.java @@ -31,18 +31,17 @@ import dev.cel.common.CelDescriptors; import dev.cel.common.CelErrorCode; import dev.cel.common.CelOptions; -import dev.cel.common.CelRuntimeException; +import dev.cel.common.exceptions.CelRuntimeException; import dev.cel.common.internal.CelDescriptorPool; import dev.cel.common.internal.DefaultDescriptorPool; import dev.cel.common.internal.DefaultMessageFactory; -// CEL-Internal-3 +// CEL-Internal-1 import dev.cel.common.internal.ProtoMessageFactory; import dev.cel.common.internal.WellKnownProto; -import dev.cel.testing.testdata.proto2.MessagesProto2; -import dev.cel.testing.testdata.proto2.MessagesProto2Extensions; -import dev.cel.testing.testdata.proto2.Proto2Message; -import dev.cel.testing.testdata.proto2.Proto2Message.NestedGroup; -import dev.cel.testing.testdata.proto3.TestAllTypesProto.TestAllTypes; +import dev.cel.expr.conformance.proto2.TestAllTypes; +import dev.cel.expr.conformance.proto2.TestAllTypes.NestedGroup; +import dev.cel.expr.conformance.proto2.TestAllTypesExtensions; +import dev.cel.expr.conformance.proto2.TestAllTypesProto; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -58,7 +57,7 @@ public void setUp() { CelOptions options = CelOptions.current().build(); CelDescriptors celDescriptors = CelDescriptorUtil.getAllDescriptorsFromFileDescriptor( - TestAllTypes.getDescriptor().getFile()); + dev.cel.expr.conformance.proto3.TestAllTypes.getDescriptor().getFile()); ProtoMessageFactory dynamicMessageFactory = DefaultMessageFactory.create(DefaultDescriptorPool.create(celDescriptors)); provider = new DescriptorMessageProvider(dynamicMessageFactory, options); @@ -66,29 +65,38 @@ public void setUp() { @Test public void createMessage_success() { - TestAllTypes message = - (TestAllTypes) + dev.cel.expr.conformance.proto3.TestAllTypes message = + (dev.cel.expr.conformance.proto3.TestAllTypes) provider.createMessage( - TestAllTypes.getDescriptor().getFullName(), ImmutableMap.of("single_int32", 1)); - assertThat(message).isEqualTo(TestAllTypes.newBuilder().setSingleInt32(1).build()); + dev.cel.expr.conformance.proto3.TestAllTypes.getDescriptor().getFullName(), + ImmutableMap.of("single_int32", 1)); + assertThat(message) + .isEqualTo( + dev.cel.expr.conformance.proto3.TestAllTypes.newBuilder().setSingleInt32(1).build()); } @Test public void createMessageDynamic_success() { - ImmutableList descriptors = ImmutableList.of(TestAllTypes.getDescriptor()); + ImmutableList descriptors = + ImmutableList.of(dev.cel.expr.conformance.proto3.TestAllTypes.getDescriptor()); provider = DynamicMessageFactory.typeProvider(descriptors); Message message = (Message) provider.createMessage( - TestAllTypes.getDescriptor().getFullName(), ImmutableMap.of("single_int32", 1)); - assertThat(message).isInstanceOf(TestAllTypes.class); - assertThat(message).isEqualTo(TestAllTypes.newBuilder().setSingleInt32(1).build()); + dev.cel.expr.conformance.proto3.TestAllTypes.getDescriptor().getFullName(), + ImmutableMap.of("single_int32", 1)); + assertThat(message).isInstanceOf(dev.cel.expr.conformance.proto3.TestAllTypes.class); + assertThat(message) + .isEqualTo( + dev.cel.expr.conformance.proto3.TestAllTypes.newBuilder().setSingleInt32(1).build()); } @Test public void createNestedGroup_success() throws Exception { - String groupType = "dev.cel.testing.testdata.proto2.Proto2Message.NestedGroup"; - provider = DynamicMessageFactory.typeProvider(ImmutableList.of(NestedGroup.getDescriptor())); + String groupType = "cel.expr.conformance.proto2.TestAllTypes.NestedGroup"; + provider = + DynamicMessageFactory.typeProvider( + ImmutableList.of(TestAllTypes.NestedGroup.getDescriptor())); Message message = (Message) provider.createMessage( @@ -106,17 +114,18 @@ public void createMessage_missingDescriptorError() { () -> provider.createMessage( "google.api.tools.contract.test.MissingMessageTypes", ImmutableMap.of())); - assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class); assertThat(e.getErrorCode()).isEqualTo(CelErrorCode.ATTRIBUTE_NOT_FOUND); + assertThat(e).hasCauseThat().isNull(); } @Test public void createMessage_unsetWrapperField() { - TestAllTypes message = - (TestAllTypes) + dev.cel.expr.conformance.proto3.TestAllTypes message = + (dev.cel.expr.conformance.proto3.TestAllTypes) provider.createMessage( - TestAllTypes.getDescriptor().getFullName(), - ImmutableMap.of("single_int64_wrapper", NullValue.NULL_VALUE)); + dev.cel.expr.conformance.proto3.TestAllTypes.getDescriptor().getFullName(), + ImmutableMap.of( + "single_int64_wrapper", dev.cel.common.values.NullValue.NULL_VALUE)); assertThat(message).isEqualToDefaultInstance(); } @@ -126,8 +135,8 @@ public void createMessage_badFieldError() { IllegalArgumentException.class, () -> provider.createMessage( - TestAllTypes.getDescriptor().getFullName(), - ImmutableMap.of("bad_field", NullValue.NULL_VALUE))); + dev.cel.expr.conformance.proto3.TestAllTypes.getDescriptor().getFullName(), + ImmutableMap.of("bad_field", dev.cel.common.values.NullValue.NULL_VALUE))); } @Test @@ -150,13 +159,16 @@ public void selectField_mapKeyNotFound() { CelRuntimeException e = Assert.assertThrows( CelRuntimeException.class, () -> provider.selectField(ImmutableMap.of(), "hello")); - assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class); assertThat(e.getErrorCode()).isEqualTo(CelErrorCode.ATTRIBUTE_NOT_FOUND); + assertThat(e).hasCauseThat().isNull(); } @Test public void selectField_unsetWrapperField() { - assertThat(provider.selectField(TestAllTypes.getDefaultInstance(), "single_int64_wrapper")) + assertThat( + provider.selectField( + dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance(), + "single_int64_wrapper")) .isEqualTo(NullValue.NULL_VALUE); } @@ -165,15 +177,15 @@ public void selectField_nonProtoObjectError() { CelRuntimeException e = Assert.assertThrows( CelRuntimeException.class, () -> provider.selectField("hello", "not_a_field")); - assertThat(e).hasCauseThat().isInstanceOf(IllegalStateException.class); - assertThat(e.getErrorCode()).isEqualTo(CelErrorCode.INTERNAL_ERROR); + assertThat(e.getErrorCode()).isEqualTo(CelErrorCode.ATTRIBUTE_NOT_FOUND); + assertThat(e).hasCauseThat().isNull(); } @Test public void selectField_extensionUsingDynamicTypes() { CelDescriptors celDescriptors = CelDescriptorUtil.getAllDescriptorsFromFileDescriptor( - ImmutableList.of(MessagesProto2Extensions.getDescriptor())); + ImmutableList.of(TestAllTypesExtensions.getDescriptor())); CelDescriptorPool pool = DefaultDescriptorPool.create(celDescriptors); provider = @@ -183,10 +195,8 @@ public void selectField_extensionUsingDynamicTypes() { long result = (long) provider.selectField( - Proto2Message.newBuilder() - .setExtension(MessagesProto2Extensions.int32Ext, 10) - .build(), - MessagesProto2.getDescriptor().getPackage() + ".int32_ext"); + TestAllTypes.newBuilder().setExtension(TestAllTypesExtensions.int32Ext, 10).build(), + TestAllTypesProto.getDescriptor().getPackage() + ".int32_ext"); assertThat(result).isEqualTo(10); } @@ -198,7 +208,8 @@ public void createMessage_wellKnownType_withCustomMessageProvider( return; } - Descriptor wellKnownDescriptor = wellKnownProto.descriptor(); + Descriptor wellKnownDescriptor = + DefaultDescriptorPool.INSTANCE.findDescriptor(wellKnownProto.typeName()).get(); DescriptorMessageProvider messageProvider = new DescriptorMessageProvider( msgName -> diff --git a/runtime/src/test/java/dev/cel/runtime/DescriptorTypeResolverTest.java b/runtime/src/test/java/dev/cel/runtime/DescriptorTypeResolverTest.java new file mode 100644 index 000000000..bdd865601 --- /dev/null +++ b/runtime/src/test/java/dev/cel/runtime/DescriptorTypeResolverTest.java @@ -0,0 +1,169 @@ +// 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. + +package dev.cel.runtime; + +import static com.google.common.truth.Truth.assertThat; + +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.types.OpaqueType; +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.common.types.TypeType; +import dev.cel.expr.conformance.proto3.TestAllTypes; +import dev.cel.extensions.CelOptionalLibrary; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public class DescriptorTypeResolverTest { + + private static final ProtoMessageTypeProvider PROTO_MESSAGE_TYPE_PROVIDER = + new ProtoMessageTypeProvider(ImmutableList.of(TestAllTypes.getDescriptor())); + + private static final Cel CEL = + CelFactory.standardCelBuilder() + .setTypeProvider(PROTO_MESSAGE_TYPE_PROVIDER) + .addCompilerLibraries(CelOptionalLibrary.INSTANCE) + .addRuntimeLibraries(CelOptionalLibrary.INSTANCE) + .addMessageTypes(TestAllTypes.getDescriptor()) + .setContainer(CelContainer.ofName(TestAllTypes.getDescriptor().getFullName())) + .build(); + + @SuppressWarnings("ImmutableEnumChecker") // Test only + private enum TypeLiteralTestCase { + BOOL("bool", TypeType.create(SimpleType.BOOL)), + BYTES("bytes", TypeType.create(SimpleType.BYTES)), + DOUBLE("double", TypeType.create(SimpleType.DOUBLE)), + DURATION("google.protobuf.Duration", TypeType.create(SimpleType.DURATION)), + INT("int", TypeType.create(SimpleType.INT)), + STRING("string", TypeType.create(SimpleType.STRING)), + TIMESTAMP("google.protobuf.Timestamp", TypeType.create(SimpleType.TIMESTAMP)), + UINT("uint", TypeType.create(SimpleType.UINT)), + + NULL_TYPE("null_type", TypeType.create(SimpleType.NULL_TYPE)), + OPTIONAL_TYPE("optional_type", TypeType.create(OptionalType.create(SimpleType.DYN))), + PROTO_MESSAGE_TYPE( + "TestAllTypes", + TypeType.create(StructTypeReference.create(TestAllTypes.getDescriptor().getFullName()))); + + private final String expression; + + private final TypeType celRuntimeType; + + TypeLiteralTestCase(String expression, TypeType celRuntimeType) { + this.expression = expression; + this.celRuntimeType = celRuntimeType; + } + } + + @Test + public void typeLiteral_success(@TestParameter TypeLiteralTestCase testCase) throws Exception { + if (!testCase.equals(TypeLiteralTestCase.DURATION)) { + return; + } + CelAbstractSyntaxTree ast = CEL.compile(testCase.expression).getAst(); + + assertThat(CEL.createProgram(ast).eval()).isEqualTo(testCase.celRuntimeType); + } + + @Test + public void typeLiteral_wrappedInDyn_success(@TestParameter TypeLiteralTestCase testCase) + throws Exception { + CelAbstractSyntaxTree ast = CEL.compile(String.format("dyn(%s)", testCase.expression)).getAst(); + + assertThat(CEL.createProgram(ast).eval()).isEqualTo(testCase.celRuntimeType); + } + + @Test + public void typeLiteral_equality(@TestParameter TypeLiteralTestCase testCase) throws Exception { + CelAbstractSyntaxTree ast = + CEL.compile(String.format("type(%s) == type", testCase.expression)).getAst(); + + assertThat(CEL.createProgram(ast).eval()).isEqualTo(true); + } + + @SuppressWarnings("ImmutableEnumChecker") // Test only + private enum TypeCallTestCase { + ANY( + "google.protobuf.Any{type_url: 'types.googleapis.com/google.protobuf.DoubleValue'}", + TypeType.create(SimpleType.DOUBLE)), + BOOL("true", TypeType.create(SimpleType.BOOL)), + BYTES("b'hi'", TypeType.create(SimpleType.BYTES)), + DOUBLE("1.5", TypeType.create(SimpleType.DOUBLE)), + DURATION("duration('1h')", TypeType.create(SimpleType.DURATION)), + INT("1", TypeType.create(SimpleType.INT)), + STRING("'test'", TypeType.create(SimpleType.STRING)), + TIMESTAMP("timestamp(123)", TypeType.create(SimpleType.TIMESTAMP)), + UINT("1u", TypeType.create(SimpleType.UINT)), + PROTO_MESSAGE( + "TestAllTypes{}", + TypeType.create(StructTypeReference.create(TestAllTypes.getDescriptor().getFullName()))), + OPTIONAL_TYPE("optional.of(1)", TypeType.create(OptionalType.create(SimpleType.DYN))); + + private final String expression; + + private final TypeType celRuntimeType; + + TypeCallTestCase(String expression, TypeType celRuntimeType) { + this.expression = expression; + this.celRuntimeType = celRuntimeType; + } + } + + @Test + public void typeCall_success(@TestParameter TypeCallTestCase testCase) throws Exception { + CelAbstractSyntaxTree ast = + CEL.compile(String.format("type(%s)", testCase.expression)).getAst(); + + assertThat(CEL.createProgram(ast).eval()).isEqualTo(testCase.celRuntimeType); + } + + @Test + public void typeOfTypeCall_success(@TestParameter TypeCallTestCase testCase) throws Exception { + CelAbstractSyntaxTree ast = + CEL.compile(String.format("type(type(%s))", testCase.expression)).getAst(); + + assertThat(CEL.createProgram(ast).eval()).isEqualTo(TypeType.create(SimpleType.DYN)); + } + + @Test + public void typeCall_wrappedInDyn_evaluatesToUnderlyingType( + @TestParameter TypeCallTestCase testCase) throws Exception { + CelAbstractSyntaxTree ast = + CEL.compile(String.format("type(dyn(%s))", testCase.expression)).getAst(); + + assertThat(CEL.createProgram(ast).eval()).isEqualTo(testCase.celRuntimeType); + } + + @Test + public void typeCall_opaqueVar() throws Exception { + OpaqueType opaqueType = OpaqueType.create("opaque_type"); + Cel cel = CEL.toCelBuilder().addVar("opaque_var", opaqueType).build(); + CelAbstractSyntaxTree ast = cel.compile("type(opaque_var)").getAst(); + final class CustomClass {} + + assertThat(CEL.createProgram(ast).eval(ImmutableMap.of("opaque_var", new CustomClass()))) + .isEqualTo(TypeType.create(opaqueType)); + } +} diff --git a/runtime/src/test/java/dev/cel/runtime/InterpreterTest.java b/runtime/src/test/java/dev/cel/runtime/InterpreterTest.java index b59c16c28..a6342c6e4 100644 --- a/runtime/src/test/java/dev/cel/runtime/InterpreterTest.java +++ b/runtime/src/test/java/dev/cel/runtime/InterpreterTest.java @@ -14,54 +14,12 @@ package dev.cel.runtime; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; // import com.google.testing.testsize.MediumTest; -import dev.cel.common.CelOptions; import dev.cel.testing.BaseInterpreterTest; -import dev.cel.testing.Eval; -import dev.cel.testing.EvalSync; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; /** Tests for {@link Interpreter} and related functionality. */ // @MediumTest -@RunWith(Parameterized.class) -public class InterpreterTest extends BaseInterpreterTest { - - private static final CelOptions SIGNED_UINT_TEST_OPTIONS = - CelOptions.current() - .enableTimestampEpoch(true) - .enableHeterogeneousNumericComparisons(true) - .comprehensionMaxIterations(1_000) - .build(); - - public InterpreterTest(boolean declareWithCelType, Eval eval) { - super(declareWithCelType, eval); - } - - @Parameters - public static List testData() { - - return new ArrayList<>( - Arrays.asList( - new Object[][] { - // SYNC_PROTO_TYPE - {/* declareWithCelType= */ false, new EvalSync(TEST_FILE_DESCRIPTORS, TEST_OPTIONS)}, - // SYNC_PROTO_TYPE_SIGNED_UINT - { - /* declareWithCelType= */ false, - new EvalSync(TEST_FILE_DESCRIPTORS, SIGNED_UINT_TEST_OPTIONS) - }, - // SYNC_CEL_TYPE - {/* declareWithCelType= */ true, new EvalSync(TEST_FILE_DESCRIPTORS, TEST_OPTIONS)}, - // SYNC_CEL_TYPE_SIGNED_UINT - { - /* declareWithCelType= */ true, - new EvalSync(TEST_FILE_DESCRIPTORS, SIGNED_UINT_TEST_OPTIONS) - }, - })); - } -} +@RunWith(TestParameterInjector.class) +public class InterpreterTest extends BaseInterpreterTest {} diff --git a/runtime/src/test/java/dev/cel/runtime/MessageFactoryTest.java b/runtime/src/test/java/dev/cel/runtime/MessageFactoryTest.java index 701a1e9c0..bead31569 100644 --- a/runtime/src/test/java/dev/cel/runtime/MessageFactoryTest.java +++ b/runtime/src/test/java/dev/cel/runtime/MessageFactoryTest.java @@ -18,8 +18,8 @@ import com.google.common.collect.ImmutableList; import com.google.protobuf.Message; +import dev.cel.expr.conformance.proto3.TestAllTypes; import dev.cel.runtime.MessageFactory.CombinedMessageFactory; -import dev.cel.testing.testdata.proto3.TestAllTypesProto.TestAllTypes; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; diff --git a/runtime/src/test/java/dev/cel/runtime/PlannerInterpreterTest.java b/runtime/src/test/java/dev/cel/runtime/PlannerInterpreterTest.java new file mode 100644 index 000000000..9ae8590d5 --- /dev/null +++ b/runtime/src/test/java/dev/cel/runtime/PlannerInterpreterTest.java @@ -0,0 +1,331 @@ +// 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 com.google.testing.junit.testparameterinjector.TestParameter; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelContainer; +import dev.cel.common.CelOptions; +import dev.cel.common.CelValidationException; +import dev.cel.common.types.CelTypeProvider; +import dev.cel.common.types.ListType; +import dev.cel.common.types.SimpleType; +import dev.cel.common.types.StructTypeReference; +import dev.cel.expr.conformance.proto3.TestAllTypes; +import dev.cel.extensions.CelExtensions; +import dev.cel.testing.BaseInterpreterTest; +import java.util.Arrays; +import java.util.Objects; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Interpreter tests using ProgramPlanner */ +@RunWith(TestParameterInjector.class) +public class PlannerInterpreterTest extends BaseInterpreterTest { + + @TestParameter boolean isParseOnly; + + @Override + protected CelRuntimeBuilder newBaseRuntimeBuilder(CelOptions celOptions) { + return CelRuntimeFactory.plannerRuntimeBuilder() + .addLateBoundFunctions("record") + .setOptions(celOptions) + .addLibraries(CelExtensions.optional()) + .addFileTypes(TEST_FILE_DESCRIPTORS) + .addMessageTypes(TestAllTypes.getDescriptor()); + } + + @Override + protected void setContainer(CelContainer container) { + super.setContainer(container); + this.celRuntime = this.celRuntime.toRuntimeBuilder().setContainer(container).build(); + } + + @Override + protected CelAbstractSyntaxTree prepareTest(CelTypeProvider typeProvider) { + super.prepareCompiler(typeProvider); + + CelAbstractSyntaxTree ast; + try { + ast = celCompiler.parse(source, testSourceDescription()).getAst(); + } catch (CelValidationException e) { + printTestValidationError(e); + return null; + } + + if (isParseOnly) { + return ast; + } + + try { + return celCompiler.check(ast).getAst(); + } catch (CelValidationException e) { + printTestValidationError(e); + return null; + } + } + + @Override + public void optional_errors() { + // Exercised in planner_optional_errors instead + skipBaselineVerification(); + } + + @Test + public void planner_optional_errors() { + source = "optional.unwrap([dyn(1)])"; + runTest(ImmutableMap.of()); + } + + @Override + public void unknownField() { + // Exercised in planner_unknownFieldAccess instead + skipBaselineVerification(); + } + + @Override + public void unknownResultSet() { + // Exercised in planner_unknownResultSet_success instead + skipBaselineVerification(); + } + + @Test + public void planner_unknownFieldSelection() { + setContainer(CelContainer.ofName(TestAllTypes.getDescriptor().getFile().getPackage())); + declareVariable("x", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())); + + CelAttributePattern patternX = CelAttributePattern.fromQualifiedIdentifier("x"); + + source = "x"; + // We have the full message, but we're claiming that the attribute is unknown. + runTest(ImmutableMap.of("x", TestAllTypes.getDefaultInstance()), patternX); + // A "partially known message". The result is still an unknown. + runTest( + ImmutableMap.of("x", TestAllTypes.getDefaultInstance()), + CelAttributePattern.fromQualifiedIdentifier("x.single_int32")); + + source = "x.single_int32"; + runTest(ImmutableMap.of(), patternX); + runTest(ImmutableMap.of(), CelAttributePattern.fromQualifiedIdentifier("x.single_int32")); + + source = "x.map_int32_int64[22]"; + runTest(ImmutableMap.of(), patternX); + runTest(ImmutableMap.of(), CelAttributePattern.fromQualifiedIdentifier("x.map_int32_int64")); + + source = "x.repeated_nested_message[1]"; + runTest(ImmutableMap.of(), patternX); + runTest( + ImmutableMap.of(), + CelAttributePattern.fromQualifiedIdentifier("x.repeated_nested_message")); + + source = "x.single_nested_message.bb"; + runTest(ImmutableMap.of(), patternX); + runTest( + ImmutableMap.of(), + CelAttributePattern.fromQualifiedIdentifier("x.single_nested_message.bb")); + + source = "{1: x.single_int32}"; + runTest(ImmutableMap.of(), patternX); + runTest(ImmutableMap.of(), CelAttributePattern.fromQualifiedIdentifier("x.single_int32")); + + source = "[1, x.single_int32]"; + runTest(ImmutableMap.of(), patternX); + runTest(ImmutableMap.of(), CelAttributePattern.fromQualifiedIdentifier("x.single_int32")); + } + + @Test + public void planner_unknownResultSet_success() { + setContainer(CelContainer.ofName(TestAllTypes.getDescriptor().getFile().getPackage())); + declareVariable("x", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())); + TestAllTypes message = + TestAllTypes.newBuilder() + .setSingleString("test") + .setSingleTimestamp(Timestamp.newBuilder().setSeconds(15)) + .build(); + ImmutableMap variables = ImmutableMap.of("x", message); + CelAttributePattern unknownInt32 = + CelAttributePattern.fromQualifiedIdentifier("x.single_int32"); + CelAttributePattern unknownInt64 = + CelAttributePattern.fromQualifiedIdentifier("x.single_int64"); + + source = "x.single_int32 == 1 && true"; + runTest(variables, unknownInt32); + + source = "x.single_int32 == 1 && false"; + runTest(variables, unknownInt32); + + source = "x.single_int32 == 1 && x.single_int64 == 1"; + runTest(variables, unknownInt32, unknownInt64); + + source = "true && x.single_int32 == 1"; + runTest(variables, unknownInt32); + + source = "false && x.single_int32 == 1"; + runTest(variables, unknownInt32); + + source = "x.single_int32 == 1 || x.single_string == \"test\""; + runTest(variables, unknownInt32); + + source = "x.single_int32 == 1 || x.single_string != \"test\""; + runTest(variables, unknownInt32); + + source = "x.single_int32 == 1 || x.single_int64 == 1"; + runTest(variables, unknownInt32, unknownInt64); + + source = "true || x.single_int32 == 1"; + runTest(variables, unknownInt32); + + source = "false || x.single_int32 == 1"; + runTest(variables, unknownInt32); + + // dispatch test + declareFunction( + "f", memberOverload("f", Arrays.asList(SimpleType.INT, SimpleType.INT), SimpleType.BOOL)); + celRuntime = + newBaseRuntimeBuilder( + CelOptions.current() + .enableHeterogeneousNumericComparisons(true) + .enableOptionalSyntax(true) + .comprehensionMaxIterations(1_000) + .build()) + .addFunctionBindings( + CelFunctionBinding.from("f", Integer.class, Integer.class, Objects::equals)) + .setContainer(CelContainer.ofName(TestAllTypes.getDescriptor().getFile().getPackage())) + .build(); + + source = "x.single_int32.f(1)"; + runTest(variables, unknownInt32); + + source = "1.f(x.single_int32)"; + runTest(variables, unknownInt32); + + source = "x.single_int64.f(x.single_int32)"; + runTest(variables, unknownInt32, unknownInt64); + + source = "[0, 2, 4].exists(z, z == 2 || z == x.single_int32)"; + runTest(variables, unknownInt32); + + source = "[0, 2, 4].exists(z, z == x.single_int32)"; + runTest(variables, unknownInt32); + + source = + "[0, 2, 4].exists_one(z, z == 0 || (z == 2 && z == x.single_int32) " + + "|| (z == 4 && z == x.single_int64))"; + runTest(variables, unknownInt32, unknownInt64); + + source = "[0, 2].all(z, z == 2 || z == x.single_int32)"; + runTest(variables, unknownInt32); + + source = + "[0, 2, 4].filter(z, z == 0 || (z == 2 && z == x.single_int32) " + + "|| (z == 4 && z == x.single_int64))"; + runTest(variables, unknownInt32, unknownInt64); + + source = + "[0, 2, 4].map(z, z == 0 || (z == 2 && z == x.single_int32) " + + "|| (z == 4 && z == x.single_int64))"; + runTest(variables, unknownInt32, unknownInt64); + + source = "x.single_int32 == 1 ? 1 : 2"; + runTest(variables, unknownInt32); + + source = "true ? x.single_int32 : 2"; + runTest(variables, unknownInt32); + + source = "true ? 1 : x.single_int32"; + runTest(variables, unknownInt32); + + source = "false ? x.single_int32 : 2"; + runTest(variables, unknownInt32); + + source = "false ? 1 : x.single_int32"; + runTest(variables, unknownInt32); + + source = "x.single_int64 == 1 ? x.single_int32 : x.single_int32"; + runTest(variables, unknownInt32, unknownInt64); + + source = "{x.single_int32: 2, 3: 4}"; + runTest(variables, unknownInt32); + + source = "{1: x.single_int32, 3: 4}"; + runTest(variables, unknownInt32); + + source = "{1: x.single_int32, x.single_int64: 4}"; + runTest(variables, unknownInt32, unknownInt64); + + source = "[1, x.single_int32, 3, 4]"; + runTest(variables, unknownInt32); + + source = "[1, x.single_int32, x.single_int64, 4]"; + runTest(variables, unknownInt32, unknownInt64); + + source = "TestAllTypes{single_int32: x.single_int32}.single_int32 == 2"; + runTest(variables, unknownInt32); + + source = "TestAllTypes{single_int32: x.single_int32, single_int64: x.single_int64}"; + runTest(variables, unknownInt32, unknownInt64); + + clearAllDeclarations(); + declareVariable("unknown_list", ListType.create(SimpleType.INT)); + source = "unknown_list.map(x, x)"; + runTest(variables, CelAttributePattern.fromQualifiedIdentifier("unknown_list")); + + clearAllDeclarations(); + declareVariable("x", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())); + source = "cel.bind(x, [1, 2, 3], 1 in x)"; + runTest(variables, CelAttributePattern.fromQualifiedIdentifier("x")); + } + + @Test + public void planner_unknownResultSet_errors() { + declareVariable("x", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())); + TestAllTypes message = + TestAllTypes.newBuilder() + .setSingleString("test") + .setSingleTimestamp(Timestamp.newBuilder().setSeconds(15)) + .build(); + ImmutableMap variables = ImmutableMap.of("x", message); + CelAttributePattern unknownInt32 = + CelAttributePattern.fromQualifiedIdentifier("x.single_int32"); + + source = "x.single_int32 == 1 && x.single_timestamp <= timestamp(\"bad timestamp string\")"; + runTest(variables, unknownInt32); + + source = "x.single_timestamp <= timestamp(\"bad timestamp string\") && x.single_int32 == 1"; + runTest(variables, unknownInt32); + + source = + "x.single_timestamp <= timestamp(\"bad timestamp string\") " + + "&& x.single_timestamp > timestamp(\"another bad timestamp string\")"; + runTest(variables, unknownInt32); + + source = "x.single_int32 == 1 || x.single_timestamp <= timestamp(\"bad timestamp string\")"; + runTest(variables, unknownInt32); + + source = "x.single_timestamp <= timestamp(\"bad timestamp string\") || x.single_int32 == 1"; + runTest(variables, unknownInt32); + + source = + "x.single_timestamp <= timestamp(\"bad timestamp string\") " + + "|| x.single_timestamp > timestamp(\"another bad timestamp string\")"; + runTest(variables, unknownInt32); + + source = "x"; + runTest(ImmutableMap.of(), CelAttributePattern.fromQualifiedIdentifier("x")); + } +} diff --git a/runtime/src/test/java/dev/cel/runtime/ProtoMessageRuntimeEqualityTest.java b/runtime/src/test/java/dev/cel/runtime/ProtoMessageRuntimeEqualityTest.java new file mode 100644 index 000000000..dc4607e5f --- /dev/null +++ b/runtime/src/test/java/dev/cel/runtime/ProtoMessageRuntimeEqualityTest.java @@ -0,0 +1,691 @@ +// 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.runtime; + +import static com.google.common.truth.Truth.assertThat; + +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.common.primitives.UnsignedLong; +import com.google.protobuf.Any; +import com.google.protobuf.BoolValue; +import com.google.protobuf.ByteString; +import com.google.protobuf.BytesValue; +import com.google.protobuf.DoubleValue; +import com.google.protobuf.FloatValue; +import com.google.protobuf.Int32Value; +import com.google.protobuf.Int64Value; +import com.google.protobuf.ListValue; +import com.google.protobuf.NullValue; +import com.google.protobuf.StringValue; +import com.google.protobuf.Struct; +import com.google.protobuf.UInt32Value; +import com.google.protobuf.UInt64Value; +import com.google.protobuf.Value; +import com.google.rpc.context.AttributeContext; +import com.google.rpc.context.AttributeContext.Auth; +import com.google.rpc.context.AttributeContext.Peer; +import com.google.rpc.context.AttributeContext.Request; +import dev.cel.common.CelDescriptorUtil; +import dev.cel.common.CelOptions; +import dev.cel.common.exceptions.CelRuntimeException; +import dev.cel.common.internal.AdaptingTypes; +import dev.cel.common.internal.BidiConverter; +import dev.cel.common.internal.DefaultDescriptorPool; +import dev.cel.common.internal.DefaultMessageFactory; +import dev.cel.common.internal.DynamicProto; +import dev.cel.common.internal.ProtoTimeUtils; +import dev.cel.common.values.CelByteString; +import java.util.Arrays; +import java.util.List; +import org.jspecify.annotations.Nullable; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public final class ProtoMessageRuntimeEqualityTest { + private static final CelOptions EMPTY_OPTIONS = + CelOptions.newBuilder().disableCelStandardEquality(false).build(); + private static final CelOptions PROTO_EQUALITY = + CelOptions.newBuilder() + .disableCelStandardEquality(false) + .enableProtoDifferencerEquality(true) + .build(); + private static final DynamicProto DYNAMIC_PROTO = + DynamicProto.create( + DefaultMessageFactory.create( + DefaultDescriptorPool.create( + CelDescriptorUtil.getAllDescriptorsFromFileDescriptor( + AttributeContext.getDescriptor().getFile())))); + + private static final ProtoMessageRuntimeEquality RUNTIME_EQUALITY_LEGACY_OPTIONS = + ProtoMessageRuntimeEquality.create(DYNAMIC_PROTO, CelOptions.LEGACY); + + private static final ProtoMessageRuntimeEquality RUNTIME_EQUALITY_DEFAULT_OPTIONS = + ProtoMessageRuntimeEquality.create(DYNAMIC_PROTO, CelOptions.DEFAULT); + + private static final ProtoMessageRuntimeEquality RUNTIME_EQUALITY_EMPTY_OPTIONS = + ProtoMessageRuntimeEquality.create(DYNAMIC_PROTO, EMPTY_OPTIONS); + + private static final ProtoMessageRuntimeEquality RUNTIME_EQUALITY_PROTO_EQUALITY = + ProtoMessageRuntimeEquality.create(DYNAMIC_PROTO, PROTO_EQUALITY); + + @Test + public void inMap() throws Exception { + ImmutableMap map = ImmutableMap.of("key", "value", "key2", "value2"); + assertThat(RUNTIME_EQUALITY_EMPTY_OPTIONS.inMap(map, "key2")).isTrue(); + assertThat(RUNTIME_EQUALITY_EMPTY_OPTIONS.inMap(map, "key3")).isFalse(); + + ImmutableMap mixedKeyMap = + ImmutableMap.of( + "key", "value", 2L, "value2", UnsignedLong.valueOf(42), "answer to everything"); + // Integer tests. + assertThat(RUNTIME_EQUALITY_EMPTY_OPTIONS.inMap(mixedKeyMap, 2)).isTrue(); + assertThat(RUNTIME_EQUALITY_EMPTY_OPTIONS.inMap(mixedKeyMap, 3)).isFalse(); + + // Long tests. + assertThat(RUNTIME_EQUALITY_EMPTY_OPTIONS.inMap(mixedKeyMap, -1L)).isFalse(); + assertThat(RUNTIME_EQUALITY_EMPTY_OPTIONS.inMap(mixedKeyMap, 3L)).isFalse(); + assertThat(RUNTIME_EQUALITY_EMPTY_OPTIONS.inMap(mixedKeyMap, 2L)).isTrue(); + assertThat(RUNTIME_EQUALITY_EMPTY_OPTIONS.inMap(mixedKeyMap, 42L)).isTrue(); + + // Floating point tests + assertThat(RUNTIME_EQUALITY_EMPTY_OPTIONS.inMap(mixedKeyMap, -1.0d)).isFalse(); + assertThat(RUNTIME_EQUALITY_EMPTY_OPTIONS.inMap(mixedKeyMap, 2.1d)).isFalse(); + assertThat( + RUNTIME_EQUALITY_EMPTY_OPTIONS.inMap(mixedKeyMap, UnsignedLong.MAX_VALUE.doubleValue())) + .isFalse(); + assertThat(RUNTIME_EQUALITY_EMPTY_OPTIONS.inMap(mixedKeyMap, 2.0d)).isTrue(); + assertThat(RUNTIME_EQUALITY_EMPTY_OPTIONS.inMap(mixedKeyMap, Double.NaN)).isFalse(); + + // Unsigned long tests. + assertThat(RUNTIME_EQUALITY_EMPTY_OPTIONS.inMap(mixedKeyMap, UnsignedLong.valueOf(1L))) + .isFalse(); + assertThat(RUNTIME_EQUALITY_EMPTY_OPTIONS.inMap(mixedKeyMap, UnsignedLong.valueOf(2L))) + .isTrue(); + assertThat(RUNTIME_EQUALITY_EMPTY_OPTIONS.inMap(mixedKeyMap, UnsignedLong.MAX_VALUE)).isFalse(); + assertThat(RUNTIME_EQUALITY_EMPTY_OPTIONS.inMap(mixedKeyMap, UInt64Value.of(2L))).isTrue(); + + // Validate the legacy behavior as well. + assertThat(RUNTIME_EQUALITY_LEGACY_OPTIONS.inMap(mixedKeyMap, 2)).isFalse(); + assertThat(RUNTIME_EQUALITY_LEGACY_OPTIONS.inMap(mixedKeyMap, 2L)).isTrue(); + assertThat(RUNTIME_EQUALITY_LEGACY_OPTIONS.inMap(mixedKeyMap, Int64Value.of(2L))).isFalse(); + assertThat(RUNTIME_EQUALITY_LEGACY_OPTIONS.inMap(mixedKeyMap, UInt64Value.of(2L))).isFalse(); + } + + @Test + public void inList() throws Exception { + ImmutableList list = ImmutableList.of("value", "value2"); + assertThat(RUNTIME_EQUALITY_EMPTY_OPTIONS.inList(list, "value")).isTrue(); + assertThat(RUNTIME_EQUALITY_EMPTY_OPTIONS.inList(list, "value3")).isFalse(); + + ImmutableList mixedValueList = ImmutableList.of(1, "value", 2, "value2"); + assertThat(RUNTIME_EQUALITY_EMPTY_OPTIONS.inList(mixedValueList, 2)).isTrue(); + assertThat(RUNTIME_EQUALITY_EMPTY_OPTIONS.inList(mixedValueList, 3)).isFalse(); + + assertThat(RUNTIME_EQUALITY_EMPTY_OPTIONS.inList(mixedValueList, 2L)).isTrue(); + assertThat(RUNTIME_EQUALITY_EMPTY_OPTIONS.inList(mixedValueList, 3L)).isFalse(); + + assertThat(RUNTIME_EQUALITY_EMPTY_OPTIONS.inList(mixedValueList, 2.0)).isTrue(); + assertThat(RUNTIME_EQUALITY_EMPTY_OPTIONS.inList(mixedValueList, Double.NaN)).isFalse(); + + assertThat(RUNTIME_EQUALITY_EMPTY_OPTIONS.inList(mixedValueList, UnsignedLong.valueOf(2L))) + .isTrue(); + assertThat(RUNTIME_EQUALITY_EMPTY_OPTIONS.inList(mixedValueList, UnsignedLong.valueOf(3L))) + .isFalse(); + + // Validate the legacy behavior as well. + assertThat(RUNTIME_EQUALITY_LEGACY_OPTIONS.inList(mixedValueList, 2)).isTrue(); + assertThat(RUNTIME_EQUALITY_LEGACY_OPTIONS.inList(mixedValueList, 2L)).isFalse(); + } + + @Test + public void indexMap() throws Exception { + ImmutableMap mixedKeyMap = + ImmutableMap.of(1L, "value", UnsignedLong.valueOf(2L), "value2"); + assertThat(RUNTIME_EQUALITY_DEFAULT_OPTIONS.indexMap(mixedKeyMap, 1.0)).isEqualTo("value"); + assertThat(RUNTIME_EQUALITY_DEFAULT_OPTIONS.indexMap(mixedKeyMap, 2.0)).isEqualTo("value2"); + Assert.assertThrows( + CelRuntimeException.class, + () -> RUNTIME_EQUALITY_LEGACY_OPTIONS.indexMap(mixedKeyMap, 1.0)); + Assert.assertThrows( + CelRuntimeException.class, + () -> RUNTIME_EQUALITY_DEFAULT_OPTIONS.indexMap(mixedKeyMap, 1.1)); + } + + @AutoValue + abstract static class State { + /** + * Expected comparison outcome when equality is performed with the given options. + * + *

The {@code null} value indicates that the outcome is an error. + */ + public abstract @Nullable Boolean outcome(); + + /** Runtime equality instance to use when performing the equality check. */ + public abstract ProtoMessageRuntimeEquality runtimeEquality(); + + public static State create( + @Nullable Boolean outcome, ProtoMessageRuntimeEquality runtimeEquality) { + return new AutoValue_ProtoMessageRuntimeEqualityTest_State(outcome, runtimeEquality); + } + } + + /** Represents expected result states for an equality test case. */ + @AutoValue + abstract static class Result { + + /** The result {@code State} value associated with different feature flag combinations. */ + public abstract ImmutableSet states(); + + /** + * Creates a Result for a comparison that is undefined (throws an Exception) under both equality + * modes. + */ + public static Result undefined() { + return always(null); + } + + /** Creates a Result for a comparison that is false under both equality modes. */ + public static Result alwaysFalse() { + return always(false); + } + + /** Creates a Result for a comparison that is true under both equality modes. */ + public static Result alwaysTrue() { + return always(true); + } + + public static Result unsigned(Boolean outcome) { + return Result.builder() + .states( + ImmutableList.of( + State.create(outcome, RUNTIME_EQUALITY_EMPTY_OPTIONS), + State.create(outcome, RUNTIME_EQUALITY_PROTO_EQUALITY))) + .build(); + } + + private static Result always(@Nullable Boolean outcome) { + return Result.builder() + .states( + ImmutableList.of( + State.create(outcome, RUNTIME_EQUALITY_EMPTY_OPTIONS), + State.create(outcome, RUNTIME_EQUALITY_PROTO_EQUALITY))) + .build(); + } + + private static Result proto(Boolean equalsOutcome, Boolean diffOutcome) { + return Result.builder() + .states( + ImmutableList.of( + State.create(equalsOutcome, RUNTIME_EQUALITY_EMPTY_OPTIONS), + State.create(diffOutcome, RUNTIME_EQUALITY_PROTO_EQUALITY))) + .build(); + } + + public static Builder builder() { + return new AutoValue_ProtoMessageRuntimeEqualityTest_Result.Builder(); + } + + @AutoValue.Builder + public abstract static class Builder { + abstract Builder states(ImmutableList states); + + abstract Result build(); + } + } + + @Parameter(0) + public Object lhs; + + @Parameter(1) + public Object rhs; + + @Parameter(2) + public Result result; + + @Parameters + public static List data() { + return Arrays.asList( + new Object[][] { + // Boolean tests. + {true, true, Result.alwaysTrue()}, + {BoolValue.of(true), true, Result.alwaysTrue()}, + {Any.pack(BoolValue.of(true)), true, Result.alwaysTrue()}, + {Value.newBuilder().setBoolValue(true).build(), true, Result.alwaysTrue()}, + {true, false, Result.alwaysFalse()}, + {0, false, Result.alwaysFalse()}, + + // Bytes tests. + {ByteString.copyFromUtf8("h¢"), ByteString.copyFromUtf8("h¢"), Result.alwaysTrue()}, + {ByteString.copyFromUtf8("hello"), ByteString.EMPTY, Result.alwaysFalse()}, + {BytesValue.of(ByteString.EMPTY), CelByteString.EMPTY, Result.alwaysTrue()}, + { + BytesValue.of(ByteString.copyFromUtf8("h¢")), + CelByteString.copyFromUtf8("h¢"), + Result.alwaysTrue() + }, + {Any.pack(BytesValue.of(ByteString.EMPTY)), CelByteString.EMPTY, Result.alwaysTrue()}, + {"h¢", CelByteString.copyFromUtf8("h¢"), Result.alwaysFalse()}, + + // Double tests. + {1.0, 1.0, Result.alwaysTrue()}, + {Double.valueOf(1.0), 1.0, Result.alwaysTrue()}, + {DoubleValue.of(42.5), 42.5, Result.alwaysTrue()}, + // Floats are unwrapped to double types. + {FloatValue.of(1.0f), 1.0, Result.alwaysTrue()}, + {Value.newBuilder().setNumberValue(-1.5D).build(), -1.5, Result.alwaysTrue()}, + {1.0, -1.0, Result.alwaysFalse()}, + {1.0, 1.0D, Result.alwaysTrue()}, + {1.0, 1.1D, Result.alwaysFalse()}, + {1.0D, 1.1f, Result.alwaysFalse()}, + {1.0, 1, Result.alwaysTrue()}, + + // Float tests. + {1.0f, 1.0f, Result.alwaysTrue()}, + {Float.valueOf(1.0f), 1.0f, Result.alwaysTrue()}, + {1.0f, -1.0f, Result.alwaysFalse()}, + {1.0f, 1.0, Result.alwaysTrue()}, + + // Integer tests. + {16, 16, Result.alwaysTrue()}, + {17, 16, Result.alwaysFalse()}, + {17, 16.0, Result.alwaysFalse()}, + + // Long tests. + {-15L, -15L, Result.alwaysTrue()}, + // Int32 values are unwrapped to int types. + {Int32Value.of(-15), -15L, Result.alwaysTrue()}, + {Int64Value.of(-15L), -15L, Result.alwaysTrue()}, + {Any.pack(Int32Value.of(-15)), -15L, Result.alwaysTrue()}, + {Any.pack(Int64Value.of(-15L)), -15L, Result.alwaysTrue()}, + {-15L, -16L, Result.alwaysFalse()}, + {-15L, -15, Result.alwaysTrue()}, + {-15L, 15.0, Result.alwaysFalse()}, + + // Null tests. + {null, null, Result.alwaysTrue()}, + {false, null, Result.alwaysFalse()}, + {0.0, null, Result.alwaysFalse()}, + {0, null, Result.alwaysFalse()}, + {null, "null", Result.alwaysFalse()}, + {"null", null, Result.alwaysFalse()}, + {null, NullValue.NULL_VALUE, Result.alwaysTrue()}, + {null, ImmutableList.of(), Result.alwaysFalse()}, + {ImmutableMap.of(), null, Result.alwaysFalse()}, + {ByteString.copyFromUtf8(""), null, Result.alwaysFalse()}, + {null, ProtoTimeUtils.TIMESTAMP_EPOCH, Result.alwaysFalse()}, + {ProtoTimeUtils.DURATION_ZERO, null, Result.alwaysFalse()}, + {NullValue.NULL_VALUE, NullValue.NULL_VALUE, Result.alwaysTrue()}, + { + Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build(), + NullValue.NULL_VALUE, + Result.alwaysTrue() + }, + { + Any.pack(Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build()), + NullValue.NULL_VALUE, + Result.alwaysTrue() + }, + + // String tests. + {"", "", Result.alwaysTrue()}, + {"str", "str", Result.alwaysTrue()}, + {StringValue.of("str"), "str", Result.alwaysTrue()}, + {Value.newBuilder().setStringValue("str").build(), "str", Result.alwaysTrue()}, + {Any.pack(StringValue.of("str")), "str", Result.alwaysTrue()}, + {Any.pack(Value.newBuilder().setStringValue("str").build()), "str", Result.alwaysTrue()}, + {"", "non-empty", Result.alwaysFalse()}, + + // Uint tests. + {UInt32Value.of(1234), 1234L, Result.alwaysTrue()}, + {UInt64Value.of(1234L), 1234L, Result.alwaysTrue()}, + {UInt64Value.of(1234L), Int64Value.of(1234L), Result.alwaysTrue()}, + {UInt32Value.of(1234), UnsignedLong.valueOf(1234L), Result.alwaysTrue()}, + {UInt64Value.of(1234L), UnsignedLong.valueOf(1234L), Result.alwaysTrue()}, + {Any.pack(UInt64Value.of(1234L)), UnsignedLong.valueOf(1234L), Result.alwaysTrue()}, + {UInt32Value.of(123), UnsignedLong.valueOf(1234L), Result.alwaysFalse()}, + {UInt64Value.of(123L), UnsignedLong.valueOf(1234L), Result.alwaysFalse()}, + {Any.pack(UInt64Value.of(123L)), UnsignedLong.valueOf(1234L), Result.alwaysFalse()}, + + // Cross-type equality tests. + {UInt32Value.of(1234), 1234.0, Result.alwaysTrue()}, + {UInt32Value.of(1234), 1234.0, Result.alwaysTrue()}, + {UInt64Value.of(1234L), 1234L, Result.alwaysTrue()}, + {UInt32Value.of(1234), 1234.1, Result.alwaysFalse()}, + {UInt64Value.of(1234L), 1233L, Result.alwaysFalse()}, + {UnsignedLong.valueOf(1234L), 1234L, Result.alwaysTrue()}, + {UnsignedLong.valueOf(1234L), 1234.1, Result.alwaysFalse()}, + {1234L, 1233.2, Result.alwaysFalse()}, + {-1234L, UnsignedLong.valueOf(1233L), Result.alwaysFalse()}, + + // List tests. + // Note, this list equality behaves equivalently to the following expression: + // 1.0 == 1.0 && "dos" == 2.0 && 3.0 == 4.0 + // The middle predicate is an error; however, the last comparison yields false and so + + // the error is short-circuited away. + {Arrays.asList(1.0, "dos", 3.0), Arrays.asList(1.0, 2.0, 4.0), Result.alwaysFalse()}, + {Arrays.asList("1", 2), ImmutableList.of("1", 2), Result.alwaysTrue()}, + {Arrays.asList("1", 2), ImmutableSet.of("1", 2), Result.alwaysTrue()}, + {Arrays.asList(1.0, 2.0, 3.0), Arrays.asList(1.0, 2.0), Result.alwaysFalse()}, + {Arrays.asList(1.0, 3.0), Arrays.asList(1.0, 2.0), Result.alwaysFalse()}, + { + AdaptingTypes.adaptingList( + ImmutableList.of(1, 2, 3), + BidiConverter.of( + ProtoMessageRuntimeHelpers.INT32_TO_INT64, + ProtoMessageRuntimeHelpers.INT64_TO_INT32)), + Arrays.asList(1L, 2L, 3L), + Result.alwaysTrue() + }, + { + ListValue.newBuilder() + .addValues(Value.newBuilder().setStringValue("hello")) + .addValues(Value.newBuilder().setStringValue("world")) + .build(), + ImmutableList.of("hello", "world"), + Result.alwaysTrue() + }, + { + ListValue.newBuilder() + .addValues(Value.newBuilder().setStringValue("hello")) + .addValues(Value.newBuilder().setListValue(ListValue.getDefaultInstance())) + .build(), + ImmutableList.of("hello", "world"), + Result.alwaysFalse() + }, + { + ListValue.newBuilder() + .addValues(Value.newBuilder().setListValue(ListValue.getDefaultInstance())) + .addValues( + Value.newBuilder() + .setListValue( + ListValue.newBuilder() + .addValues(Value.newBuilder().setBoolValue(true)))) + .build(), + ImmutableList.of(ImmutableList.of(), ImmutableList.of(true)), + Result.alwaysTrue() + }, + { + Value.newBuilder() + .setListValue( + ListValue.newBuilder() + .addValues(Value.newBuilder().setNumberValue(-1.5)) + .addValues(Value.newBuilder().setNumberValue(42.25))) + .build(), + AdaptingTypes.adaptingList( + ImmutableList.of(-1.5f, 42.25f), + BidiConverter.of( + ProtoMessageRuntimeHelpers.FLOAT_TO_DOUBLE, + ProtoMessageRuntimeHelpers.DOUBLE_TO_FLOAT)), + Result.alwaysTrue() + }, + + // Map tests. + {ImmutableMap.of("one", 1), ImmutableMap.of("one", "uno"), Result.alwaysFalse()}, + {ImmutableMap.of("two", 2), ImmutableMap.of("two", 3), Result.alwaysFalse()}, + {ImmutableMap.of("one", 2), ImmutableMap.of("two", 3), Result.alwaysFalse()}, + // Note, this map is the composition of the following two tests above where: + // ("one", 1) == ("one", "uno") -> error + // ("two", 2) == ("two", 3) -> false + // Within CEL error && false -> false, and the key order in the test has specifically + // been chosen to exercise this behavior. + { + ImmutableMap.of("one", 1, "two", 2), + ImmutableMap.of("one", "uno", "two", 3), + Result.alwaysFalse() + }, + {ImmutableMap.of("key", "value"), ImmutableMap.of("key", "value"), Result.alwaysTrue()}, + {ImmutableMap.of(), ImmutableMap.of("key", "value"), Result.alwaysFalse()}, + {ImmutableMap.of("key", "value"), ImmutableMap.of("key", "diff"), Result.alwaysFalse()}, + {ImmutableMap.of("key", 42), ImmutableMap.of("key", 42L), Result.alwaysTrue()}, + {ImmutableMap.of("key", 42.0), ImmutableMap.of("key", 42L), Result.alwaysTrue()}, + { + AdaptingTypes.adaptingMap( + ImmutableMap.of("key1", 42, "key2", 31, "key3", 20), + BidiConverter.identity(), + BidiConverter.of( + ProtoMessageRuntimeHelpers.INT32_TO_INT64, + ProtoMessageRuntimeHelpers.INT64_TO_INT32)), + ImmutableMap.of("key1", 42L, "key2", 31L, "key3", 20L), + Result.alwaysTrue() + }, + { + AdaptingTypes.adaptingMap( + ImmutableMap.of(1, 42.5f, 2, 31f, 3, 20.25f), + BidiConverter.of( + ProtoMessageRuntimeHelpers.INT32_TO_INT64, + ProtoMessageRuntimeHelpers.INT64_TO_INT32), + BidiConverter.of( + ProtoMessageRuntimeHelpers.FLOAT_TO_DOUBLE, + ProtoMessageRuntimeHelpers.DOUBLE_TO_FLOAT)), + ImmutableMap.of(1L, 42.5D, 2L, 31D, 3L, 20.25D), + Result.alwaysTrue() + }, + { + AdaptingTypes.adaptingMap( + ImmutableMap.of("1", 42.5f, "2", 31f, "3", 20.25f), + BidiConverter.identity(), + BidiConverter.of( + ProtoMessageRuntimeHelpers.FLOAT_TO_DOUBLE, + ProtoMessageRuntimeHelpers.DOUBLE_TO_FLOAT)), + Struct.getDefaultInstance(), + Result.alwaysFalse() + }, + { + AdaptingTypes.adaptingMap( + ImmutableMap.of("1", 42.5f, "2", 31f, "3", 20.25f), + BidiConverter.identity(), + BidiConverter.of( + ProtoMessageRuntimeHelpers.FLOAT_TO_DOUBLE, + ProtoMessageRuntimeHelpers.DOUBLE_TO_FLOAT)), + Struct.newBuilder() + .putFields("1", Value.newBuilder().setNumberValue(42.5D).build()) + .putFields("2", Value.newBuilder().setNumberValue(31D).build()) + .putFields("3", Value.newBuilder().setNumberValue(20.25D).build()) + .build(), + Result.alwaysTrue() + }, + { + AdaptingTypes.adaptingMap( + ImmutableMap.of("1", 42.5f, "2", 31f, "3", 20.25f), + BidiConverter.identity(), + BidiConverter.of( + ProtoMessageRuntimeHelpers.FLOAT_TO_DOUBLE, + ProtoMessageRuntimeHelpers.DOUBLE_TO_FLOAT)), + Struct.newBuilder() + .putFields("1", Value.newBuilder().setNumberValue(42.5D).build()) + .putFields("2", Value.newBuilder().setNumberValue(31D).build()) + .putFields("3", Value.newBuilder().setStringValue("oops").build()) + .build(), + Result.alwaysFalse() + }, + + // Protobuf tests. + { + AttributeContext.newBuilder().setRequest(Request.getDefaultInstance()).build(), + AttributeContext.newBuilder().setRequest(Request.newBuilder().setHost("")).build(), + Result.alwaysTrue() + }, + { + AttributeContext.newBuilder() + .setRequest(Request.getDefaultInstance()) + .setOrigin(Peer.getDefaultInstance()) + .build(), + AttributeContext.newBuilder().setRequest(Request.getDefaultInstance()).build(), + Result.alwaysFalse() + }, + // Proto differencer unpacks any values. + { + AttributeContext.newBuilder() + .addExtensions( + Any.newBuilder() + .setTypeUrl("type.googleapis.com/google.rpc.context.AttributeContext") + .setValue(ByteString.copyFromUtf8("\032\000:\000")) + .build()) + .build(), + AttributeContext.newBuilder() + .addExtensions( + Any.newBuilder() + .setTypeUrl("type.googleapis.com/google.rpc.context.AttributeContext") + .setValue(ByteString.copyFromUtf8(":\000\032\000")) + .build()) + .build(), + Result.builder() + .states( + ImmutableList.of( + State.create(false, RUNTIME_EQUALITY_EMPTY_OPTIONS), + State.create(true, RUNTIME_EQUALITY_PROTO_EQUALITY))) + .build() + }, + // If type url is missing, fallback to bytes comparison for payload. + { + AttributeContext.newBuilder() + .addExtensions( + Any.newBuilder().setValue(ByteString.copyFromUtf8("\032\000:\000")).build()) + .build(), + AttributeContext.newBuilder() + .addExtensions( + Any.newBuilder().setValue(ByteString.copyFromUtf8(":\000\032\000")).build()) + .build(), + Result.alwaysFalse() + }, + { + AttributeContext.newBuilder() + .setRequest(Request.getDefaultInstance()) + .setOrigin(Peer.getDefaultInstance()) + .build(), + "test string", + Result.alwaysFalse() + }, + { + AttributeContext.newBuilder() + .setRequest(Request.getDefaultInstance()) + .setOrigin(Peer.getDefaultInstance()) + .build(), + null, + Result.alwaysFalse() + }, + { + AttributeContext.newBuilder() + .addExtensions( + Any.pack( + AttributeContext.newBuilder() + .setRequest(Request.getDefaultInstance()) + .setOrigin(Peer.getDefaultInstance()) + .build())) + .build(), + AttributeContext.newBuilder() + .addExtensions( + Any.pack( + AttributeContext.newBuilder() + .setRequest(Request.getDefaultInstance()) + .build())) + .build(), + Result.alwaysFalse() + }, + { + AttributeContext.getDefaultInstance(), + AttributeContext.newBuilder() + .setRequest(Request.newBuilder().setHost("localhost")) + .build(), + Result.alwaysFalse() + }, + // Differently typed messages aren't comparable. + {AttributeContext.getDefaultInstance(), Auth.getDefaultInstance(), Result.alwaysFalse()}, + // Message.equals() treats NaN values as equal. Message differencer treats NaN values + // as inequal (the same behavior as the C++ implementation). + { + AttributeContext.newBuilder() + .setRequest( + Request.newBuilder() + .setAuth( + Auth.newBuilder() + .setClaims( + Struct.newBuilder() + .putFields( + "custom", + Value.newBuilder() + .setNumberValue(Double.NaN) + .build())))) + .build(), + AttributeContext.newBuilder() + .setRequest( + Request.newBuilder() + .setAuth( + Auth.newBuilder() + .setClaims( + Struct.newBuilder() + .putFields( + "custom", + Value.newBuilder() + .setNumberValue(Double.NaN) + .build())))) + .build(), + Result.proto(/* equalsOutcome= */ true, /* diffOutcome= */ false), + }, + + // Note: this is the motivating use case for converting to heterogeneous equality in + // the future. + { + AttributeContext.newBuilder() + .setRequest( + Request.newBuilder() + .setAuth( + Auth.newBuilder() + .setClaims( + Struct.newBuilder() + .putFields( + "custom", + Value.newBuilder().setNumberValue(123.0).build())))) + .build(), + AttributeContext.newBuilder() + .setRequest( + Request.newBuilder() + .setAuth( + Auth.newBuilder() + .setClaims( + Struct.newBuilder() + .putFields( + "custom", + Value.newBuilder().setBoolValue(true).build())))) + .build(), + Result.alwaysFalse(), + }, + }); + } + + @Test + public void objectEquals() throws Exception { + for (State state : result.states()) { + if (state.outcome() == null) { + Assert.assertThrows( + CelRuntimeException.class, () -> state.runtimeEquality().objectEquals(lhs, rhs)); + Assert.assertThrows( + CelRuntimeException.class, () -> state.runtimeEquality().objectEquals(rhs, lhs)); + return; + } + assertThat(state.runtimeEquality().objectEquals(lhs, rhs)).isEqualTo(state.outcome()); + assertThat(state.runtimeEquality().objectEquals(rhs, lhs)).isEqualTo(state.outcome()); + } + } +} diff --git a/runtime/src/test/java/dev/cel/runtime/RuntimeHelpersTest.java b/runtime/src/test/java/dev/cel/runtime/ProtoMessageRuntimeHelpersTest.java similarity index 50% rename from runtime/src/test/java/dev/cel/runtime/RuntimeHelpersTest.java rename to runtime/src/test/java/dev/cel/runtime/ProtoMessageRuntimeHelpersTest.java index b22c3f14e..45b050fd1 100644 --- a/runtime/src/test/java/dev/cel/runtime/RuntimeHelpersTest.java +++ b/runtime/src/test/java/dev/cel/runtime/ProtoMessageRuntimeHelpersTest.java @@ -36,9 +36,11 @@ import com.google.protobuf.UInt64Value; import com.google.protobuf.Value; import dev.cel.common.CelOptions; -import dev.cel.common.CelRuntimeException; +import dev.cel.common.exceptions.CelNumericOverflowException; +import dev.cel.common.exceptions.CelRuntimeException; import dev.cel.common.internal.DefaultMessageFactory; import dev.cel.common.internal.DynamicProto; +import dev.cel.common.values.CelByteString; import java.util.HashMap; import java.util.Map; import java.util.Optional; @@ -48,13 +50,15 @@ import org.junit.runners.JUnit4; @RunWith(JUnit4.class) -public final class RuntimeHelpersTest { +public final class ProtoMessageRuntimeHelpersTest { private static final DynamicProto DYNAMIC_PROTO = DynamicProto.create(DefaultMessageFactory.INSTANCE); + private static final RuntimeHelpers RUNTIME_HELPER = + ProtoMessageRuntimeHelpers.create(DYNAMIC_PROTO, CelOptions.DEFAULT); @Test public void createDurationFromString() throws Exception { - assertThat(RuntimeHelpers.createDurationFromString("15.11s")) + assertThat(ProtoMessageRuntimeHelpers.createDurationFromString("15.11s")) .isEqualTo(Duration.newBuilder().setSeconds(15).setNanos(110000000).build()); } @@ -62,127 +66,136 @@ public void createDurationFromString() throws Exception { public void createDurationFromString_outOfRange() throws Exception { assertThrows( IllegalArgumentException.class, - () -> RuntimeHelpers.createDurationFromString("-320000000000s")); + () -> ProtoMessageRuntimeHelpers.createDurationFromString("-320000000000s")); } @Test public void int64Add() throws Exception { - assertThat(RuntimeHelpers.int64Add(1, 1, CelOptions.LEGACY)).isEqualTo(2); - assertThat(RuntimeHelpers.int64Add(2, 2, CelOptions.DEFAULT)).isEqualTo(4); - assertThat(RuntimeHelpers.int64Add(1, Long.MAX_VALUE, CelOptions.LEGACY)) + assertThat(ProtoMessageRuntimeHelpers.int64Add(1, 1, CelOptions.LEGACY)).isEqualTo(2); + assertThat(ProtoMessageRuntimeHelpers.int64Add(2, 2, CelOptions.DEFAULT)).isEqualTo(4); + assertThat(ProtoMessageRuntimeHelpers.int64Add(1, Long.MAX_VALUE, CelOptions.LEGACY)) .isEqualTo(Long.MIN_VALUE); - assertThat(RuntimeHelpers.int64Add(-1, Long.MIN_VALUE, CelOptions.LEGACY)) + assertThat(ProtoMessageRuntimeHelpers.int64Add(-1, Long.MIN_VALUE, CelOptions.LEGACY)) .isEqualTo(Long.MAX_VALUE); assertThrows( ArithmeticException.class, - () -> RuntimeHelpers.int64Add(1, Long.MAX_VALUE, CelOptions.DEFAULT)); + () -> ProtoMessageRuntimeHelpers.int64Add(1, Long.MAX_VALUE, CelOptions.DEFAULT)); assertThrows( ArithmeticException.class, - () -> RuntimeHelpers.int64Add(-1, Long.MIN_VALUE, CelOptions.DEFAULT)); + () -> ProtoMessageRuntimeHelpers.int64Add(-1, Long.MIN_VALUE, CelOptions.DEFAULT)); } @Test public void int64Divide() throws Exception { - assertThat(RuntimeHelpers.int64Divide(-44, 11, CelOptions.LEGACY)).isEqualTo(-4); - assertThat(RuntimeHelpers.int64Divide(-44, 11, CelOptions.DEFAULT)).isEqualTo(-4); - assertThat(RuntimeHelpers.int64Divide(Long.MIN_VALUE, -1, CelOptions.LEGACY)) + assertThat(ProtoMessageRuntimeHelpers.int64Divide(-44, 11, CelOptions.LEGACY)).isEqualTo(-4); + assertThat(ProtoMessageRuntimeHelpers.int64Divide(-44, 11, CelOptions.DEFAULT)).isEqualTo(-4); + assertThat(ProtoMessageRuntimeHelpers.int64Divide(Long.MIN_VALUE, -1, CelOptions.LEGACY)) .isEqualTo(Long.MIN_VALUE); assertThrows( - ArithmeticException.class, - () -> RuntimeHelpers.int64Divide(Long.MIN_VALUE, -1, CelOptions.DEFAULT)); + CelNumericOverflowException.class, + () -> ProtoMessageRuntimeHelpers.int64Divide(Long.MIN_VALUE, -1, CelOptions.DEFAULT)); } @Test public void int64Multiply() throws Exception { - assertThat(RuntimeHelpers.int64Multiply(2, 3, CelOptions.LEGACY)).isEqualTo(6); - assertThat(RuntimeHelpers.int64Multiply(2, 3, CelOptions.DEFAULT)).isEqualTo(6); - assertThat(RuntimeHelpers.int64Multiply(Long.MIN_VALUE, -1, CelOptions.LEGACY)) + assertThat(ProtoMessageRuntimeHelpers.int64Multiply(2, 3, CelOptions.LEGACY)).isEqualTo(6); + assertThat(ProtoMessageRuntimeHelpers.int64Multiply(2, 3, CelOptions.DEFAULT)).isEqualTo(6); + assertThat(ProtoMessageRuntimeHelpers.int64Multiply(Long.MIN_VALUE, -1, CelOptions.LEGACY)) .isEqualTo(Long.MIN_VALUE); assertThrows( ArithmeticException.class, - () -> RuntimeHelpers.int64Multiply(Long.MIN_VALUE, -1, CelOptions.DEFAULT)); + () -> ProtoMessageRuntimeHelpers.int64Multiply(Long.MIN_VALUE, -1, CelOptions.DEFAULT)); } @Test public void int64Negate() throws Exception { - assertThat(RuntimeHelpers.int64Negate(7, CelOptions.LEGACY)).isEqualTo(-7); - assertThat(RuntimeHelpers.int64Negate(7, CelOptions.DEFAULT)).isEqualTo(-7); - assertThat(RuntimeHelpers.int64Negate(Long.MIN_VALUE, CelOptions.LEGACY)) + assertThat(ProtoMessageRuntimeHelpers.int64Negate(7, CelOptions.LEGACY)).isEqualTo(-7); + assertThat(ProtoMessageRuntimeHelpers.int64Negate(7, CelOptions.DEFAULT)).isEqualTo(-7); + assertThat(ProtoMessageRuntimeHelpers.int64Negate(Long.MIN_VALUE, CelOptions.LEGACY)) .isEqualTo(Long.MIN_VALUE); assertThrows( ArithmeticException.class, - () -> RuntimeHelpers.int64Negate(Long.MIN_VALUE, CelOptions.DEFAULT)); + () -> ProtoMessageRuntimeHelpers.int64Negate(Long.MIN_VALUE, CelOptions.DEFAULT)); } @Test public void int64Subtract() throws Exception { - assertThat(RuntimeHelpers.int64Subtract(50, 100, CelOptions.LEGACY)).isEqualTo(-50); - assertThat(RuntimeHelpers.int64Subtract(50, 100, CelOptions.DEFAULT)).isEqualTo(-50); - assertThat(RuntimeHelpers.int64Subtract(Long.MIN_VALUE, 1, CelOptions.LEGACY)) + assertThat(ProtoMessageRuntimeHelpers.int64Subtract(50, 100, CelOptions.LEGACY)).isEqualTo(-50); + assertThat(ProtoMessageRuntimeHelpers.int64Subtract(50, 100, CelOptions.DEFAULT)) + .isEqualTo(-50); + assertThat(ProtoMessageRuntimeHelpers.int64Subtract(Long.MIN_VALUE, 1, CelOptions.LEGACY)) .isEqualTo(Long.MAX_VALUE); - assertThat(RuntimeHelpers.int64Subtract(Long.MAX_VALUE, -1, CelOptions.LEGACY)) + assertThat(ProtoMessageRuntimeHelpers.int64Subtract(Long.MAX_VALUE, -1, CelOptions.LEGACY)) .isEqualTo(Long.MIN_VALUE); assertThrows( ArithmeticException.class, - () -> RuntimeHelpers.int64Subtract(Long.MIN_VALUE, 1, CelOptions.DEFAULT)); + () -> ProtoMessageRuntimeHelpers.int64Subtract(Long.MIN_VALUE, 1, CelOptions.DEFAULT)); assertThrows( ArithmeticException.class, - () -> RuntimeHelpers.int64Subtract(Long.MAX_VALUE, -1, CelOptions.DEFAULT)); + () -> ProtoMessageRuntimeHelpers.int64Subtract(Long.MAX_VALUE, -1, CelOptions.DEFAULT)); } @Test public void uint64CompareTo_unsignedLongs() { - assertThat(RuntimeHelpers.uint64CompareTo(UnsignedLong.ONE, UnsignedLong.ZERO)).isEqualTo(1); - assertThat(RuntimeHelpers.uint64CompareTo(UnsignedLong.ZERO, UnsignedLong.ONE)).isEqualTo(-1); - assertThat(RuntimeHelpers.uint64CompareTo(UnsignedLong.ONE, UnsignedLong.ONE)).isEqualTo(0); + assertThat(ProtoMessageRuntimeHelpers.uint64CompareTo(UnsignedLong.ONE, UnsignedLong.ZERO)) + .isEqualTo(1); + assertThat(ProtoMessageRuntimeHelpers.uint64CompareTo(UnsignedLong.ZERO, UnsignedLong.ONE)) + .isEqualTo(-1); + assertThat(ProtoMessageRuntimeHelpers.uint64CompareTo(UnsignedLong.ONE, UnsignedLong.ONE)) + .isEqualTo(0); assertThat( - RuntimeHelpers.uint64CompareTo( + ProtoMessageRuntimeHelpers.uint64CompareTo( UnsignedLong.valueOf(Long.MAX_VALUE), UnsignedLong.MAX_VALUE)) .isEqualTo(-1); } @Test public void uint64CompareTo_throwsWhenNegativeOrGreaterThanSignedLongMax() throws Exception { - assertThrows(IllegalArgumentException.class, () -> RuntimeHelpers.uint64CompareTo(-1, 0)); - assertThrows(IllegalArgumentException.class, () -> RuntimeHelpers.uint64CompareTo(0, -1)); + assertThrows( + IllegalArgumentException.class, () -> ProtoMessageRuntimeHelpers.uint64CompareTo(-1, 0)); + assertThrows( + IllegalArgumentException.class, () -> ProtoMessageRuntimeHelpers.uint64CompareTo(0, -1)); } @Test public void uint64CompareTo_unsignedComparisonAndArithmeticIsUnsigned() throws Exception { // In twos complement, -1 is represented by all bits being set. This is equivalent to the // maximum value when unsigned. - assertThat(RuntimeHelpers.uint64CompareTo(-1, 0, CelOptions.DEFAULT)).isGreaterThan(0); - assertThat(RuntimeHelpers.uint64CompareTo(0, -1, CelOptions.DEFAULT)).isLessThan(0); + assertThat(ProtoMessageRuntimeHelpers.uint64CompareTo(-1, 0, CelOptions.DEFAULT)) + .isGreaterThan(0); + assertThat(ProtoMessageRuntimeHelpers.uint64CompareTo(0, -1, CelOptions.DEFAULT)).isLessThan(0); } @Test public void uint64Add_signedLongs() throws Exception { - assertThat(RuntimeHelpers.uint64Add(4, 4, CelOptions.LEGACY)).isEqualTo(8); - assertThat(RuntimeHelpers.uint64Add(4, 4, CelOptions.DEFAULT)).isEqualTo(8); - assertThat(RuntimeHelpers.uint64Add(-1, 1, CelOptions.LEGACY)).isEqualTo(0); + assertThat(ProtoMessageRuntimeHelpers.uint64Add(4, 4, CelOptions.LEGACY)).isEqualTo(8); + assertThat(ProtoMessageRuntimeHelpers.uint64Add(4, 4, CelOptions.DEFAULT)).isEqualTo(8); + assertThat(ProtoMessageRuntimeHelpers.uint64Add(-1, 1, CelOptions.LEGACY)).isEqualTo(0); assertThrows( - ArithmeticException.class, () -> RuntimeHelpers.uint64Add(-1, 1, CelOptions.DEFAULT)); + CelNumericOverflowException.class, + () -> ProtoMessageRuntimeHelpers.uint64Add(-1, 1, CelOptions.DEFAULT)); } @Test public void uint64Add_unsignedLongs() throws Exception { - assertThat(RuntimeHelpers.uint64Add(UnsignedLong.valueOf(4), UnsignedLong.valueOf(4))) + assertThat( + ProtoMessageRuntimeHelpers.uint64Add(UnsignedLong.valueOf(4), UnsignedLong.valueOf(4))) .isEqualTo(UnsignedLong.valueOf(8)); assertThat( - RuntimeHelpers.uint64Add( + ProtoMessageRuntimeHelpers.uint64Add( UnsignedLong.MAX_VALUE.minus(UnsignedLong.ONE), UnsignedLong.ONE)) .isEqualTo(UnsignedLong.MAX_VALUE); assertThrows( - ArithmeticException.class, - () -> RuntimeHelpers.uint64Add(UnsignedLong.MAX_VALUE, UnsignedLong.ONE)); + CelNumericOverflowException.class, + () -> ProtoMessageRuntimeHelpers.uint64Add(UnsignedLong.MAX_VALUE, UnsignedLong.ONE)); } @Test public void uint64Multiply_signedLongs() throws Exception { - assertThat(RuntimeHelpers.uint64Multiply(32, 2, CelOptions.LEGACY)).isEqualTo(64); - assertThat(RuntimeHelpers.uint64Multiply(32, 2, CelOptions.DEFAULT)).isEqualTo(64); + assertThat(ProtoMessageRuntimeHelpers.uint64Multiply(32, 2, CelOptions.LEGACY)).isEqualTo(64); + assertThat(ProtoMessageRuntimeHelpers.uint64Multiply(32, 2, CelOptions.DEFAULT)).isEqualTo(64); assertThat( - RuntimeHelpers.uint64Multiply( + ProtoMessageRuntimeHelpers.uint64Multiply( Long.MIN_VALUE, 2, CelOptions.newBuilder() @@ -190,106 +203,120 @@ public void uint64Multiply_signedLongs() throws Exception { .build())) .isEqualTo(0); assertThrows( - ArithmeticException.class, - () -> RuntimeHelpers.uint64Multiply(Long.MIN_VALUE, 2, CelOptions.DEFAULT)); + CelNumericOverflowException.class, + () -> ProtoMessageRuntimeHelpers.uint64Multiply(Long.MIN_VALUE, 2, CelOptions.DEFAULT)); } @Test public void uint64Multiply_unsignedLongs() throws Exception { - assertThat(RuntimeHelpers.uint64Multiply(UnsignedLong.valueOf(32), UnsignedLong.valueOf(2))) + assertThat( + ProtoMessageRuntimeHelpers.uint64Multiply( + UnsignedLong.valueOf(32), UnsignedLong.valueOf(2))) .isEqualTo(UnsignedLong.valueOf(64)); assertThrows( - ArithmeticException.class, - () -> RuntimeHelpers.uint64Multiply(UnsignedLong.MAX_VALUE, UnsignedLong.valueOf(2))); + CelNumericOverflowException.class, + () -> + ProtoMessageRuntimeHelpers.uint64Multiply( + UnsignedLong.MAX_VALUE, UnsignedLong.valueOf(2))); } @Test public void uint64Multiply_throwsWhenNegativeOrGreaterThanSignedLongMax() throws Exception { - assertThrows(IllegalArgumentException.class, () -> RuntimeHelpers.uint64Multiply(-1, 0)); - assertThrows(IllegalArgumentException.class, () -> RuntimeHelpers.uint64Multiply(0, -1)); + assertThrows( + IllegalArgumentException.class, () -> ProtoMessageRuntimeHelpers.uint64Multiply(-1, 0)); + assertThrows( + IllegalArgumentException.class, () -> ProtoMessageRuntimeHelpers.uint64Multiply(0, -1)); } @Test public void uint64Multiply_unsignedComparisonAndArithmeticIsUnsigned() throws Exception { // In twos complement, -1 is represented by all bits being set. This is equivalent to the // maximum value when unsigned. - assertThat(RuntimeHelpers.uint64Multiply(-1, 0, CelOptions.DEFAULT)).isEqualTo(0); - assertThat(RuntimeHelpers.uint64Multiply(0, -1, CelOptions.DEFAULT)).isEqualTo(0); + assertThat(ProtoMessageRuntimeHelpers.uint64Multiply(-1, 0, CelOptions.DEFAULT)).isEqualTo(0); + assertThat(ProtoMessageRuntimeHelpers.uint64Multiply(0, -1, CelOptions.DEFAULT)).isEqualTo(0); } @Test public void uint64Divide_unsignedLongs() { - assertThat(RuntimeHelpers.uint64Divide(UnsignedLong.ZERO, UnsignedLong.ONE)) + assertThat(ProtoMessageRuntimeHelpers.uint64Divide(UnsignedLong.ZERO, UnsignedLong.ONE)) .isEqualTo(UnsignedLong.ZERO); - assertThat(RuntimeHelpers.uint64Divide(UnsignedLong.MAX_VALUE, UnsignedLong.MAX_VALUE)) + assertThat( + ProtoMessageRuntimeHelpers.uint64Divide(UnsignedLong.MAX_VALUE, UnsignedLong.MAX_VALUE)) .isEqualTo(UnsignedLong.ONE); assertThrows( CelRuntimeException.class, - () -> RuntimeHelpers.uint64Divide(UnsignedLong.MAX_VALUE, UnsignedLong.ZERO)); + () -> ProtoMessageRuntimeHelpers.uint64Divide(UnsignedLong.MAX_VALUE, UnsignedLong.ZERO)); } @Test public void uint64Divide_throwsWhenNegativeOrGreaterThanSignedLongMax() throws Exception { - assertThrows(IllegalArgumentException.class, () -> RuntimeHelpers.uint64Divide(0, -1)); - assertThrows(IllegalArgumentException.class, () -> RuntimeHelpers.uint64Divide(-1, -1)); + assertThrows( + IllegalArgumentException.class, () -> ProtoMessageRuntimeHelpers.uint64Divide(0, -1)); + assertThrows( + IllegalArgumentException.class, () -> ProtoMessageRuntimeHelpers.uint64Divide(-1, -1)); } @Test public void uint64Divide_unsignedComparisonAndArithmeticIsUnsigned() throws Exception { // In twos complement, -1 is represented by all bits being set. This is equivalent to the // maximum value when unsigned. - assertThat(RuntimeHelpers.uint64Divide(0, -1, CelOptions.DEFAULT)).isEqualTo(0); - assertThat(RuntimeHelpers.uint64Divide(-1, -1, CelOptions.DEFAULT)).isEqualTo(1); + assertThat(ProtoMessageRuntimeHelpers.uint64Divide(0, -1, CelOptions.DEFAULT)).isEqualTo(0); + assertThat(ProtoMessageRuntimeHelpers.uint64Divide(-1, -1, CelOptions.DEFAULT)).isEqualTo(1); } @Test public void uint64Mod_unsignedLongs() throws Exception { - assertThat(RuntimeHelpers.uint64Mod(UnsignedLong.ONE, UnsignedLong.valueOf(2))) + assertThat(ProtoMessageRuntimeHelpers.uint64Mod(UnsignedLong.ONE, UnsignedLong.valueOf(2))) .isEqualTo(UnsignedLong.ONE); - assertThat(RuntimeHelpers.uint64Mod(UnsignedLong.ONE, UnsignedLong.ONE)) + assertThat(ProtoMessageRuntimeHelpers.uint64Mod(UnsignedLong.ONE, UnsignedLong.ONE)) .isEqualTo(UnsignedLong.ZERO); assertThrows( CelRuntimeException.class, - () -> RuntimeHelpers.uint64Mod(UnsignedLong.ONE, UnsignedLong.ZERO)); + () -> ProtoMessageRuntimeHelpers.uint64Mod(UnsignedLong.ONE, UnsignedLong.ZERO)); } @Test public void uint64Mod_throwsWhenNegativeOrGreaterThanSignedLongMax() throws Exception { - assertThrows(IllegalArgumentException.class, () -> RuntimeHelpers.uint64Mod(0, -1)); - assertThrows(IllegalArgumentException.class, () -> RuntimeHelpers.uint64Mod(-1, -1)); + assertThrows(IllegalArgumentException.class, () -> ProtoMessageRuntimeHelpers.uint64Mod(0, -1)); + assertThrows( + IllegalArgumentException.class, () -> ProtoMessageRuntimeHelpers.uint64Mod(-1, -1)); } @Test public void uint64Mod_unsignedComparisonAndArithmeticIsUnsigned() throws Exception { // In twos complement, -1 is represented by all bits being set. This is equivalent to the // maximum value when unsigned. - assertThat(RuntimeHelpers.uint64Mod(0, -1, CelOptions.DEFAULT)).isEqualTo(0); - assertThat(RuntimeHelpers.uint64Mod(-1, -1, CelOptions.DEFAULT)).isEqualTo(0); + assertThat(ProtoMessageRuntimeHelpers.uint64Mod(0, -1, CelOptions.DEFAULT)).isEqualTo(0); + assertThat(ProtoMessageRuntimeHelpers.uint64Mod(-1, -1, CelOptions.DEFAULT)).isEqualTo(0); } @Test public void uint64Subtract_signedLongs() throws Exception { - assertThat(RuntimeHelpers.uint64Subtract(-1, 2, CelOptions.LEGACY)).isEqualTo(-3); - assertThat(RuntimeHelpers.uint64Subtract(-1, 2, CelOptions.DEFAULT)).isEqualTo(-3); - assertThat(RuntimeHelpers.uint64Subtract(0, 1, CelOptions.LEGACY)).isEqualTo(-1); + assertThat(ProtoMessageRuntimeHelpers.uint64Subtract(-1, 2, CelOptions.LEGACY)).isEqualTo(-3); + assertThat(ProtoMessageRuntimeHelpers.uint64Subtract(-1, 2, CelOptions.DEFAULT)).isEqualTo(-3); + assertThat(ProtoMessageRuntimeHelpers.uint64Subtract(0, 1, CelOptions.LEGACY)).isEqualTo(-1); assertThrows( - ArithmeticException.class, () -> RuntimeHelpers.uint64Subtract(0, 1, CelOptions.DEFAULT)); + CelNumericOverflowException.class, + () -> ProtoMessageRuntimeHelpers.uint64Subtract(0, 1, CelOptions.DEFAULT)); assertThrows( - ArithmeticException.class, () -> RuntimeHelpers.uint64Subtract(-3, -1, CelOptions.DEFAULT)); + CelNumericOverflowException.class, + () -> ProtoMessageRuntimeHelpers.uint64Subtract(-3, -1, CelOptions.DEFAULT)); assertThrows( - ArithmeticException.class, - () -> RuntimeHelpers.uint64Subtract(55, -40, CelOptions.DEFAULT)); + CelNumericOverflowException.class, + () -> ProtoMessageRuntimeHelpers.uint64Subtract(55, -40, CelOptions.DEFAULT)); } @Test public void uint64Subtract_unsignedLongs() throws Exception { - assertThat(RuntimeHelpers.uint64Subtract(UnsignedLong.ONE, UnsignedLong.ONE)) + assertThat(ProtoMessageRuntimeHelpers.uint64Subtract(UnsignedLong.ONE, UnsignedLong.ONE)) .isEqualTo(UnsignedLong.ZERO); - assertThat(RuntimeHelpers.uint64Subtract(UnsignedLong.valueOf(3), UnsignedLong.valueOf(2))) + assertThat( + ProtoMessageRuntimeHelpers.uint64Subtract( + UnsignedLong.valueOf(3), UnsignedLong.valueOf(2))) .isEqualTo(UnsignedLong.ONE); assertThrows( - ArithmeticException.class, - () -> RuntimeHelpers.uint64Subtract(UnsignedLong.ONE, UnsignedLong.valueOf(2))); + CelNumericOverflowException.class, + () -> ProtoMessageRuntimeHelpers.uint64Subtract(UnsignedLong.ONE, UnsignedLong.valueOf(2))); } @Test @@ -313,67 +340,43 @@ public void maybeAdaptPrimitive_optionalValues() { @Test public void adaptProtoToValue_wrapperValues() throws Exception { - CelOptions celOptions = CelOptions.LEGACY; - assertThat(RuntimeHelpers.adaptProtoToValue(DYNAMIC_PROTO, BoolValue.of(true), celOptions)) - .isEqualTo(true); - assertThat( - RuntimeHelpers.adaptProtoToValue( - DYNAMIC_PROTO, BytesValue.of(ByteString.EMPTY), celOptions)) - .isEqualTo(ByteString.EMPTY); - assertThat(RuntimeHelpers.adaptProtoToValue(DYNAMIC_PROTO, DoubleValue.of(1.5d), celOptions)) - .isEqualTo(1.5d); - assertThat(RuntimeHelpers.adaptProtoToValue(DYNAMIC_PROTO, FloatValue.of(1.5f), celOptions)) - .isEqualTo(1.5d); - assertThat(RuntimeHelpers.adaptProtoToValue(DYNAMIC_PROTO, Int32Value.of(12), celOptions)) - .isEqualTo(12L); - assertThat(RuntimeHelpers.adaptProtoToValue(DYNAMIC_PROTO, Int64Value.of(-12L), celOptions)) - .isEqualTo(-12L); - assertThat(RuntimeHelpers.adaptProtoToValue(DYNAMIC_PROTO, UInt32Value.of(123), celOptions)) - .isEqualTo(123L); - assertThat(RuntimeHelpers.adaptProtoToValue(DYNAMIC_PROTO, UInt64Value.of(1234L), celOptions)) - .isEqualTo(1234L); - assertThat(RuntimeHelpers.adaptProtoToValue(DYNAMIC_PROTO, StringValue.of("hello"), celOptions)) - .isEqualTo("hello"); + assertThat(RUNTIME_HELPER.adaptProtoToValue(BoolValue.of(true))).isEqualTo(true); + assertThat(RUNTIME_HELPER.adaptProtoToValue(BytesValue.of(ByteString.EMPTY))) + .isEqualTo(CelByteString.EMPTY); + assertThat(RUNTIME_HELPER.adaptProtoToValue(DoubleValue.of(1.5d))).isEqualTo(1.5d); + assertThat(RUNTIME_HELPER.adaptProtoToValue(FloatValue.of(1.5f))).isEqualTo(1.5d); + assertThat(RUNTIME_HELPER.adaptProtoToValue(Int32Value.of(12))).isEqualTo(12L); + assertThat(RUNTIME_HELPER.adaptProtoToValue(Int64Value.of(-12L))).isEqualTo(-12L); + assertThat(RUNTIME_HELPER.adaptProtoToValue(UInt32Value.of(123))) + .isEqualTo(UnsignedLong.valueOf(123L)); + assertThat(RUNTIME_HELPER.adaptProtoToValue(UInt64Value.of(1234L))) + .isEqualTo(UnsignedLong.valueOf(1234L)); + assertThat(RUNTIME_HELPER.adaptProtoToValue(StringValue.of("hello"))).isEqualTo("hello"); - assertThat( - RuntimeHelpers.adaptProtoToValue( - DYNAMIC_PROTO, - UInt32Value.of(123), - CelOptions.newBuilder().enableUnsignedLongs(true).build())) + assertThat(RUNTIME_HELPER.adaptProtoToValue(UInt32Value.of(123))) .isEqualTo(UnsignedLong.valueOf(123L)); - assertThat( - RuntimeHelpers.adaptProtoToValue( - DYNAMIC_PROTO, - UInt64Value.of(1234L), - CelOptions.newBuilder().enableUnsignedLongs(true).build())) + assertThat(RUNTIME_HELPER.adaptProtoToValue(UInt64Value.of(1234L))) .isEqualTo(UnsignedLong.valueOf(1234L)); } @Test public void adaptProtoToValue_jsonValues() throws Exception { - assertThat( - RuntimeHelpers.adaptProtoToValue( - DYNAMIC_PROTO, - Value.newBuilder().setStringValue("json").build(), - CelOptions.LEGACY)) + assertThat(RUNTIME_HELPER.adaptProtoToValue(Value.newBuilder().setStringValue("json").build())) .isEqualTo("json"); assertThat( - RuntimeHelpers.adaptProtoToValue( - DYNAMIC_PROTO, + RUNTIME_HELPER.adaptProtoToValue( Value.newBuilder() .setListValue( ListValue.newBuilder() .addValues(Value.newBuilder().setNumberValue(1.2d).build())) - .build(), - CelOptions.LEGACY)) + .build())) .isEqualTo(ImmutableList.of(1.2d)); Map mp = new HashMap<>(); - mp.put("list_value", ImmutableList.of(false, NullValue.NULL_VALUE)); + mp.put("list_value", ImmutableList.of(false, dev.cel.common.values.NullValue.NULL_VALUE)); assertThat( - RuntimeHelpers.adaptProtoToValue( - DYNAMIC_PROTO, + RUNTIME_HELPER.adaptProtoToValue( Struct.newBuilder() .putFields( "list_value", @@ -384,8 +387,7 @@ public void adaptProtoToValue_jsonValues() throws Exception { .addValues( Value.newBuilder().setNullValue(NullValue.NULL_VALUE))) .build()) - .build(), - CelOptions.LEGACY)) + .build())) .isEqualTo(mp); } @@ -404,17 +406,13 @@ public void adaptProtoToValue_anyValues() throws Exception { .build()) .build(); Any anyJsonValue = Any.pack(jsonValue); - mp.put("list_value", ImmutableList.of(false, NullValue.NULL_VALUE)); - assertThat(RuntimeHelpers.adaptProtoToValue(DYNAMIC_PROTO, anyJsonValue, CelOptions.LEGACY)) - .isEqualTo(mp); + mp.put("list_value", ImmutableList.of(false, dev.cel.common.values.NullValue.NULL_VALUE)); + assertThat(RUNTIME_HELPER.adaptProtoToValue(anyJsonValue)).isEqualTo(mp); } @Test public void adaptProtoToValue_builderValue() throws Exception { - CelOptions celOptions = CelOptions.LEGACY; - assertThat( - RuntimeHelpers.adaptProtoToValue( - DYNAMIC_PROTO, BoolValue.newBuilder().setValue(true), celOptions)) + assertThat(RUNTIME_HELPER.adaptProtoToValue(BoolValue.newBuilder().setValue(true))) .isEqualTo(true); } diff --git a/runtime/src/test/java/dev/cel/runtime/RuntimeEqualityTest.java b/runtime/src/test/java/dev/cel/runtime/RuntimeEqualityTest.java index 250584f48..00e55873c 100644 --- a/runtime/src/test/java/dev/cel/runtime/RuntimeEqualityTest.java +++ b/runtime/src/test/java/dev/cel/runtime/RuntimeEqualityTest.java @@ -1,4 +1,4 @@ -// Copyright 2022 Google LLC +// 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. @@ -15,672 +15,53 @@ package dev.cel.runtime; 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; import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; import com.google.common.primitives.UnsignedLong; -import com.google.protobuf.Any; -import com.google.protobuf.BoolValue; -import com.google.protobuf.ByteString; -import com.google.protobuf.BytesValue; -import com.google.protobuf.DoubleValue; -import com.google.protobuf.FloatValue; -import com.google.protobuf.Int32Value; -import com.google.protobuf.Int64Value; -import com.google.protobuf.ListValue; -import com.google.protobuf.NullValue; -import com.google.protobuf.StringValue; -import com.google.protobuf.Struct; -import com.google.protobuf.UInt32Value; -import com.google.protobuf.UInt64Value; -import com.google.protobuf.Value; -import com.google.protobuf.util.Durations; -import com.google.protobuf.util.Timestamps; -import com.google.rpc.context.AttributeContext; -import com.google.rpc.context.AttributeContext.Auth; -import com.google.rpc.context.AttributeContext.Peer; -import com.google.rpc.context.AttributeContext.Request; -import dev.cel.common.CelDescriptorUtil; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; import dev.cel.common.CelOptions; -import dev.cel.common.CelRuntimeException; -import dev.cel.common.internal.AdaptingTypes; -import dev.cel.common.internal.BidiConverter; -import dev.cel.common.internal.DefaultDescriptorPool; -import dev.cel.common.internal.DefaultMessageFactory; -import dev.cel.common.internal.DynamicProto; -import java.util.Arrays; -import java.util.List; -import org.jspecify.nullness.Nullable; -import org.junit.Assert; +import dev.cel.expr.conformance.proto2.TestAllTypes; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameter; -import org.junit.runners.Parameterized.Parameters; -@RunWith(Parameterized.class) +@RunWith(TestParameterInjector.class) public final class RuntimeEqualityTest { - private static final CelOptions EMPTY_OPTIONS = - CelOptions.newBuilder().disableCelStandardEquality(false).build(); - private static final CelOptions PROTO_EQUALITY = - CelOptions.newBuilder() - .disableCelStandardEquality(false) - .enableProtoDifferencerEquality(true) - .build(); - private static final CelOptions UNSIGNED_LONGS = - CelOptions.newBuilder().disableCelStandardEquality(false).enableUnsignedLongs(true).build(); - private static final CelOptions PROTO_EQUALITY_UNSIGNED_LONGS = - CelOptions.newBuilder() - .disableCelStandardEquality(false) - .enableProtoDifferencerEquality(true) - .enableUnsignedLongs(true) - .build(); - - private static final RuntimeEquality RUNTIME_EQUALITY = - new RuntimeEquality( - DynamicProto.create( - DefaultMessageFactory.create( - DefaultDescriptorPool.create( - CelDescriptorUtil.getAllDescriptorsFromFileDescriptor( - AttributeContext.getDescriptor().getFile()))))); - - @Test - public void inMap() throws Exception { - CelOptions celOptions = CelOptions.newBuilder().disableCelStandardEquality(false).build(); - ImmutableMap map = ImmutableMap.of("key", "value", "key2", "value2"); - assertThat(RUNTIME_EQUALITY.inMap(map, "key2", celOptions)).isTrue(); - assertThat(RUNTIME_EQUALITY.inMap(map, "key3", celOptions)).isFalse(); - - ImmutableMap mixedKeyMap = - ImmutableMap.of( - "key", "value", 2L, "value2", UnsignedLong.valueOf(42), "answer to everything"); - // Integer tests. - assertThat(RUNTIME_EQUALITY.inMap(mixedKeyMap, 2, celOptions)).isTrue(); - assertThat(RUNTIME_EQUALITY.inMap(mixedKeyMap, 3, celOptions)).isFalse(); - - // Long tests. - assertThat(RUNTIME_EQUALITY.inMap(mixedKeyMap, -1L, celOptions)).isFalse(); - assertThat(RUNTIME_EQUALITY.inMap(mixedKeyMap, 3L, celOptions)).isFalse(); - assertThat(RUNTIME_EQUALITY.inMap(mixedKeyMap, 2L, celOptions)).isTrue(); - assertThat(RUNTIME_EQUALITY.inMap(mixedKeyMap, 42L, celOptions)).isTrue(); - - // Floating point tests - assertThat(RUNTIME_EQUALITY.inMap(mixedKeyMap, -1.0d, celOptions)).isFalse(); - assertThat(RUNTIME_EQUALITY.inMap(mixedKeyMap, 2.1d, celOptions)).isFalse(); - assertThat( - RUNTIME_EQUALITY.inMap(mixedKeyMap, UnsignedLong.MAX_VALUE.doubleValue(), celOptions)) - .isFalse(); - assertThat(RUNTIME_EQUALITY.inMap(mixedKeyMap, 2.0d, celOptions)).isTrue(); - assertThat(RUNTIME_EQUALITY.inMap(mixedKeyMap, Double.NaN, celOptions)).isFalse(); - - // Unsigned long tests. - assertThat(RUNTIME_EQUALITY.inMap(mixedKeyMap, UnsignedLong.valueOf(1L), celOptions)).isFalse(); - assertThat(RUNTIME_EQUALITY.inMap(mixedKeyMap, UnsignedLong.valueOf(2L), celOptions)).isTrue(); - assertThat(RUNTIME_EQUALITY.inMap(mixedKeyMap, UnsignedLong.MAX_VALUE, celOptions)).isFalse(); - assertThat(RUNTIME_EQUALITY.inMap(mixedKeyMap, UInt64Value.of(2L), celOptions)).isTrue(); - - // Validate the legacy behavior as well. - assertThat(RUNTIME_EQUALITY.inMap(mixedKeyMap, 2, CelOptions.LEGACY)).isFalse(); - assertThat(RUNTIME_EQUALITY.inMap(mixedKeyMap, 2L, CelOptions.LEGACY)).isTrue(); - assertThat(RUNTIME_EQUALITY.inMap(mixedKeyMap, Int64Value.of(2L), CelOptions.LEGACY)).isFalse(); - assertThat(RUNTIME_EQUALITY.inMap(mixedKeyMap, UInt64Value.of(2L), CelOptions.LEGACY)) - .isFalse(); - } - - @Test - public void inList() throws Exception { - CelOptions celOptions = CelOptions.newBuilder().disableCelStandardEquality(false).build(); - ImmutableList list = ImmutableList.of("value", "value2"); - assertThat(RUNTIME_EQUALITY.inList(list, "value", celOptions)).isTrue(); - assertThat(RUNTIME_EQUALITY.inList(list, "value3", celOptions)).isFalse(); - - ImmutableList mixedValueList = ImmutableList.of(1, "value", 2, "value2"); - assertThat(RUNTIME_EQUALITY.inList(mixedValueList, 2, celOptions)).isTrue(); - assertThat(RUNTIME_EQUALITY.inList(mixedValueList, 3, celOptions)).isFalse(); - - assertThat(RUNTIME_EQUALITY.inList(mixedValueList, 2L, celOptions)).isTrue(); - assertThat(RUNTIME_EQUALITY.inList(mixedValueList, 3L, celOptions)).isFalse(); - - assertThat(RUNTIME_EQUALITY.inList(mixedValueList, 2.0, celOptions)).isTrue(); - assertThat(RUNTIME_EQUALITY.inList(mixedValueList, Double.NaN, celOptions)).isFalse(); - - assertThat(RUNTIME_EQUALITY.inList(mixedValueList, UnsignedLong.valueOf(2L), celOptions)) - .isTrue(); - assertThat(RUNTIME_EQUALITY.inList(mixedValueList, UnsignedLong.valueOf(3L), celOptions)) - .isFalse(); - - // Validate the legacy behavior as well. - assertThat(RUNTIME_EQUALITY.inList(mixedValueList, 2, CelOptions.LEGACY)).isTrue(); - assertThat(RUNTIME_EQUALITY.inList(mixedValueList, 2L, CelOptions.LEGACY)).isFalse(); - } @Test - public void indexMap() throws Exception { - ImmutableMap mixedKeyMap = - ImmutableMap.of(1L, "value", UnsignedLong.valueOf(2L), "value2"); - assertThat(RUNTIME_EQUALITY.indexMap(mixedKeyMap, 1.0, CelOptions.DEFAULT)).isEqualTo("value"); - assertThat(RUNTIME_EQUALITY.indexMap(mixedKeyMap, 2.0, CelOptions.DEFAULT)).isEqualTo("value2"); - Assert.assertThrows( - CelRuntimeException.class, - () -> RUNTIME_EQUALITY.indexMap(mixedKeyMap, 1.0, CelOptions.LEGACY)); - Assert.assertThrows( - CelRuntimeException.class, - () -> RUNTIME_EQUALITY.indexMap(mixedKeyMap, 1.1, CelOptions.DEFAULT)); + public void objectEquals_and_hashCode() { + RuntimeEquality runtimeEquality = + RuntimeEquality.create(RuntimeHelpers.create(), CelOptions.DEFAULT); + assertEqualityAndHashCode(runtimeEquality, 1, 1); + assertEqualityAndHashCode(runtimeEquality, 2, 2L); + assertEqualityAndHashCode(runtimeEquality, 3, 3.0); + assertEqualityAndHashCode(runtimeEquality, 4, UnsignedLong.valueOf(4)); + assertEqualityAndHashCode( + runtimeEquality, + ImmutableList.of(1, 2, 3), + ImmutableList.of(1.0, 2L, UnsignedLong.valueOf(3))); + assertEqualityAndHashCode( + runtimeEquality, + ImmutableMap.of("a", 1, "b", 2), + ImmutableMap.of("a", 1L, "b", UnsignedLong.valueOf(2))); } - @AutoValue - abstract static class State { - /** - * Expected comparison outcome when equality is performed with the given options. - * - *

The {@code null} value indicates that the outcome is an error. - */ - public abstract @Nullable Boolean outcome(); - - /** Set of options to use when performing the equality check. */ - public abstract CelOptions celOptions(); - - public static State create(@Nullable Boolean outcome, CelOptions celOptions) { - return new AutoValue_RuntimeEqualityTest_State(outcome, celOptions); - } - } - - /** Represents expected result states for an equality test case. */ - @AutoValue - abstract static class Result { - - /** The result {@code State} value associated with different feature flag combinations. */ - public abstract ImmutableSet states(); - - /** - * Creates a Result for a comparison that is undefined (throws an Exception) under both equality - * modes. - */ - public static Result undefined() { - return always(null); - } - - /** Creates a Result for a comparison that is false under both equality modes. */ - public static Result alwaysFalse() { - return always(false); - } - - /** Creates a Result for a comparison that is true under both equality modes. */ - public static Result alwaysTrue() { - return always(true); - } - - public static Result signed(Boolean outcome) { - return Result.builder() - .states( - ImmutableList.of( - State.create(outcome, EMPTY_OPTIONS), State.create(outcome, PROTO_EQUALITY))) - .build(); - } - - public static Result unsigned(Boolean outcome) { - return Result.builder() - .states( - ImmutableList.of( - State.create(outcome, UNSIGNED_LONGS), - State.create(outcome, PROTO_EQUALITY_UNSIGNED_LONGS))) - .build(); - } - - private static Result always(@Nullable Boolean outcome) { - return Result.builder() - .states( - ImmutableList.of( - State.create(outcome, EMPTY_OPTIONS), - State.create(outcome, PROTO_EQUALITY), - State.create(outcome, PROTO_EQUALITY_UNSIGNED_LONGS))) - .build(); - } - - private static Result proto(Boolean equalsOutcome, Boolean diffOutcome) { - return Result.builder() - .states( - ImmutableList.of( - State.create(equalsOutcome, EMPTY_OPTIONS), - State.create(diffOutcome, PROTO_EQUALITY), - State.create(diffOutcome, PROTO_EQUALITY_UNSIGNED_LONGS))) - .build(); - } - - public static Builder builder() { - return new AutoValue_RuntimeEqualityTest_Result.Builder(); - } - - @AutoValue.Builder - public abstract static class Builder { - abstract Builder states(ImmutableList states); - - abstract Result build(); - } - } - - @Parameter(0) - public Object lhs; - - @Parameter(1) - public Object rhs; - - @Parameter(2) - public Result result; - - @Parameters - public static List data() { - return Arrays.asList( - new Object[][] { - // Boolean tests. - {true, true, Result.alwaysTrue()}, - {BoolValue.of(true), true, Result.alwaysTrue()}, - {Any.pack(BoolValue.of(true)), true, Result.alwaysTrue()}, - {Value.newBuilder().setBoolValue(true).build(), true, Result.alwaysTrue()}, - {true, false, Result.alwaysFalse()}, - {0, false, Result.alwaysFalse()}, - - // Bytes tests. - {ByteString.copyFromUtf8("h¢"), ByteString.copyFromUtf8("h¢"), Result.alwaysTrue()}, - {ByteString.copyFromUtf8("hello"), ByteString.EMPTY, Result.alwaysFalse()}, - {BytesValue.of(ByteString.EMPTY), ByteString.EMPTY, Result.alwaysTrue()}, - { - BytesValue.of(ByteString.copyFromUtf8("h¢")), - ByteString.copyFromUtf8("h¢"), - Result.alwaysTrue() - }, - {Any.pack(BytesValue.of(ByteString.EMPTY)), ByteString.EMPTY, Result.alwaysTrue()}, - {"h¢", ByteString.copyFromUtf8("h¢"), Result.alwaysFalse()}, - - // Double tests. - {1.0, 1.0, Result.alwaysTrue()}, - {Double.valueOf(1.0), 1.0, Result.alwaysTrue()}, - {DoubleValue.of(42.5), 42.5, Result.alwaysTrue()}, - // Floats are unwrapped to double types. - {FloatValue.of(1.0f), 1.0, Result.alwaysTrue()}, - {Value.newBuilder().setNumberValue(-1.5D).build(), -1.5, Result.alwaysTrue()}, - {1.0, -1.0, Result.alwaysFalse()}, - {1.0, 1.0D, Result.alwaysTrue()}, - {1.0, 1.1D, Result.alwaysFalse()}, - {1.0D, 1.1f, Result.alwaysFalse()}, - {1.0, 1, Result.alwaysTrue()}, - - // Float tests. - {1.0f, 1.0f, Result.alwaysTrue()}, - {Float.valueOf(1.0f), 1.0f, Result.alwaysTrue()}, - {1.0f, -1.0f, Result.alwaysFalse()}, - {1.0f, 1.0, Result.alwaysTrue()}, - - // Integer tests. - {16, 16, Result.alwaysTrue()}, - {17, 16, Result.alwaysFalse()}, - {17, 16.0, Result.alwaysFalse()}, - - // Long tests. - {-15L, -15L, Result.alwaysTrue()}, - // Int32 values are unwrapped to int types. - {Int32Value.of(-15), -15L, Result.alwaysTrue()}, - {Int64Value.of(-15L), -15L, Result.alwaysTrue()}, - {Any.pack(Int32Value.of(-15)), -15L, Result.alwaysTrue()}, - {Any.pack(Int64Value.of(-15L)), -15L, Result.alwaysTrue()}, - {-15L, -16L, Result.alwaysFalse()}, - {-15L, -15, Result.alwaysTrue()}, - {-15L, 15.0, Result.alwaysFalse()}, - - // Null tests. - {null, null, Result.alwaysTrue()}, - {false, null, Result.alwaysFalse()}, - {0.0, null, Result.alwaysFalse()}, - {0, null, Result.alwaysFalse()}, - {null, "null", Result.alwaysFalse()}, - {"null", null, Result.alwaysFalse()}, - {null, NullValue.NULL_VALUE, Result.alwaysTrue()}, - {null, ImmutableList.of(), Result.alwaysFalse()}, - {ImmutableMap.of(), null, Result.alwaysFalse()}, - {ByteString.copyFromUtf8(""), null, Result.alwaysFalse()}, - {null, Timestamps.EPOCH, Result.alwaysFalse()}, - {Durations.ZERO, null, Result.alwaysFalse()}, - {NullValue.NULL_VALUE, NullValue.NULL_VALUE, Result.alwaysTrue()}, - { - Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build(), - NullValue.NULL_VALUE, - Result.alwaysTrue() - }, - { - Any.pack(Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build()), - NullValue.NULL_VALUE, - Result.alwaysTrue() - }, - - // String tests. - {"", "", Result.alwaysTrue()}, - {"str", "str", Result.alwaysTrue()}, - {StringValue.of("str"), "str", Result.alwaysTrue()}, - {Value.newBuilder().setStringValue("str").build(), "str", Result.alwaysTrue()}, - {Any.pack(StringValue.of("str")), "str", Result.alwaysTrue()}, - {Any.pack(Value.newBuilder().setStringValue("str").build()), "str", Result.alwaysTrue()}, - {"", "non-empty", Result.alwaysFalse()}, - - // Uint tests. - {UInt32Value.of(1234), 1234L, Result.alwaysTrue()}, - {UInt64Value.of(1234L), 1234L, Result.alwaysTrue()}, - {UInt64Value.of(1234L), Int64Value.of(1234L), Result.alwaysTrue()}, - {UInt32Value.of(1234), UnsignedLong.valueOf(1234L), Result.alwaysTrue()}, - {UInt64Value.of(1234L), UnsignedLong.valueOf(1234L), Result.alwaysTrue()}, - {Any.pack(UInt64Value.of(1234L)), UnsignedLong.valueOf(1234L), Result.alwaysTrue()}, - {UInt32Value.of(123), UnsignedLong.valueOf(1234L), Result.alwaysFalse()}, - {UInt64Value.of(123L), UnsignedLong.valueOf(1234L), Result.alwaysFalse()}, - {Any.pack(UInt64Value.of(123L)), UnsignedLong.valueOf(1234L), Result.alwaysFalse()}, - - // Cross-type equality tests. - {UInt32Value.of(1234), 1234.0, Result.alwaysTrue()}, - {UInt32Value.of(1234), 1234.0, Result.alwaysTrue()}, - {UInt64Value.of(1234L), 1234L, Result.alwaysTrue()}, - {UInt32Value.of(1234), 1234.1, Result.alwaysFalse()}, - {UInt64Value.of(1234L), 1233L, Result.alwaysFalse()}, - {UnsignedLong.valueOf(1234L), 1234L, Result.alwaysTrue()}, - {UnsignedLong.valueOf(1234L), 1234.1, Result.alwaysFalse()}, - {1234L, 1233.2, Result.alwaysFalse()}, - {-1234L, UnsignedLong.valueOf(1233L), Result.alwaysFalse()}, - - // List tests. - // Note, this list equality behaves equivalently to the following expression: - // 1.0 == 1.0 && "dos" == 2.0 && 3.0 == 4.0 - // The middle predicate is an error; however, the last comparison yields false and so - - // the error is short-circuited away. - {Arrays.asList(1.0, "dos", 3.0), Arrays.asList(1.0, 2.0, 4.0), Result.alwaysFalse()}, - {Arrays.asList("1", 2), ImmutableList.of("1", 2), Result.alwaysTrue()}, - {Arrays.asList("1", 2), ImmutableSet.of("1", 2), Result.alwaysTrue()}, - {Arrays.asList(1.0, 2.0, 3.0), Arrays.asList(1.0, 2.0), Result.alwaysFalse()}, - {Arrays.asList(1.0, 3.0), Arrays.asList(1.0, 2.0), Result.alwaysFalse()}, - { - AdaptingTypes.adaptingList( - ImmutableList.of(1, 2, 3), - BidiConverter.of(RuntimeHelpers.INT32_TO_INT64, RuntimeHelpers.INT64_TO_INT32)), - Arrays.asList(1L, 2L, 3L), - Result.alwaysTrue() - }, - { - ListValue.newBuilder() - .addValues(Value.newBuilder().setStringValue("hello")) - .addValues(Value.newBuilder().setStringValue("world")) - .build(), - ImmutableList.of("hello", "world"), - Result.alwaysTrue() - }, - { - ListValue.newBuilder() - .addValues(Value.newBuilder().setStringValue("hello")) - .addValues(Value.newBuilder().setListValue(ListValue.getDefaultInstance())) - .build(), - ImmutableList.of("hello", "world"), - Result.alwaysFalse() - }, - { - ListValue.newBuilder() - .addValues(Value.newBuilder().setListValue(ListValue.getDefaultInstance())) - .addValues( - Value.newBuilder() - .setListValue( - ListValue.newBuilder() - .addValues(Value.newBuilder().setBoolValue(true)))) - .build(), - ImmutableList.of(ImmutableList.of(), ImmutableList.of(true)), - Result.alwaysTrue() - }, - { - Value.newBuilder() - .setListValue( - ListValue.newBuilder() - .addValues(Value.newBuilder().setNumberValue(-1.5)) - .addValues(Value.newBuilder().setNumberValue(42.25))) - .build(), - AdaptingTypes.adaptingList( - ImmutableList.of(-1.5f, 42.25f), - BidiConverter.of(RuntimeHelpers.FLOAT_TO_DOUBLE, RuntimeHelpers.DOUBLE_TO_FLOAT)), - Result.alwaysTrue() - }, - - // Map tests. - {ImmutableMap.of("one", 1), ImmutableMap.of("one", "uno"), Result.alwaysFalse()}, - {ImmutableMap.of("two", 2), ImmutableMap.of("two", 3), Result.alwaysFalse()}, - {ImmutableMap.of("one", 2), ImmutableMap.of("two", 3), Result.alwaysFalse()}, - // Note, this map is the composition of the following two tests above where: - // ("one", 1) == ("one", "uno") -> error - // ("two", 2) == ("two", 3) -> false - // Within CEL error && false -> false, and the key order in the test has specifically - // been chosen to exercise this behavior. - { - ImmutableMap.of("one", 1, "two", 2), - ImmutableMap.of("one", "uno", "two", 3), - Result.alwaysFalse() - }, - {ImmutableMap.of("key", "value"), ImmutableMap.of("key", "value"), Result.alwaysTrue()}, - {ImmutableMap.of(), ImmutableMap.of("key", "value"), Result.alwaysFalse()}, - {ImmutableMap.of("key", "value"), ImmutableMap.of("key", "diff"), Result.alwaysFalse()}, - {ImmutableMap.of("key", 42), ImmutableMap.of("key", 42L), Result.alwaysTrue()}, - {ImmutableMap.of("key", 42.0), ImmutableMap.of("key", 42L), Result.alwaysTrue()}, - { - AdaptingTypes.adaptingMap( - ImmutableMap.of("key1", 42, "key2", 31, "key3", 20), - BidiConverter.identity(), - BidiConverter.of(RuntimeHelpers.INT32_TO_INT64, RuntimeHelpers.INT64_TO_INT32)), - ImmutableMap.of("key1", 42L, "key2", 31L, "key3", 20L), - Result.alwaysTrue() - }, - { - AdaptingTypes.adaptingMap( - ImmutableMap.of(1, 42.5f, 2, 31f, 3, 20.25f), - BidiConverter.of(RuntimeHelpers.INT32_TO_INT64, RuntimeHelpers.INT64_TO_INT32), - BidiConverter.of(RuntimeHelpers.FLOAT_TO_DOUBLE, RuntimeHelpers.DOUBLE_TO_FLOAT)), - ImmutableMap.of(1L, 42.5D, 2L, 31D, 3L, 20.25D), - Result.alwaysTrue() - }, - { - AdaptingTypes.adaptingMap( - ImmutableMap.of("1", 42.5f, "2", 31f, "3", 20.25f), - BidiConverter.identity(), - BidiConverter.of(RuntimeHelpers.FLOAT_TO_DOUBLE, RuntimeHelpers.DOUBLE_TO_FLOAT)), - Struct.getDefaultInstance(), - Result.alwaysFalse() - }, - { - AdaptingTypes.adaptingMap( - ImmutableMap.of("1", 42.5f, "2", 31f, "3", 20.25f), - BidiConverter.identity(), - BidiConverter.of(RuntimeHelpers.FLOAT_TO_DOUBLE, RuntimeHelpers.DOUBLE_TO_FLOAT)), - Struct.newBuilder() - .putFields("1", Value.newBuilder().setNumberValue(42.5D).build()) - .putFields("2", Value.newBuilder().setNumberValue(31D).build()) - .putFields("3", Value.newBuilder().setNumberValue(20.25D).build()) - .build(), - Result.alwaysTrue() - }, - { - AdaptingTypes.adaptingMap( - ImmutableMap.of("1", 42.5f, "2", 31f, "3", 20.25f), - BidiConverter.identity(), - BidiConverter.of(RuntimeHelpers.FLOAT_TO_DOUBLE, RuntimeHelpers.DOUBLE_TO_FLOAT)), - Struct.newBuilder() - .putFields("1", Value.newBuilder().setNumberValue(42.5D).build()) - .putFields("2", Value.newBuilder().setNumberValue(31D).build()) - .putFields("3", Value.newBuilder().setStringValue("oops").build()) - .build(), - Result.alwaysFalse() - }, - - // Protobuf tests. - { - AttributeContext.newBuilder().setRequest(Request.getDefaultInstance()).build(), - AttributeContext.newBuilder().setRequest(Request.newBuilder().setHost("")).build(), - Result.alwaysTrue() - }, - { - AttributeContext.newBuilder() - .setRequest(Request.getDefaultInstance()) - .setOrigin(Peer.getDefaultInstance()) - .build(), - AttributeContext.newBuilder().setRequest(Request.getDefaultInstance()).build(), - Result.alwaysFalse() - }, - // Proto differencer unpacks any values. - { - AttributeContext.newBuilder() - .addExtensions( - Any.newBuilder() - .setTypeUrl("type.googleapis.com/google.rpc.context.AttributeContext") - .setValue(ByteString.copyFromUtf8("\032\000:\000")) - .build()) - .build(), - AttributeContext.newBuilder() - .addExtensions( - Any.newBuilder() - .setTypeUrl("type.googleapis.com/google.rpc.context.AttributeContext") - .setValue(ByteString.copyFromUtf8(":\000\032\000")) - .build()) - .build(), - Result.builder() - .states( - ImmutableList.of( - State.create(false, EMPTY_OPTIONS), State.create(true, PROTO_EQUALITY))) - .build() - }, - // If type url is missing, fallback to bytes comparison for payload. - { - AttributeContext.newBuilder() - .addExtensions( - Any.newBuilder().setValue(ByteString.copyFromUtf8("\032\000:\000")).build()) - .build(), - AttributeContext.newBuilder() - .addExtensions( - Any.newBuilder().setValue(ByteString.copyFromUtf8(":\000\032\000")).build()) - .build(), - Result.alwaysFalse() - }, - { - AttributeContext.newBuilder() - .setRequest(Request.getDefaultInstance()) - .setOrigin(Peer.getDefaultInstance()) - .build(), - "test string", - Result.alwaysFalse() - }, - { - AttributeContext.newBuilder() - .setRequest(Request.getDefaultInstance()) - .setOrigin(Peer.getDefaultInstance()) - .build(), - null, - Result.alwaysFalse() - }, - { - AttributeContext.newBuilder() - .addExtensions( - Any.pack( - AttributeContext.newBuilder() - .setRequest(Request.getDefaultInstance()) - .setOrigin(Peer.getDefaultInstance()) - .build())) - .build(), - AttributeContext.newBuilder() - .addExtensions( - Any.pack( - AttributeContext.newBuilder() - .setRequest(Request.getDefaultInstance()) - .build())) - .build(), - Result.alwaysFalse() - }, - { - AttributeContext.getDefaultInstance(), - AttributeContext.newBuilder() - .setRequest(Request.newBuilder().setHost("localhost")) - .build(), - Result.alwaysFalse() - }, - // Differently typed messages aren't comparable. - {AttributeContext.getDefaultInstance(), Auth.getDefaultInstance(), Result.alwaysFalse()}, - // Message.equals() treats NaN values as equal. Message differencer treats NaN values - // as inequal (the same behavior as the C++ implementation). - { - AttributeContext.newBuilder() - .setRequest( - Request.newBuilder() - .setAuth( - Auth.newBuilder() - .setClaims( - Struct.newBuilder() - .putFields( - "custom", - Value.newBuilder() - .setNumberValue(Double.NaN) - .build())))) - .build(), - AttributeContext.newBuilder() - .setRequest( - Request.newBuilder() - .setAuth( - Auth.newBuilder() - .setClaims( - Struct.newBuilder() - .putFields( - "custom", - Value.newBuilder() - .setNumberValue(Double.NaN) - .build())))) - .build(), - Result.proto(/* equalsOutcome= */ true, /* diffOutcome= */ false), - }, - - // Note: this is the motivating use case for converting to heterogeneous equality in - // the future. - { - AttributeContext.newBuilder() - .setRequest( - Request.newBuilder() - .setAuth( - Auth.newBuilder() - .setClaims( - Struct.newBuilder() - .putFields( - "custom", - Value.newBuilder().setNumberValue(123.0).build())))) - .build(), - AttributeContext.newBuilder() - .setRequest( - Request.newBuilder() - .setAuth( - Auth.newBuilder() - .setClaims( - Struct.newBuilder() - .putFields( - "custom", - Value.newBuilder().setBoolValue(true).build())))) - .build(), - Result.alwaysFalse(), - }, - }); + private void assertEqualityAndHashCode(RuntimeEquality runtimeEquality, Object obj1, Object obj2) { + assertThat(runtimeEquality.objectEquals(obj1, obj2)).isTrue(); + assertThat(runtimeEquality.hashCode(obj1)).isEqualTo(runtimeEquality.hashCode(obj2)); } @Test - public void objectEquals() throws Exception { - for (State state : result.states()) { - if (state.outcome() == null) { - Assert.assertThrows( - CelRuntimeException.class, - () -> RUNTIME_EQUALITY.objectEquals(lhs, rhs, state.celOptions())); - Assert.assertThrows( - CelRuntimeException.class, - () -> RUNTIME_EQUALITY.objectEquals(rhs, lhs, state.celOptions())); - return; - } - assertThat(RUNTIME_EQUALITY.objectEquals(lhs, rhs, state.celOptions())) - .isEqualTo(state.outcome()); - assertThat(RUNTIME_EQUALITY.objectEquals(rhs, lhs, state.celOptions())) - .isEqualTo(state.outcome()); - } + public void objectEquals_messageLite_throws() { + RuntimeEquality runtimeEquality = + RuntimeEquality.create(RuntimeHelpers.create(), CelOptions.DEFAULT); + + // Unimplemented until CelLiteDescriptor is available. + assertThrows( + UnsupportedOperationException.class, + () -> + runtimeEquality.objectEquals( + TestAllTypes.newBuilder(), TestAllTypes.getDefaultInstance())); } } diff --git a/runtime/src/test/java/dev/cel/runtime/TypeResolverTest.java b/runtime/src/test/java/dev/cel/runtime/TypeResolverTest.java new file mode 100644 index 000000000..0437a31e5 --- /dev/null +++ b/runtime/src/test/java/dev/cel/runtime/TypeResolverTest.java @@ -0,0 +1,92 @@ +// 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 static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.primitives.UnsignedLong; +import com.google.protobuf.NullValue; +import com.google.testing.junit.testparameterinjector.TestParameter; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.common.internal.ProtoTimeUtils; +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.TypeType; +import dev.cel.common.values.CelValueConverter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Optional; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public class TypeResolverTest { + private static final TypeResolver TYPE_RESOLVER = + TypeResolver.create(CelValueConverter.getDefaultInstance()); + + @Test + public void resolveWellKnownObjectType_sentinelRuntimeType() { + Optional resolvedType = + TYPE_RESOLVER.resolveWellKnownObjectType(TypeType.create(SimpleType.INT)); + + assertThat(resolvedType).hasValue(TypeResolver.RUNTIME_TYPE_TYPE); + } + + @Test + public void resolveWellKnownObjectType_commonType( + @TestParameter WellKnownObjectTestCase testCase) { + Optional resolvedType = TYPE_RESOLVER.resolveWellKnownObjectType(testCase.obj); + + assertThat(resolvedType).hasValue(testCase.expectedTypeType); + } + + @Test + public void resolveWellKnownObjectType_unknownObjectType_returnsEmpty() { + Optional resolvedType = TYPE_RESOLVER.resolveWellKnownObjectType(new Object()); + + assertThat(resolvedType).isEmpty(); + } + + @SuppressWarnings("ImmutableEnumChecker") // Test only + private enum WellKnownObjectTestCase { + BOOLEAN(true, TypeType.create(SimpleType.BOOL)), + DOUBLE(1.0, TypeType.create(SimpleType.DOUBLE)), + LONG(1L, TypeType.create(SimpleType.INT)), + UNSIGNED_LONG(UnsignedLong.valueOf(1L), TypeType.create(SimpleType.UINT)), + STRING("test", TypeType.create(SimpleType.STRING)), + NULL(NullValue.NULL_VALUE, TypeType.create(SimpleType.NULL_TYPE)), + DURATION(ProtoTimeUtils.fromSecondsToDuration(1), TypeType.create(SimpleType.DURATION)), + TIMESTAMP(ProtoTimeUtils.fromSecondsToTimestamp(1), TypeType.create(SimpleType.TIMESTAMP)), + ARRAY_LIST(new ArrayList<>(), TypeType.create(ListType.create(SimpleType.DYN))), + IMMUTABLE_LIST(ImmutableList.of(), TypeType.create(ListType.create(SimpleType.DYN))), + HASH_MAP(new HashMap<>(), TypeType.create(MapType.create(SimpleType.DYN, SimpleType.DYN))), + IMMUTABLE_MAP( + ImmutableMap.of(), TypeType.create(MapType.create(SimpleType.DYN, SimpleType.DYN))), + OPTIONAL(Optional.empty(), TypeType.create(OptionalType.create(SimpleType.DYN))); + ; + + private final Object obj; + private final TypeType expectedTypeType; + + WellKnownObjectTestCase(Object obj, TypeType expectedTypeType) { + this.obj = obj; + this.expectedTypeType = expectedTypeType; + } + } +} 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 599a76835..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,36 +1,42 @@ +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_truth", "//bundle:cel", - "//common", + "//common:cel_ast", + "//common:container", "//common:options", - "//common/resources/testdata/proto3:test_all_types_java_proto", "//common/testing", "//common/types", + # "//java/com/google/testing/testsize:annotations", "//runtime", "//runtime:unknown_attributes", "//runtime:unknown_options", "//runtime/async", - "@cel_spec//proto/cel/expr:expr_java_proto", - "@maven//:com_google_guava_guava", - "@maven//:com_google_testparameterinjector_test_parameter_injector", "@maven//:junit_junit", + "@maven//:com_google_testparameterinjector_test_parameter_injector", + "//:java_truth", + "@cel_spec//proto/cel/expr:checked_java_proto", + "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", + "@maven//:com_google_guava_guava", ], ) junit4_test_suites( name = "test_suites", + shard_count = 4, sizes = [ - "small", + "medium", ], src_dir = "src/test/java", deps = [":tests"], diff --git a/runtime/src/test/java/dev/cel/runtime/async/CelAsyncRuntimeImplTest.java b/runtime/src/test/java/dev/cel/runtime/async/CelAsyncRuntimeImplTest.java index 4eebb499b..d24e99860 100644 --- a/runtime/src/test/java/dev/cel/runtime/async/CelAsyncRuntimeImplTest.java +++ b/runtime/src/test/java/dev/cel/runtime/async/CelAsyncRuntimeImplTest.java @@ -27,18 +27,19 @@ import com.google.common.util.concurrent.SettableFuture; import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; +// import com.google.testing.testsize.MediumTest; 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.testing.RepeatedTestProvider; import dev.cel.common.types.SimpleType; +import dev.cel.expr.conformance.proto3.TestAllTypes; import dev.cel.runtime.CelAttributeParser; import dev.cel.runtime.CelAttributePattern; import dev.cel.runtime.CelEvaluationException; -import dev.cel.runtime.UnknownContext; import dev.cel.runtime.async.CelAsyncRuntime.AsyncProgram; -import dev.cel.testing.testdata.proto3.TestAllTypesProto.TestAllTypes; import java.time.Duration; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; @@ -48,6 +49,7 @@ import org.junit.runner.RunWith; @RunWith(TestParameterInjector.class) +// @MediumTest public final class CelAsyncRuntimeImplTest { @Test @@ -67,18 +69,12 @@ public void asyncProgram_basicUnknownResolution() throws Exception { .addVar("com.google.var2", SimpleType.STRING) .addVar("com.google.var3", SimpleType.STRING) .setResultType(SimpleType.BOOL) - .setContainer("com.google") + .setContainer(CelContainer.ofName("com.google")) .build(); CelAsyncRuntime asyncRuntime = CelAsyncRuntimeFactory.defaultAsyncRuntime() .setRuntime(cel) - .addResolvableAttributePattern( - CelAttributePattern.fromQualifiedIdentifier("com.google.var1"), resolveName) - .addResolvableAttributePattern( - CelAttributePattern.fromQualifiedIdentifier("com.google.var2"), resolveName) - .addResolvableAttributePattern( - CelAttributePattern.fromQualifiedIdentifier("com.google.var3"), resolveName) .setExecutorService(newDirectExecutorService()) .build(); @@ -90,11 +86,16 @@ public void asyncProgram_basicUnknownResolution() throws Exception { .getAst(); AsyncProgram program = asyncRuntime.createProgram(ast); - // empty starting context - UnknownContext context = asyncRuntime.newAsyncContext(); // Act - ListenableFuture future = program.evaluateToCompletion(context); + ListenableFuture future = + program.evaluateToCompletion( + CelResolvableAttributePattern.of( + CelAttributePattern.fromQualifiedIdentifier("com.google.var1"), resolveName), + CelResolvableAttributePattern.of( + CelAttributePattern.fromQualifiedIdentifier("com.google.var2"), resolveName), + CelResolvableAttributePattern.of( + CelAttributePattern.fromQualifiedIdentifier("com.google.var3"), resolveName)); Object result = future.get(2, SECONDS); // Assert @@ -103,11 +104,61 @@ public void asyncProgram_basicUnknownResolution() throws Exception { } @Test - public void asyncProgram_basicAsyncResovler() throws Exception { + public void asyncProgram_sequentialUnknownResolution() throws Exception { // Arrange - final SettableFuture var1 = SettableFuture.create(); - final SettableFuture var2 = SettableFuture.create(); - final SettableFuture var3 = SettableFuture.create(); + CelUnknownAttributeValueResolver resolveName = + CelUnknownAttributeValueResolver.fromResolver( + (attr) -> { + Thread.sleep(500); + return attr.toString(); + }); + Cel cel = + CelFactory.standardCelBuilder() + .setOptions(CelOptions.current().enableUnknownTracking(true).build()) + .addMessageTypes(TestAllTypes.getDescriptor()) + .addVar("com.google.var1", SimpleType.BOOL) + .addVar("com.google.var2", SimpleType.STRING) + .addVar("com.google.var3", SimpleType.STRING) + .setResultType(SimpleType.STRING) + .setContainer(CelContainer.ofName("com.google")) + .build(); + + CelAsyncRuntime asyncRuntime = + CelAsyncRuntimeFactory.defaultAsyncRuntime() + .setRuntime(cel) + .setExecutorService(newDirectExecutorService()) + .build(); + + CelAbstractSyntaxTree ast = + cel.compile( + "var1 ? var2 : var3") + .getAst(); + + AsyncProgram program = asyncRuntime.createProgram(ast); + + // Act + ListenableFuture future = + program.evaluateToCompletion( + CelResolvableAttributePattern.of( + CelAttributePattern.fromQualifiedIdentifier("com.google.var1"), + CelUnknownAttributeValueResolver.fromResolver(unused -> true)), + CelResolvableAttributePattern.of( + CelAttributePattern.fromQualifiedIdentifier("com.google.var2"), resolveName), + CelResolvableAttributePattern.of( + CelAttributePattern.fromQualifiedIdentifier("com.google.var3"), resolveName)); + Object result = future.get(2, SECONDS); + + // Assert + assertThat(result).isInstanceOf(String.class); + assertThat(result).isEqualTo("com.google.var2"); + } + + @Test + public void asyncProgram_basicAsyncResolver() throws Exception { + // Arrange + SettableFuture var1 = SettableFuture.create(); + SettableFuture var2 = SettableFuture.create(); + SettableFuture var3 = SettableFuture.create(); Cel cel = CelFactory.standardCelBuilder() @@ -117,21 +168,12 @@ public void asyncProgram_basicAsyncResovler() throws Exception { .addVar("com.google.var2", SimpleType.STRING) .addVar("com.google.var3", SimpleType.STRING) .setResultType(SimpleType.BOOL) - .setContainer("com.google") + .setContainer(CelContainer.ofName("com.google")) .build(); CelAsyncRuntime asyncRuntime = CelAsyncRuntimeFactory.defaultAsyncRuntime() .setRuntime(cel) - .addResolvableAttributePattern( - CelAttributePattern.fromQualifiedIdentifier("com.google.var1"), - CelUnknownAttributeValueResolver.fromAsyncResolver((attr) -> var1)) - .addResolvableAttributePattern( - CelAttributePattern.fromQualifiedIdentifier("com.google.var2"), - CelUnknownAttributeValueResolver.fromAsyncResolver((attr) -> var2)) - .addResolvableAttributePattern( - CelAttributePattern.fromQualifiedIdentifier("com.google.var3"), - CelUnknownAttributeValueResolver.fromAsyncResolver((attr) -> var3)) .setExecutorService(Executors.newSingleThreadExecutor()) .build(); @@ -139,11 +181,19 @@ public void asyncProgram_basicAsyncResovler() throws Exception { cel.compile("var1 == 'first' && var2 == 'second' && var3 == 'third'").getAst(); AsyncProgram program = asyncRuntime.createProgram(ast); - // empty starting context - UnknownContext context = asyncRuntime.newAsyncContext(); // Act - ListenableFuture future = program.evaluateToCompletion(context); + ListenableFuture future = + program.evaluateToCompletion( + CelResolvableAttributePattern.of( + CelAttributePattern.fromQualifiedIdentifier("com.google.var1"), + CelUnknownAttributeValueResolver.fromAsyncResolver((attr) -> var1)), + CelResolvableAttributePattern.of( + CelAttributePattern.fromQualifiedIdentifier("com.google.var2"), + CelUnknownAttributeValueResolver.fromAsyncResolver((attr) -> var2)), + CelResolvableAttributePattern.of( + CelAttributePattern.fromQualifiedIdentifier("com.google.var3"), + CelUnknownAttributeValueResolver.fromAsyncResolver((attr) -> var3))); assertThrows(TimeoutException.class, () -> future.get(1, SECONDS)); var1.set("first"); var2.set("second"); @@ -158,9 +208,9 @@ public void asyncProgram_basicAsyncResovler() throws Exception { @Test public void asyncProgram_honorsCancellation() throws Exception { // Arrange - final SettableFuture var1 = SettableFuture.create(); - final SettableFuture var2 = SettableFuture.create(); - final SettableFuture var3 = SettableFuture.create(); + SettableFuture var1 = SettableFuture.create(); + SettableFuture var2 = SettableFuture.create(); + SettableFuture var3 = SettableFuture.create(); Cel cel = CelFactory.standardCelBuilder() @@ -170,21 +220,12 @@ public void asyncProgram_honorsCancellation() throws Exception { .addVar("com.google.var2", SimpleType.STRING) .addVar("com.google.var3", SimpleType.STRING) .setResultType(SimpleType.BOOL) - .setContainer("com.google") + .setContainer(CelContainer.ofName("com.google")) .build(); CelAsyncRuntime asyncRuntime = CelAsyncRuntimeFactory.defaultAsyncRuntime() .setRuntime(cel) - .addResolvableAttributePattern( - CelAttributePattern.fromQualifiedIdentifier("com.google.var1"), - CelUnknownAttributeValueResolver.fromAsyncResolver((attr) -> var1)) - .addResolvableAttributePattern( - CelAttributePattern.fromQualifiedIdentifier("com.google.var2"), - CelUnknownAttributeValueResolver.fromAsyncResolver((attr) -> var2)) - .addResolvableAttributePattern( - CelAttributePattern.fromQualifiedIdentifier("com.google.var3"), - CelUnknownAttributeValueResolver.fromAsyncResolver((attr) -> var3)) .setExecutorService(Executors.newSingleThreadExecutor()) .build(); @@ -192,11 +233,19 @@ public void asyncProgram_honorsCancellation() throws Exception { cel.compile("var1 == 'first' && var2 == 'second' && var3 == 'third'").getAst(); AsyncProgram program = asyncRuntime.createProgram(ast); - // empty starting context - UnknownContext context = asyncRuntime.newAsyncContext(); // Act - ListenableFuture future = program.evaluateToCompletion(context); + ListenableFuture future = + program.evaluateToCompletion( + CelResolvableAttributePattern.of( + CelAttributePattern.fromQualifiedIdentifier("com.google.var1"), + CelUnknownAttributeValueResolver.fromAsyncResolver((attr) -> var1)), + CelResolvableAttributePattern.of( + CelAttributePattern.fromQualifiedIdentifier("com.google.var2"), + CelUnknownAttributeValueResolver.fromAsyncResolver((attr) -> var2)), + CelResolvableAttributePattern.of( + CelAttributePattern.fromQualifiedIdentifier("com.google.var3"), + CelUnknownAttributeValueResolver.fromAsyncResolver((attr) -> var3))); var1.set("first"); future.cancel(true); assertThrows(CancellationException.class, () -> future.get(1, SECONDS)); @@ -212,7 +261,7 @@ interface ResolverFactory { public void asyncProgram_concurrency( @TestParameter(valuesProvider = RepeatedTestProvider.class) int testRunIndex) throws Exception { - final Duration taskDelay = Duration.ofMillis(500); + Duration taskDelay = Duration.ofMillis(500); // Arrange Cel cel = CelFactory.standardCelBuilder() @@ -222,7 +271,7 @@ public void asyncProgram_concurrency( .addVar("com.google.var2", SimpleType.STRING) .addVar("com.google.var3", SimpleType.STRING) .setResultType(SimpleType.BOOL) - .setContainer("com.google") + .setContainer(CelContainer.ofName("com.google")) .build(); ResolverFactory resolverFactory = @@ -236,15 +285,6 @@ public void asyncProgram_concurrency( CelAsyncRuntime asyncRuntime = CelAsyncRuntimeFactory.defaultAsyncRuntime() .setRuntime(cel) - .addResolvableAttributePattern( - CelAttributePattern.fromQualifiedIdentifier("com.google.var1"), - resolverFactory.get("first")) - .addResolvableAttributePattern( - CelAttributePattern.fromQualifiedIdentifier("com.google.var2"), - resolverFactory.get("second")) - .addResolvableAttributePattern( - CelAttributePattern.fromQualifiedIdentifier("com.google.var3"), - resolverFactory.get("third")) .setExecutorService(Executors.newFixedThreadPool(3)) .build(); @@ -252,11 +292,19 @@ public void asyncProgram_concurrency( cel.compile("var1 == 'first' && var2 == 'second' && var3 == 'third'").getAst(); AsyncProgram program = asyncRuntime.createProgram(ast); - // empty starting context - UnknownContext context = asyncRuntime.newAsyncContext(); // Act - ListenableFuture future = program.evaluateToCompletion(context); + ListenableFuture future = + program.evaluateToCompletion( + CelResolvableAttributePattern.of( + CelAttributePattern.fromQualifiedIdentifier("com.google.var1"), + resolverFactory.get("first")), + CelResolvableAttributePattern.of( + CelAttributePattern.fromQualifiedIdentifier("com.google.var2"), + resolverFactory.get("second")), + CelResolvableAttributePattern.of( + CelAttributePattern.fromQualifiedIdentifier("com.google.var3"), + resolverFactory.get("third"))); // Total wait is 2 times the worker delay. This is a little conservative for the size of the // threadpool executor above, but should prevent flakes. @@ -280,7 +328,7 @@ public void asyncProgram_elementResolver() throws Exception { .setElemType(Type.newBuilder().setPrimitive(PrimitiveType.STRING))) .build()) .setResultType(SimpleType.BOOL) - .setContainer("com.google") + .setContainer(CelContainer.ofName("com.google")) .build(); CelUnknownAttributeValueResolver resolver = @@ -290,12 +338,6 @@ public void asyncProgram_elementResolver() throws Exception { CelAsyncRuntime asyncRuntime = CelAsyncRuntimeFactory.defaultAsyncRuntime() .setRuntime(cel) - .addResolvableAttributePattern( - CelAttributeParser.parsePattern("com.google.listVar[0]"), resolver) - .addResolvableAttributePattern( - CelAttributeParser.parsePattern("com.google.listVar[1]"), resolver) - .addResolvableAttributePattern( - CelAttributeParser.parsePattern("com.google.listVar[2]"), resolver) .setExecutorService(Executors.newSingleThreadExecutor()) .build(); @@ -303,11 +345,16 @@ public void asyncProgram_elementResolver() throws Exception { cel.compile("listVar[0] == 'el0' && listVar[1] == 'el1' && listVar[2] == 'el2'").getAst(); AsyncProgram program = asyncRuntime.createProgram(ast); - // empty starting context - UnknownContext context = asyncRuntime.newAsyncContext(); // Act - ListenableFuture future = program.evaluateToCompletion(context); + ListenableFuture future = + program.evaluateToCompletion( + CelResolvableAttributePattern.of( + CelAttributeParser.parsePattern("com.google.listVar[0]"), resolver), + CelResolvableAttributePattern.of( + CelAttributeParser.parsePattern("com.google.listVar[1]"), resolver), + CelResolvableAttributePattern.of( + CelAttributeParser.parsePattern("com.google.listVar[2]"), resolver)); Object result = future.get(1, SECONDS); // Assert @@ -326,7 +373,7 @@ public void asyncProgram_thrownExceptionPropagatesImmediately() throws Exception .addVar("com.google.var2", SimpleType.STRING) .addVar("com.google.var3", SimpleType.STRING) .setResultType(SimpleType.BOOL) - .setContainer("com.google") + .setContainer(CelContainer.ofName("com.google")) .build(); CelUnknownAttributeValueResolver resolveName = @@ -339,16 +386,6 @@ public void asyncProgram_thrownExceptionPropagatesImmediately() throws Exception CelAsyncRuntime asyncRuntime = CelAsyncRuntimeFactory.defaultAsyncRuntime() .setRuntime(cel) - .addResolvableAttributePattern( - CelAttributePattern.fromQualifiedIdentifier("com.google.var1"), resolveName) - .addResolvableAttributePattern( - CelAttributePattern.fromQualifiedIdentifier("com.google.var2"), - CelUnknownAttributeValueResolver.fromResolver( - (attr) -> { - throw new IllegalArgumentException("example_var2"); - })) - .addResolvableAttributePattern( - CelAttributePattern.fromQualifiedIdentifier("com.google.var3"), resolveName) .setExecutorService(newDirectExecutorService()) .build(); @@ -360,10 +397,20 @@ public void asyncProgram_thrownExceptionPropagatesImmediately() throws Exception .getAst(); AsyncProgram program = asyncRuntime.createProgram(ast); - UnknownContext context = asyncRuntime.newAsyncContext(); // Act - ListenableFuture future = program.evaluateToCompletion(context); + ListenableFuture future = + program.evaluateToCompletion( + CelResolvableAttributePattern.of( + CelAttributePattern.fromQualifiedIdentifier("com.google.var1"), resolveName), + CelResolvableAttributePattern.of( + CelAttributePattern.fromQualifiedIdentifier("com.google.var2"), + CelUnknownAttributeValueResolver.fromResolver( + (attr) -> { + throw new IllegalArgumentException("example_var2"); + })), + CelResolvableAttributePattern.of( + CelAttributePattern.fromQualifiedIdentifier("com.google.var3"), resolveName)); // Assert ExecutionException e = assertThrows(ExecutionException.class, () -> future.get(2, SECONDS)); @@ -382,7 +429,7 @@ public void asyncProgram_returnedExceptionPropagatesToEvaluator() throws Excepti .addVar("com.google.var2", SimpleType.STRING) .addVar("com.google.var3", SimpleType.STRING) .setResultType(SimpleType.BOOL) - .setContainer("com.google") + .setContainer(CelContainer.ofName("com.google")) .build(); CelUnknownAttributeValueResolver resolveName = @@ -395,14 +442,6 @@ public void asyncProgram_returnedExceptionPropagatesToEvaluator() throws Excepti CelAsyncRuntime asyncRuntime = CelAsyncRuntimeFactory.defaultAsyncRuntime() .setRuntime(cel) - .addResolvableAttributePattern( - CelAttributePattern.fromQualifiedIdentifier("com.google.var1"), resolveName) - .addResolvableAttributePattern( - CelAttributePattern.fromQualifiedIdentifier("com.google.var2"), - CelUnknownAttributeValueResolver.fromResolver( - (attr) -> new IllegalStateException("example_var2"))) - .addResolvableAttributePattern( - CelAttributePattern.fromQualifiedIdentifier("com.google.var3"), resolveName) .setExecutorService(newDirectExecutorService()) .build(); @@ -414,10 +453,18 @@ public void asyncProgram_returnedExceptionPropagatesToEvaluator() throws Excepti .getAst(); AsyncProgram program = asyncRuntime.createProgram(ast); - UnknownContext context = asyncRuntime.newAsyncContext(); // Act - ListenableFuture future = program.evaluateToCompletion(context); + ListenableFuture future = + program.evaluateToCompletion( + CelResolvableAttributePattern.of( + CelAttributePattern.fromQualifiedIdentifier("com.google.var1"), resolveName), + CelResolvableAttributePattern.of( + CelAttributePattern.fromQualifiedIdentifier("com.google.var2"), + CelUnknownAttributeValueResolver.fromResolver( + (attr) -> new IllegalStateException("example_var2"))), + CelResolvableAttributePattern.of( + CelAttributePattern.fromQualifiedIdentifier("com.google.var3"), resolveName)); // Assert ExecutionException e = assertThrows(ExecutionException.class, () -> future.get(2, SECONDS)); @@ -437,7 +484,7 @@ public void asyncProgram_returnedExceptionPropagatesToEvaluatorIsPruneable() thr .addVar("com.google.var2", SimpleType.STRING) .addVar("com.google.var3", SimpleType.STRING) .setResultType(SimpleType.BOOL) - .setContainer("com.google") + .setContainer(CelContainer.ofName("com.google")) .build(); CelUnknownAttributeValueResolver resolveName = @@ -450,14 +497,6 @@ public void asyncProgram_returnedExceptionPropagatesToEvaluatorIsPruneable() thr CelAsyncRuntime asyncRuntime = CelAsyncRuntimeFactory.defaultAsyncRuntime() .setRuntime(cel) - .addResolvableAttributePattern( - CelAttributePattern.fromQualifiedIdentifier("com.google.var1"), resolveName) - .addResolvableAttributePattern( - CelAttributePattern.fromQualifiedIdentifier("com.google.var2"), - CelUnknownAttributeValueResolver.fromResolver( - (attr) -> new IllegalStateException("example"))) - .addResolvableAttributePattern( - CelAttributePattern.fromQualifiedIdentifier("com.google.var3"), resolveName) .setExecutorService(newDirectExecutorService()) .build(); @@ -466,10 +505,18 @@ public void asyncProgram_returnedExceptionPropagatesToEvaluatorIsPruneable() thr .getAst(); AsyncProgram program = asyncRuntime.createProgram(ast); - UnknownContext context = asyncRuntime.newAsyncContext(); // Act - ListenableFuture future = program.evaluateToCompletion(context); + ListenableFuture future = + program.evaluateToCompletion( + CelResolvableAttributePattern.of( + CelAttributePattern.fromQualifiedIdentifier("com.google.var1"), resolveName), + CelResolvableAttributePattern.of( + CelAttributePattern.fromQualifiedIdentifier("com.google.var2"), + CelUnknownAttributeValueResolver.fromResolver( + (attr) -> new IllegalStateException("example"))), + CelResolvableAttributePattern.of( + CelAttributePattern.fromQualifiedIdentifier("com.google.var3"), resolveName)); Object result = future.get(2, SECONDS); // Assert diff --git a/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel b/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel new file mode 100644 index 000000000..9116818dc --- /dev/null +++ b/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel @@ -0,0 +1,70 @@ +load("@rules_java//java:defs.bzl", "java_library") +load("//:testing.bzl", "junit4_test_suites") + +package( + default_applicable_licenses = ["//:license"], + default_testonly = True, +) + +java_library( + name = "tests", + testonly = 1, + srcs = glob( + ["*.java"], + ), + deps = [ + "//:java_truth", + "//common:cel_ast", + "//common:cel_descriptor_util", + "//common:cel_source", + "//common:compiler_common", + "//common:container", + "//common:error_codes", + "//common:options", + "//common/ast", + "//common/exceptions:divide_by_zero", + "//common/internal:cel_descriptor_pools", + "//common/internal:default_message_factory", + "//common/internal:dynamic_proto", + "//common/types", + "//common/types:default_type_provider", + "//common/types:message_type_provider", + "//common/types:type_providers", + "//common/values", + "//common/values:cel_byte_string", + "//common/values:cel_value_provider", + "//common/values:proto_message_value", + "//common/values:proto_message_value_provider", + "//compiler", + "//compiler:compiler_builder", + "//extensions", + "//parser:macro", + "//runtime", + "//runtime:descriptor_type_resolver", + "//runtime:dispatcher", + "//runtime:function_binding", + "//runtime:partial_vars", + "//runtime:program", + "//runtime:runtime_equality", + "//runtime:runtime_helpers", + "//runtime:standard_functions", + "//runtime:unknown_attributes", + "//runtime/planner:program_planner", + "//runtime/standard:type", + "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", + "@maven//:com_google_guava_guava", + "@maven//:com_google_testparameterinjector_test_parameter_injector", + "@maven//:junit_junit", + ], +) + +junit4_test_suites( + name = "test_suites", + sizes = [ + "small", + ], + src_dir = "src/test/java", + deps = [ + ":tests", + ], +) diff --git a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java new file mode 100644 index 000000000..c749028ff --- /dev/null +++ b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java @@ -0,0 +1,1191 @@ +// 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 com.google.common.truth.Truth.assertThat; +import static dev.cel.common.CelFunctionDecl.newFunctionDeclaration; +import static dev.cel.common.CelOverloadDecl.newGlobalOverload; +import static dev.cel.common.CelOverloadDecl.newMemberOverload; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableCollection; +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 com.google.testing.junit.testparameterinjector.TestParameters; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelContainer; +import dev.cel.common.CelDescriptorUtil; +import dev.cel.common.CelErrorCode; +import dev.cel.common.CelOptions; +import dev.cel.common.CelSource; +import dev.cel.common.ast.CelExpr; +import dev.cel.common.exceptions.CelDivideByZeroException; +import dev.cel.common.internal.CelDescriptorPool; +import dev.cel.common.internal.DefaultDescriptorPool; +import dev.cel.common.internal.DefaultMessageFactory; +import dev.cel.common.internal.DynamicProto; +import dev.cel.common.types.CelType; +import dev.cel.common.types.CelTypeProvider; +import dev.cel.common.types.CelTypeProvider.CombinedCelTypeProvider; +import dev.cel.common.types.DefaultTypeProvider; +import dev.cel.common.types.ListType; +import dev.cel.common.types.MapType; +import dev.cel.common.types.OptionalType; +import dev.cel.common.types.ProtoMessageTypeProvider; +import dev.cel.common.types.SimpleType; +import dev.cel.common.types.StructTypeReference; +import dev.cel.common.types.TypeType; +import dev.cel.common.values.CelByteString; +import dev.cel.common.values.CelValueConverter; +import dev.cel.common.values.CelValueProvider; +import dev.cel.common.values.NullValue; +import dev.cel.common.values.ProtoCelValueConverter; +import dev.cel.common.values.ProtoMessageValueProvider; +import dev.cel.compiler.CelCompiler; +import dev.cel.compiler.CelCompilerFactory; +import dev.cel.expr.conformance.proto3.GlobalEnum; +import dev.cel.expr.conformance.proto3.TestAllTypes; +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; +import dev.cel.runtime.standard.TypeFunction; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public final class ProgramPlannerTest { + // Note that the following deps will be built from top-level builder APIs + private static final CelOptions CEL_OPTIONS = CelOptions.current().build(); + private static final CelTypeProvider TYPE_PROVIDER = + new CombinedCelTypeProvider( + DefaultTypeProvider.getInstance(), + new ProtoMessageTypeProvider(ImmutableSet.of(TestAllTypes.getDescriptor()))); + private static final RuntimeEquality RUNTIME_EQUALITY = + RuntimeEquality.create(RuntimeHelpers.create(), CEL_OPTIONS); + private static final CelDescriptorPool DESCRIPTOR_POOL = + DefaultDescriptorPool.create( + CelDescriptorUtil.getAllDescriptorsFromFileDescriptor( + TestAllTypes.getDescriptor().getFile())); + private static final DynamicProto DYNAMIC_PROTO = + DynamicProto.create(DefaultMessageFactory.create(DESCRIPTOR_POOL)); + private static final CelValueProvider VALUE_PROVIDER = + ProtoMessageValueProvider.newInstance(CEL_OPTIONS, DYNAMIC_PROTO); + private static final CelValueConverter CEL_VALUE_CONVERTER = + ProtoCelValueConverter.newInstance(DESCRIPTOR_POOL, DYNAMIC_PROTO, CelOptions.DEFAULT); + private static final CelContainer CEL_CONTAINER = + CelContainer.newBuilder() + .setName("cel.expr.conformance.proto3") + .addAbbreviations("really.long.abbr") + .build(); + + private static final ProgramPlanner PLANNER = + ProgramPlanner.newPlanner( + TYPE_PROVIDER, + VALUE_PROVIDER, + newDispatcher(), + CEL_VALUE_CONVERTER, + CEL_CONTAINER, + CEL_OPTIONS, + ImmutableSet.of("late_bound_func")); + + private static final CelCompiler CEL_COMPILER = + CelCompilerFactory.standardCelCompilerBuilder() + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addFunctionDeclarations( + newFunctionDeclaration( + "late_bound_func", + newGlobalOverload( + "late_bound_func_overload", SimpleType.STRING, SimpleType.STRING))) + .addVar("msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())) + .addVar("map_var", MapType.create(SimpleType.STRING, SimpleType.DYN)) + .addVar("int_var", SimpleType.INT) + .addVar("dyn_var", SimpleType.DYN) + .addVar("really.long.abbr.ident", SimpleType.DYN) + .addFunctionDeclarations( + newFunctionDeclaration("zero", newGlobalOverload("zero_overload", SimpleType.INT)), + newFunctionDeclaration("error", newGlobalOverload("error_overload", SimpleType.INT)), + newFunctionDeclaration( + "neg", + newGlobalOverload("neg_int", SimpleType.INT, SimpleType.INT), + newGlobalOverload("neg_double", SimpleType.DOUBLE, SimpleType.DOUBLE)), + newFunctionDeclaration( + "cel.expr.conformance.proto3.power", + newGlobalOverload( + "power_int_int", SimpleType.INT, SimpleType.INT, SimpleType.INT)), + newFunctionDeclaration( + "concat", + newGlobalOverload( + "concat_bytes_bytes", SimpleType.BYTES, SimpleType.BYTES, SimpleType.BYTES), + newMemberOverload( + "bytes_concat_bytes", SimpleType.BYTES, SimpleType.BYTES, SimpleType.BYTES))) + .addMessageTypes(TestAllTypes.getDescriptor()) + .addLibraries(CelExtensions.optional(), CelExtensions.comprehensions()) + .setContainer(CEL_CONTAINER) + .build(); + + /** + * Configure dispatcher for testing purposes. This is done manually here, but this should be + * driven by the top-level runtime APIs in the future + */ + private static DefaultDispatcher newDispatcher() { + DefaultDispatcher.Builder builder = DefaultDispatcher.newBuilder(); + + // Subsetted StdLib + CelStandardFunctions stdFunctions = + CelStandardFunctions.newBuilder() + .includeFunctions( + StandardFunction.INDEX, + StandardFunction.LOGICAL_NOT, + StandardFunction.ADD, + StandardFunction.GREATER, + StandardFunction.GREATER_EQUALS, + StandardFunction.LESS, + StandardFunction.DIVIDE, + StandardFunction.EQUALS, + StandardFunction.NOT_STRICTLY_FALSE, + StandardFunction.DYN) + .build(); + addBindingsToDispatcher( + builder, stdFunctions.newFunctionBindings(RUNTIME_EQUALITY, CEL_OPTIONS)); + + TypeFunction typeFunction = + TypeFunction.create( + DescriptorTypeResolver.create(TYPE_PROVIDER, CelValueConverter.getDefaultInstance())); + addBindingsToDispatcher( + builder, typeFunction.newFunctionBindings(CEL_OPTIONS, RUNTIME_EQUALITY)); + + // Custom functions + addBindingsToDispatcher( + builder, + CelFunctionBinding.fromOverloads( + "zero", CelFunctionBinding.from("zero_overload", ImmutableList.of(), (unused) -> 0L))); + + addBindingsToDispatcher( + builder, + CelFunctionBinding.fromOverloads( + "error", + CelFunctionBinding.from( + "error_overload", + ImmutableList.of(), + (unused) -> { + throw new IllegalArgumentException("Intentional error"); + }))); + + addBindingsToDispatcher( + builder, + CelFunctionBinding.fromOverloads( + "neg", + CelFunctionBinding.from("neg_int", Long.class, arg -> -arg), + CelFunctionBinding.from("neg_double", Double.class, arg -> -arg))); + + addBindingsToDispatcher( + builder, + CelFunctionBinding.fromOverloads( + "cel.expr.conformance.proto3.power", + CelFunctionBinding.from( + "power_int_int", + Long.class, + Long.class, + (value, power) -> (long) Math.pow(value, power)))); + + addBindingsToDispatcher( + builder, + CelFunctionBinding.fromOverloads( + "concat", + CelFunctionBinding.from( + "concat_bytes_bytes", + CelByteString.class, + CelByteString.class, + ProgramPlannerTest::concatenateByteArrays), + CelFunctionBinding.from( + "bytes_concat_bytes", + CelByteString.class, + CelByteString.class, + ProgramPlannerTest::concatenateByteArrays))); + + return builder.build(); + } + + private static void addBindingsToDispatcher( + DefaultDispatcher.Builder builder, ImmutableCollection overloadBindings) { + if (overloadBindings.isEmpty()) { + throw new IllegalArgumentException("Invalid bindings"); + } + + overloadBindings.forEach( + overload -> + builder.addOverload( + ((InternalCelFunctionBinding) overload).getFunctionName(), + overload.getOverloadId(), + overload.getArgTypes(), + overload.isStrict(), + overload.getDefinition())); + } + + @TestParameter boolean isParseOnly; + + @Test + public void plan_notSet_throws() { + CelAbstractSyntaxTree invalidAst = + CelAbstractSyntaxTree.newParsedAst(CelExpr.ofNotSet(0L), CelSource.newBuilder().build()); + + CelEvaluationException e = + assertThrows(CelEvaluationException.class, () -> PLANNER.plan(invalidAst)); + + assertThat(e).hasMessageThat().contains("evaluation error: Unsupported kind: NOT_SET"); + } + + @Test + public void plan_constant(@TestParameter ConstantTestCase testCase) throws Exception { + CelAbstractSyntaxTree ast = compile(testCase.expression); + Program program = PLANNER.plan(ast); + + Object result = program.eval(); + + assertThat(result).isEqualTo(testCase.expected); + } + + @Test + public void plan_ident_enum() throws Exception { + CelAbstractSyntaxTree ast = + compile(GlobalEnum.getDescriptor().getFullName() + "." + GlobalEnum.GAR); + Program program = PLANNER.plan(ast); + + Object result = program.eval(); + + assertThat(result).isEqualTo(1L); + } + + @Test + public void plan_ident_enumContainer() throws Exception { + CelContainer container = CelContainer.ofName(GlobalEnum.getDescriptor().getFullName()); + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addMessageTypes(TestAllTypes.getDescriptor()) + .setContainer(container) + .build(); + CelAbstractSyntaxTree ast = compile(compiler, GlobalEnum.GAR.name()); + ProgramPlanner planner = + ProgramPlanner.newPlanner( + TYPE_PROVIDER, + VALUE_PROVIDER, + newDispatcher(), + CEL_VALUE_CONVERTER, + container, + CEL_OPTIONS, + ImmutableSet.of()); + + Program program = planner.plan(ast); + + Object result = program.eval(); + + assertThat(result).isEqualTo(1L); + } + + @Test + public void plan_ident_variable() throws Exception { + CelAbstractSyntaxTree ast = compile("int_var"); + Program program = PLANNER.plan(ast); + + Object result = program.eval(ImmutableMap.of("int_var", 1L)); + + 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); + Program program = PLANNER.plan(ast); + + TypeType result = (TypeType) program.eval(); + + assertThat(result).isEqualTo(testCase.type); + } + + @Test + public void planIdent_typeLiteral_equality(@TestParameter TypeLiteralTestCase testCase) + throws Exception { + // ex: type(bool) == type, type(TestAllTypes) == type + CelAbstractSyntaxTree ast = compile(String.format("type(%s) == type", testCase.expression)); + Program program = PLANNER.plan(ast); + + boolean result = (boolean) program.eval(); + + assertThat(result).isTrue(); + } + + @Test + public void plan_ident_missingAttribute_throws() throws Exception { + CelAbstractSyntaxTree ast = compile("int_var"); + Program program = PLANNER.plan(ast); + + CelEvaluationException e = assertThrows(CelEvaluationException.class, program::eval); + assertThat(e).hasMessageThat().contains("evaluation error at :0: No such attribute(s)"); + } + + @Test + public void plan_ident_withContainer() throws Exception { + CelAbstractSyntaxTree ast = compile("abbr.ident"); + Program program = PLANNER.plan(ast); + + Object result = program.eval(ImmutableMap.of("really.long.abbr.ident", 1L)); + + assertThat(result).isEqualTo(1); + } + + @Test + @SuppressWarnings("unchecked") // test only + public void plan_createList() throws Exception { + CelAbstractSyntaxTree ast = compile("[1, 'foo', true, [2, false]]"); + Program program = PLANNER.plan(ast); + + ImmutableList result = (ImmutableList) program.eval(); + + assertThat(result).containsExactly(1L, "foo", true, ImmutableList.of(2L, false)).inOrder(); + } + + @Test + @SuppressWarnings("unchecked") // test only + public void plan_createMap() throws Exception { + CelAbstractSyntaxTree ast = compile("{'foo': 1, true: 'bar'}"); + Program program = PLANNER.plan(ast); + + ImmutableMap result = (ImmutableMap) program.eval(); + + assertThat(result).containsExactly("foo", 1L, true, "bar").inOrder(); + } + + @Test + public void plan_createMap_containsDuplicateKey_throws() throws Exception { + CelAbstractSyntaxTree ast = compile("{true: 1, false: 2, true: 3}"); + Program program = PLANNER.plan(ast); + + CelEvaluationException e = assertThrows(CelEvaluationException.class, program::eval); + assertThat(e) + .hasMessageThat() + .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{}"); + Program program = PLANNER.plan(ast); + + TestAllTypes result = (TestAllTypes) program.eval(); + + assertThat(result).isEqualTo(TestAllTypes.getDefaultInstance()); + } + + @Test + public void plan_createStruct_wrapper() throws Exception { + CelAbstractSyntaxTree ast = compile("google.protobuf.StringValue { value: 'foo' }"); + Program program = PLANNER.plan(ast); + + String result = (String) program.eval(); + + assertThat(result).isEqualTo("foo"); + } + + @Test + public void planCreateStruct_withFields() throws Exception { + CelAbstractSyntaxTree ast = + compile( + "cel.expr.conformance.proto3.TestAllTypes{" + + "single_string: 'foo'," + + "single_bool: true" + + "}"); + Program program = PLANNER.plan(ast); + + TestAllTypes result = (TestAllTypes) program.eval(); + + assertThat(result) + .isEqualTo(TestAllTypes.newBuilder().setSingleString("foo").setSingleBool(true).build()); + } + + @Test + public void plan_createStruct_withContainer() throws Exception { + CelAbstractSyntaxTree ast = compile("TestAllTypes{}"); + Program program = PLANNER.plan(ast); + + TestAllTypes result = (TestAllTypes) program.eval(); + + assertThat(result).isEqualTo(TestAllTypes.getDefaultInstance()); + } + + @Test + public void plan_call_zeroArgs() throws Exception { + CelAbstractSyntaxTree ast = compile("zero()"); + Program program = PLANNER.plan(ast); + + Long result = (Long) program.eval(); + + assertThat(result).isEqualTo(0L); + } + + @Test + public void plan_call_throws() throws Exception { + CelAbstractSyntaxTree ast = compile("error()"); + Program program = PLANNER.plan(ast); + + CelEvaluationException e = assertThrows(CelEvaluationException.class, program::eval); + assertThat(e) + .hasMessageThat() + .contains("evaluation error at :5: Function 'error' failed with arg(s) ''"); + assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class); + assertThat(e.getCause()).hasMessageThat().contains("Intentional error"); + } + + @Test + public void plan_call_oneArg_int() throws Exception { + CelAbstractSyntaxTree ast = compile("neg(1)"); + Program program = PLANNER.plan(ast); + + Long result = (Long) program.eval(); + + assertThat(result).isEqualTo(-1L); + } + + @Test + public void plan_call_oneArg_double() throws Exception { + CelAbstractSyntaxTree ast = compile("neg(2.5)"); + Program program = PLANNER.plan(ast); + + Double result = (Double) program.eval(); + + assertThat(result).isEqualTo(-2.5d); + } + + @Test + public void plan_call_twoArgs_global() throws Exception { + CelAbstractSyntaxTree ast = compile("concat(b'abc', b'def')"); + Program program = PLANNER.plan(ast); + + CelByteString result = (CelByteString) program.eval(); + + assertThat(result).isEqualTo(CelByteString.of("abcdef".getBytes(UTF_8))); + } + + @Test + public void plan_call_twoArgs_receiver() throws Exception { + CelAbstractSyntaxTree ast = compile("b'abc'.concat(b'def')"); + Program program = PLANNER.plan(ast); + + CelByteString result = (CelByteString) program.eval(); + + assertThat(result).isEqualTo(CelByteString.of("abcdef".getBytes(UTF_8))); + } + + @Test + public void plan_call_mapIndex() throws Exception { + CelAbstractSyntaxTree ast = compile("map_var['key'][1]"); + Program program = PLANNER.plan(ast); + ImmutableMap mapVarPayload = ImmutableMap.of("key", ImmutableList.of(1L, 2L)); + + Long result = (Long) program.eval(ImmutableMap.of("map_var", mapVarPayload)); + + assertThat(result).isEqualTo(2L); + } + + @Test + public void plan_call_noMatchingOverload_throws() throws Exception { + CelAbstractSyntaxTree ast = compile("concat(b'abc', dyn_var)"); + Program program = PLANNER.plan(ast); + String errorMsg = + "No matching overload for function 'concat'. Overload candidates: concat_bytes_bytes"; + if (isParseOnly) { + // Parsed-only evaluation includes both overloads as candidates due to dynamic dispatch + errorMsg += ", bytes_concat_bytes"; + } + + CelEvaluationException e = + assertThrows( + CelEvaluationException.class, + () -> program.eval(ImmutableMap.of("dyn_var", "Impossible Overload"))); + + assertThat(e).hasMessageThat().contains(errorMsg); + } + + @Test + @TestParameters("{expression: 'true || true', expectedResult: true}") + @TestParameters("{expression: 'true || false', expectedResult: true}") + @TestParameters("{expression: 'false || true', expectedResult: true}") + @TestParameters("{expression: 'false || false', expectedResult: false}") + @TestParameters("{expression: 'true || (1 / 0 > 2)', expectedResult: true}") + @TestParameters("{expression: '(1 / 0 > 2) || true', expectedResult: true}") + public void plan_call_logicalOr_shortCircuit(String expression, boolean expectedResult) + throws Exception { + CelAbstractSyntaxTree ast = compile(expression); + Program program = PLANNER.plan(ast); + + boolean result = (boolean) program.eval(); + + assertThat(result).isEqualTo(expectedResult); + } + + @Test + @TestParameters("{expression: '(1 / 0 > 2) || (1 / 0 > 2)'}") + @TestParameters("{expression: 'false || (1 / 0 > 2)'}") + @TestParameters("{expression: '(1 / 0 > 2) || false'}") + public void plan_call_logicalOr_throws(String expression) throws Exception { + CelAbstractSyntaxTree ast = compile(expression); + Program program = PLANNER.plan(ast); + + CelEvaluationException e = assertThrows(CelEvaluationException.class, program::eval); + assertThat(e).hasMessageThat().startsWith("evaluation error at :"); + assertThat(e).hasMessageThat().endsWith("/ by zero"); + assertThat(e).hasCauseThat().isInstanceOf(CelDivideByZeroException.class); + assertThat(e.getErrorCode()).isEqualTo(CelErrorCode.DIVIDE_BY_ZERO); + } + + @Test + @TestParameters("{expression: 'true && true', expectedResult: true}") + @TestParameters("{expression: 'true && false', expectedResult: false}") + @TestParameters("{expression: 'false && true', expectedResult: false}") + @TestParameters("{expression: 'false && false', expectedResult: false}") + @TestParameters("{expression: 'false && (1 / 0 > 2)', expectedResult: false}") + @TestParameters("{expression: '(1 / 0 > 2) && false', expectedResult: false}") + public void plan_call_logicalAnd_shortCircuit(String expression, boolean expectedResult) + throws Exception { + CelAbstractSyntaxTree ast = compile(expression); + Program program = PLANNER.plan(ast); + + boolean result = (boolean) program.eval(); + + assertThat(result).isEqualTo(expectedResult); + } + + @Test + @TestParameters("{expression: '(1 / 0 > 2) && (1 / 0 > 2)'}") + @TestParameters("{expression: 'true && (1 / 0 > 2)'}") + @TestParameters("{expression: '(1 / 0 > 2) && true'}") + public void plan_call_logicalAnd_throws(String expression) throws Exception { + CelAbstractSyntaxTree ast = compile(expression); + Program program = PLANNER.plan(ast); + + CelEvaluationException e = assertThrows(CelEvaluationException.class, program::eval); + assertThat(e).hasMessageThat().startsWith("evaluation error at :"); + assertThat(e).hasMessageThat().endsWith("/ by zero"); + assertThat(e).hasCauseThat().isInstanceOf(CelDivideByZeroException.class); + assertThat(e.getErrorCode()).isEqualTo(CelErrorCode.DIVIDE_BY_ZERO); + } + + @Test + @TestParameters("{expression: 'false ? (1 / 0) > 2 : false', expectedResult: false}") + @TestParameters("{expression: 'false ? (1 / 0) > 2 : true', expectedResult: true}") + @TestParameters("{expression: 'true ? false : (1 / 0) > 2', expectedResult: false}") + @TestParameters("{expression: 'true ? true : (1 / 0) > 2', expectedResult: true}") + public void plan_call_conditional_shortCircuit(String expression, boolean expectedResult) + throws Exception { + CelAbstractSyntaxTree ast = compile(expression); + Program program = PLANNER.plan(ast); + + boolean result = (boolean) program.eval(); + + assertThat(result).isEqualTo(expectedResult); + } + + @Test + @TestParameters("{expression: '(1 / 0) > 2 ? true : true'}") + @TestParameters("{expression: 'true ? (1 / 0) > 2 : true'}") + @TestParameters("{expression: 'false ? true : (1 / 0) > 2'}") + public void plan_call_conditional_throws(String expression) throws Exception { + CelAbstractSyntaxTree ast = compile(expression); + Program program = PLANNER.plan(ast); + + CelEvaluationException e = assertThrows(CelEvaluationException.class, program::eval); + assertThat(e).hasMessageThat().startsWith("evaluation error at :"); + assertThat(e).hasMessageThat().endsWith("/ by zero"); + assertThat(e).hasCauseThat().isInstanceOf(CelDivideByZeroException.class); + assertThat(e.getErrorCode()).isEqualTo(CelErrorCode.DIVIDE_BY_ZERO); + } + + @Test + @TestParameters("{expression: 'power(2,3)'}") + @TestParameters("{expression: 'proto3.power(2,3)'}") + @TestParameters("{expression: 'conformance.proto3.power(2,3)'}") + @TestParameters("{expression: 'expr.conformance.proto3.power(2,3)'}") + @TestParameters("{expression: 'cel.expr.conformance.proto3.power(2,3)'}") + public void plan_call_withContainer(String expression) throws Exception { + CelAbstractSyntaxTree ast = compile(expression); // invokes cel.expr.conformance.proto3.power + Program program = PLANNER.plan(ast); + + Long result = (Long) program.eval(); + + assertThat(result).isEqualTo(8); + } + + @Test + public void plan_call_lateBoundFunction() throws Exception { + CelAbstractSyntaxTree ast = compile("late_bound_func('test')"); + + Program program = PLANNER.plan(ast); + + String result = + (String) + program.eval( + ImmutableMap.of(), + CelLateFunctionBindings.from( + CelFunctionBinding.from( + "late_bound_func_overload", String.class, (arg) -> arg + "_resolved"))); + + assertThat(result).isEqualTo("test_resolved"); + } + + @Test + public void plan_call_typeResolution(@TestParameter TypeObjectTestCase testCase) + throws Exception { + CelAbstractSyntaxTree ast = compile(testCase.expression); + Program program = PLANNER.plan(ast); + + TypeType result = (TypeType) program.eval(); + + assertThat(result).isEqualTo(testCase.type); + } + + @Test + public void plan_select_protoMessageField() throws Exception { + CelAbstractSyntaxTree ast = compile("msg.single_string"); + Program program = PLANNER.plan(ast); + + String result = + (String) + program.eval( + ImmutableMap.of("msg", TestAllTypes.newBuilder().setSingleString("foo").build())); + + assertThat(result).isEqualTo("foo"); + } + + @Test + public void plan_select_nestedProtoMessage() throws Exception { + CelAbstractSyntaxTree ast = compile("msg.single_nested_message"); + NestedMessage nestedMessage = NestedMessage.newBuilder().setBb(42).build(); + Program program = PLANNER.plan(ast); + + Object result = + program.eval( + ImmutableMap.of( + "msg", TestAllTypes.newBuilder().setSingleNestedMessage(nestedMessage).build())); + + assertThat(result).isEqualTo(nestedMessage); + } + + @Test + public void plan_select_nestedProtoMessageField() throws Exception { + CelAbstractSyntaxTree ast = compile("msg.single_nested_message.bb"); + Program program = PLANNER.plan(ast); + + Object result = + program.eval( + ImmutableMap.of( + "msg", + TestAllTypes.newBuilder() + .setSingleNestedMessage(NestedMessage.newBuilder().setBb(42)) + .build())); + + assertThat(result).isEqualTo(42); + } + + @Test + public void plan_select_safeTraversal() throws Exception { + CelAbstractSyntaxTree ast = compile("msg.single_nested_message.bb"); + Program program = PLANNER.plan(ast); + + Object result = program.eval(ImmutableMap.of("msg", TestAllTypes.getDefaultInstance())); + + assertThat(result).isEqualTo(0L); + } + + @Test + public void plan_select_onCreateStruct() throws Exception { + CelAbstractSyntaxTree ast = + compile("cel.expr.conformance.proto3.TestAllTypes{ single_string: 'foo'}.single_string"); + Program program = PLANNER.plan(ast); + + Object result = program.eval(); + + assertThat(result).isEqualTo("foo"); + } + + @Test + public void plan_select_onCreateMap() throws Exception { + CelAbstractSyntaxTree ast = compile("{'foo':'bar'}.foo"); + Program program = PLANNER.plan(ast); + + Object result = program.eval(); + + assertThat(result).isEqualTo("bar"); + } + + @Test + public void plan_select_onMapVariable() throws Exception { + CelAbstractSyntaxTree ast = compile("map_var.foo"); + Program program = PLANNER.plan(ast); + + Object result = program.eval(ImmutableMap.of("map_var", ImmutableMap.of("foo", 42L))); + + assertThat(result).isEqualTo(42L); + } + + @Test + public void plan_select_mapVarInputMissing_throws() throws Exception { + CelAbstractSyntaxTree ast = compile("map_var.foo"); + Program program = PLANNER.plan(ast); + String errorMessage = "evaluation error at :7: No such attribute(s): "; + if (isParseOnly) { + errorMessage += + "cel.expr.conformance.proto3.map_var, cel.expr.conformance.map_var, cel.expr.map_var," + + " cel.map_var, "; + } + errorMessage += "map_var"; + + CelEvaluationException e = + assertThrows(CelEvaluationException.class, () -> program.eval(ImmutableMap.of())); + + assertThat(e).hasMessageThat().contains(errorMessage); + } + + @Test + public void plan_select_mapVarKeyMissing_throws() throws Exception { + CelAbstractSyntaxTree ast = compile("map_var.foo"); + Program program = PLANNER.plan(ast); + + CelEvaluationException e = + assertThrows( + CelEvaluationException.class, + () -> program.eval(ImmutableMap.of("map_var", ImmutableMap.of()))); + assertThat(e) + .hasMessageThat() + .contains("evaluation error at :7: key 'foo' is not present in map"); + } + + @Test + public void plan_select_stringQualificationFail_throws() throws Exception { + CelAbstractSyntaxTree ast = compile("map_var.foo"); + Program program = PLANNER.plan(ast); + + CelEvaluationException e = + assertThrows( + CelEvaluationException.class, + () -> program.eval(ImmutableMap.of("map_var", "bogus string"))); + + assertThat(e) + .hasMessageThat() + .isEqualTo( + "evaluation error at :7: Error resolving field 'foo'. Field selections must be" + + " performed on messages or maps."); + } + + @Test + public void plan_select_presenceTest(@TestParameter PresenceTestCase testCase) throws Exception { + CelAbstractSyntaxTree ast = compile(testCase.expression); + Program program = PLANNER.plan(ast); + + boolean result = + (boolean) + program.eval( + ImmutableMap.of("msg", testCase.inputParam, "map_var", testCase.inputParam)); + + assertThat(result).isEqualTo(testCase.expected); + } + + @Test + public void plan_select_badPresenceTest_throws() throws Exception { + CelAbstractSyntaxTree ast = compile("has(dyn([]).invalid)"); + Program program = PLANNER.plan(ast); + + CelEvaluationException e = assertThrows(CelEvaluationException.class, program::eval); + assertThat(e) + .hasMessageThat() + .contains( + "Error resolving field 'invalid'. Field selections must be performed on messages or" + + " maps."); + } + + @Test + @TestParameters("{expression: '[1,2,3].exists(x, x > 0) == true'}") + @TestParameters("{expression: '[1,2,3].exists(x, x < 0) == false'}") + @TestParameters("{expression: '[1,2,3].exists(i, v, i >= 0 && v > 0) == true'}") + @TestParameters("{expression: '[1,2,3].exists(i, v, i < 0 || v < 0) == false'}") + @TestParameters("{expression: '[1,2,3].map(x, x + 1) == [2,3,4]'}") + public void plan_comprehension_lists(String expression) throws Exception { + CelAbstractSyntaxTree ast = compile(expression); + Program program = PLANNER.plan(ast); + + boolean result = (boolean) program.eval(); + + assertThat(result).isTrue(); + } + + @Test + @TestParameters("{expression: '{\"a\": 1, \"b\": 2}.exists(k, k == \"a\")'}") + @TestParameters("{expression: '{\"a\": 1, \"b\": 2}.exists(k, k == \"c\") == false'}") + @TestParameters("{expression: '{\"a\": \"b\", \"c\": \"c\"}.exists(k, v, k == v)'}") + @TestParameters("{expression: '{\"a\": 1, \"b\": 2}.exists(k, v, v == 3) == false'}") + public void plan_comprehension_maps(String expression) throws Exception { + CelAbstractSyntaxTree ast = compile(expression); + Program program = PLANNER.plan(ast); + + boolean result = (boolean) program.eval(); + + assertThat(result).isTrue(); + } + + @Test + @TestParameters("{expression: '[1, 2, 3, 4, 5, 6].map(x, x)'}") + @TestParameters("{expression: '[1, 2, 3].map(x, [1, 2].map(y, x + y))'}") + public void plan_comprehension_iterationLimit_throws(String expression) throws Exception { + CelOptions options = CelOptions.current().comprehensionMaxIterations(5).build(); + ProgramPlanner planner = + ProgramPlanner.newPlanner( + TYPE_PROVIDER, + ProtoMessageValueProvider.newInstance(options, DYNAMIC_PROTO), + newDispatcher(), + CEL_VALUE_CONVERTER, + CEL_CONTAINER, + options, + ImmutableSet.of()); + CelAbstractSyntaxTree ast = compile(expression); + + Program program = planner.plan(ast); + + CelEvaluationException e = assertThrows(CelEvaluationException.class, program::eval); + assertThat(e).hasMessageThat().contains("Iteration budget exceeded: 5"); + assertThat(e.getErrorCode()).isEqualTo(CelErrorCode.ITERATION_BUDGET_EXCEEDED); + } + + @Test + public void plan_comprehension_iterationLimit_success() throws Exception { + CelOptions options = CelOptions.current().comprehensionMaxIterations(10).build(); + ProgramPlanner planner = + ProgramPlanner.newPlanner( + TYPE_PROVIDER, + ProtoMessageValueProvider.newInstance(options, DYNAMIC_PROTO), + newDispatcher(), + CEL_VALUE_CONVERTER, + CEL_CONTAINER, + options, + /* lateBoundFunctionNames= */ ImmutableSet.of()); + CelAbstractSyntaxTree ast = compile("[1, 2, 3].map(x, [1, 2].map(y, x + y))"); + + Program program = planner.plan(ast); + + Object result = program.eval(); + assertThat(result) + .isEqualTo( + ImmutableList.of( + ImmutableList.of(2L, 3L), ImmutableList.of(3L, 4L), ImmutableList.of(4L, 5L))); + } + + @Test + public void plan_partialEval_withWildcardQualification() throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addVar("unk", MapType.create(SimpleType.STRING, SimpleType.BOOL)) + .addVar("unk.a", SimpleType.BOOL) + .addVar("unk.b", SimpleType.BOOL) + .build(); + CelAbstractSyntaxTree ast = compile(compiler, "unk.a && unk.b && unk['c']"); + + Program program = PLANNER.plan(ast); + + CelUnknownSet result = + (CelUnknownSet) + program.eval( + PartialVars.of( + CelAttributePattern.create("unk") + .qualify(CelAttribute.Qualifier.ofWildCard()))); + + assertThat(result) + .isEqualTo( + CelUnknownSet.create( + ImmutableSet.of( + CelAttribute.create("unk"), + CelAttribute.create("unk").qualify(CelAttribute.Qualifier.ofString("a")), + CelAttribute.create("unk").qualify(CelAttribute.Qualifier.ofString("b"))), + ImmutableSet.of(2L, 5L, 7L))); + } + + @Test + public void localShadowIdentifier_inSelect() throws Exception { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addVar("cel.example.y", SimpleType.INT) + .build(); + ProgramPlanner planner = + ProgramPlanner.newPlanner( + TYPE_PROVIDER, + ProtoMessageValueProvider.newInstance(CEL_OPTIONS, DYNAMIC_PROTO), + newDispatcher(), + CEL_VALUE_CONVERTER, + CelContainer.ofName("cel.example"), + CEL_OPTIONS, + /* lateBoundFunctionNames= */ ImmutableSet.of()); + CelAbstractSyntaxTree ast = compile(celCompiler, "[{'z': 0}].exists(y, y.z == 0)"); + + Program program = planner.plan(ast); + + boolean result = + (boolean) program.eval(ImmutableMap.of("cel.example.y", ImmutableMap.of("z", 1))); + assertThat(result).isTrue(); + } + + @Test + public void localShadowIdentifier_inSelect_globalDisambiguation() throws Exception { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addVar("y.z", SimpleType.INT) + .build(); + ProgramPlanner planner = + ProgramPlanner.newPlanner( + TYPE_PROVIDER, + ProtoMessageValueProvider.newInstance(CEL_OPTIONS, DYNAMIC_PROTO), + newDispatcher(), + CEL_VALUE_CONVERTER, + CelContainer.ofName("y"), + CEL_OPTIONS, + /* lateBoundFunctionNames= */ ImmutableSet.of()); + CelAbstractSyntaxTree ast = compile(celCompiler, "[{'z': 0}].exists(y, y.z == 0 && .y.z == 1)"); + + Program program = planner.plan(ast); + + boolean result = (boolean) program.eval(ImmutableMap.of("y.z", 1)); + assertThat(result).isTrue(); + } + + @Test + public void localShadowIdentifier_withGlobalDisambiguation() throws Exception { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addVar("x", SimpleType.INT) + .build(); + ProgramPlanner planner = + ProgramPlanner.newPlanner( + TYPE_PROVIDER, + ProtoMessageValueProvider.newInstance(CEL_OPTIONS, DYNAMIC_PROTO), + newDispatcher(), + CEL_VALUE_CONVERTER, + CelContainer.newBuilder().build(), + CEL_OPTIONS, + /* lateBoundFunctionNames= */ ImmutableSet.of()); + CelAbstractSyntaxTree ast = compile(celCompiler, "[0].exists(x, x == 0 && .x == 1)"); + + Program program = planner.plan(ast); + + boolean result = (boolean) program.eval(ImmutableMap.of("x", 1)); + assertThat(result).isTrue(); + } + + @Test + public void localDoubleShadowIdentifier_withGlobalDisambiguation() throws Exception { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addVar("x", SimpleType.INT) + .build(); + ProgramPlanner planner = + ProgramPlanner.newPlanner( + TYPE_PROVIDER, + ProtoMessageValueProvider.newInstance(CEL_OPTIONS, DYNAMIC_PROTO), + newDispatcher(), + CEL_VALUE_CONVERTER, + CelContainer.newBuilder().build(), + CEL_OPTIONS, + /* lateBoundFunctionNames= */ ImmutableSet.of()); + CelAbstractSyntaxTree ast = compile(celCompiler, "[0].exists(x, [x+1].exists(x, x == .x))"); + + Program program = planner.plan(ast); + + boolean result = (boolean) program.eval(ImmutableMap.of("x", 1)); + assertThat(result).isTrue(); + } + + private CelAbstractSyntaxTree compile(String expression) throws Exception { + return compile(CEL_COMPILER, expression); + } + + private CelAbstractSyntaxTree compile(CelCompiler compiler, String expression) throws Exception { + CelAbstractSyntaxTree ast = compiler.parse(expression).getAst(); + if (isParseOnly) { + return ast; + } + + return compiler.check(ast).getAst(); + } + + private static CelByteString concatenateByteArrays(CelByteString bytes1, CelByteString bytes2) { + if (bytes1.isEmpty()) { + return bytes2; + } + + if (bytes2.isEmpty()) { + return bytes1; + } + + return bytes1.concat(bytes2); + } + + @SuppressWarnings("ImmutableEnumChecker") // Test only + private enum ConstantTestCase { + NULL("null", NullValue.NULL_VALUE), + BOOLEAN("true", true), + INT64("42", 42L), + UINT64("42u", UnsignedLong.valueOf(42)), + DOUBLE("1.5", 1.5d), + STRING("'hello world'", "hello world"), + BYTES("b'abc'", CelByteString.of("abc".getBytes(UTF_8))); + + private final String expression; + private final Object expected; + + ConstantTestCase(String expression, Object expected) { + this.expression = expression; + this.expected = expected; + } + } + + @SuppressWarnings("ImmutableEnumChecker") // Test only + private enum TypeLiteralTestCase { + BOOL("bool", SimpleType.BOOL), + BYTES("bytes", SimpleType.BYTES), + DOUBLE("double", SimpleType.DOUBLE), + INT("int", SimpleType.INT), + UINT("uint", SimpleType.UINT), + STRING("string", SimpleType.STRING), + LIST("list", ListType.create(SimpleType.DYN)), + MAP("map", MapType.create(SimpleType.DYN, SimpleType.DYN)), + NULL("null_type", SimpleType.NULL_TYPE), + DURATION("google.protobuf.Duration", SimpleType.DURATION), + TIMESTAMP("google.protobuf.Timestamp", SimpleType.TIMESTAMP), + OPTIONAL("optional_type", OptionalType.create(SimpleType.DYN)), + PROTO_MESSAGE_TYPE( + "cel.expr.conformance.proto3.TestAllTypes", + TYPE_PROVIDER.findType(TestAllTypes.getDescriptor().getFullName()).get()); + + private final String expression; + private final TypeType type; + + TypeLiteralTestCase(String expression, CelType type) { + this.expression = expression; + this.type = TypeType.create(type); + } + } + + private enum TypeObjectTestCase { + BOOL("type(true)", SimpleType.BOOL), + INT("type(1)", SimpleType.INT), + DOUBLE("type(1.5)", SimpleType.DOUBLE), + PROTO_MESSAGE_TYPE( + "type(cel.expr.conformance.proto3.TestAllTypes{})", + TYPE_PROVIDER.findType("cel.expr.conformance.proto3.TestAllTypes").get()); + + private final String expression; + private final TypeType type; + + TypeObjectTestCase(String expression, CelType type) { + this.expression = expression; + this.type = TypeType.create(type); + } + } + + @SuppressWarnings("Immutable") // Test only + private enum PresenceTestCase { + PROTO_FIELD_PRESENT( + "has(msg.single_string)", TestAllTypes.newBuilder().setSingleString("foo").build(), true), + PROTO_FIELD_ABSENT("has(msg.single_string)", TestAllTypes.getDefaultInstance(), false), + PROTO_NESTED_FIELD_PRESENT( + "has(msg.single_nested_message.bb)", + TestAllTypes.newBuilder() + .setSingleNestedMessage(NestedMessage.newBuilder().setBb(42).build()) + .build(), + true), + PROTO_NESTED_FIELD_ABSENT( + "has(msg.single_nested_message.bb)", TestAllTypes.getDefaultInstance(), false), + PROTO_MAP_KEY_PRESENT("has(map_var.foo)", ImmutableMap.of("foo", "1"), true), + PROTO_MAP_KEY_ABSENT("has(map_var.bar)", ImmutableMap.of(), false); + + private final String expression; + private final Object inputParam; + private final Object expected; + + PresenceTestCase(String expression, Object inputParam, Object expected) { + this.expression = expression; + this.inputParam = inputParam; + this.expected = expected; + } + } +} diff --git a/runtime/src/test/resources/BUILD.bazel b/runtime/src/test/resources/BUILD.bazel index 73b85b496..e419ff319 100644 --- a/runtime/src/test/resources/BUILD.bazel +++ b/runtime/src/test/resources/BUILD.bazel @@ -9,20 +9,3 @@ filegroup( name = "resources", srcs = glob(["*.baseline"]), ) - -java_proto_library( - name = "test_java_proto", - deps = [":test_proto"], -) - -proto_library( - name = "test_proto", - srcs = glob(["*.proto"]), - deps = [ - "@com_google_protobuf//:any_proto", - "@com_google_protobuf//:duration_proto", - "@com_google_protobuf//:struct_proto", - "@com_google_protobuf//:timestamp_proto", - "@com_google_protobuf//:wrappers_proto", - ], -) diff --git a/runtime/src/test/resources/arithmDouble.baseline b/runtime/src/test/resources/arithmDouble.baseline index 8d914202a..3c219ef29 100644 --- a/runtime/src/test/resources/arithmDouble.baseline +++ b/runtime/src/test/resources/arithmDouble.baseline @@ -1,3 +1,8 @@ +Source: 0.0 == -0.0 +=====> +bindings: {} +result: true + Source: 1.9 < 2.0 && 1.1 <= 1.1 && 2.0 > 1.9 && 1.1 >= 1.1 && 1.1 == 1.1 && 2.0 != 1.9 =====> bindings: {} diff --git a/runtime/src/test/resources/arithmDuration.baseline b/runtime/src/test/resources/arithmDuration.baseline index 3422aced4..9aa73405d 100644 --- a/runtime/src/test/resources/arithmDuration.baseline +++ b/runtime/src/test/resources/arithmDuration.baseline @@ -9,13 +9,7 @@ declare d3 { value google.protobuf.Duration } =====> -bindings: {d3=seconds: 25 -nanos: 45 -} +> {d2=seconds: 10 -nanos: 20 -} +> {d1=seconds: 15 -nanos: 25 -} +bindings: {d3=PT25.000000045S} +> {d2=PT10.00000002S} +> {d1=PT15.000000025S} result: true Source: d3 - d1 == d2 @@ -29,11 +23,5 @@ declare d3 { value google.protobuf.Duration } =====> -bindings: {d3=seconds: 25 -nanos: 45 -} +> {d2=seconds: 10 -nanos: 20 -} +> {d1=seconds: 15 -nanos: 25 -} +bindings: {d3=PT25.000000045S} +> {d2=PT10.00000002S} +> {d1=PT15.000000025S} result: true \ No newline at end of file diff --git a/runtime/src/test/resources/arithmInt64_error.baseline b/runtime/src/test/resources/arithmInt64_error.baseline index 4c3d44d4f..05080171e 100644 --- a/runtime/src/test/resources/arithmInt64_error.baseline +++ b/runtime/src/test/resources/arithmInt64_error.baseline @@ -1,41 +1,41 @@ Source: 9223372036854775807 + 1 =====> bindings: {} -error: evaluation error: long overflow +error: evaluation error at test_location:20: long overflow error_code: NUMERIC_OVERFLOW Source: -9223372036854775808 - 1 =====> bindings: {} -error: evaluation error: long overflow +error: evaluation error at test_location:21: long overflow error_code: NUMERIC_OVERFLOW Source: -(-9223372036854775808) =====> bindings: {} -error: evaluation error: long overflow +error: evaluation error at test_location:0: long overflow error_code: NUMERIC_OVERFLOW Source: 5000000000 * 5000000000 =====> bindings: {} -error: evaluation error: long overflow +error: evaluation error at test_location:11: long overflow error_code: NUMERIC_OVERFLOW Source: (-9223372036854775808)/-1 =====> bindings: {} -error: evaluation error: most negative number wraps +error: evaluation error at test_location:22: most negative number wraps error_code: NUMERIC_OVERFLOW Source: 1 / 0 =====> bindings: {} -error: evaluation error: / by zero +error: evaluation error at test_location:2: / by zero error_code: DIVIDE_BY_ZERO Source: 1 % 0 =====> bindings: {} -error: evaluation error: / by zero -error_code: DIVIDE_BY_ZERO +error: evaluation error at test_location:2: / by zero +error_code: DIVIDE_BY_ZERO \ No newline at end of file diff --git a/runtime/src/test/resources/arithmTimestamp.baseline b/runtime/src/test/resources/arithmTimestamp.baseline index d7a1f318c..7ddf702ae 100644 --- a/runtime/src/test/resources/arithmTimestamp.baseline +++ b/runtime/src/test/resources/arithmTimestamp.baseline @@ -9,13 +9,7 @@ declare d1 { value google.protobuf.Duration } =====> -bindings: {ts2=seconds: 10 -nanos: 10 -} +> {ts1=seconds: 25 -nanos: 35 -} +> {d1=seconds: 15 -nanos: 25 -} +bindings: {ts2=1970-01-01T00:00:10.000000010Z} +> {ts1=1970-01-01T00:00:25.000000035Z} +> {d1=PT15.000000025S} result: true Source: ts1 - d1 == ts2 @@ -29,13 +23,7 @@ declare d1 { value google.protobuf.Duration } =====> -bindings: {ts2=seconds: 10 -nanos: 10 -} +> {ts1=seconds: 25 -nanos: 35 -} +> {d1=seconds: 15 -nanos: 25 -} +bindings: {ts2=1970-01-01T00:00:10.000000010Z} +> {ts1=1970-01-01T00:00:25.000000035Z} +> {d1=PT15.000000025S} result: true Source: ts2 + d1 == ts1 @@ -49,13 +37,7 @@ declare d1 { value google.protobuf.Duration } =====> -bindings: {ts2=seconds: 10 -nanos: 10 -} +> {ts1=seconds: 25 -nanos: 35 -} +> {d1=seconds: 15 -nanos: 25 -} +bindings: {ts2=1970-01-01T00:00:10.000000010Z} +> {ts1=1970-01-01T00:00:25.000000035Z} +> {d1=PT15.000000025S} result: true Source: d1 + ts2 == ts1 @@ -69,11 +51,5 @@ declare d1 { value google.protobuf.Duration } =====> -bindings: {ts2=seconds: 10 -nanos: 10 -} +> {ts1=seconds: 25 -nanos: 35 -} +> {d1=seconds: 15 -nanos: 25 -} +bindings: {ts2=1970-01-01T00:00:10.000000010Z} +> {ts1=1970-01-01T00:00:25.000000035Z} +> {d1=PT15.000000025S} result: true \ No newline at end of file diff --git a/runtime/src/test/resources/arithmUInt64_error.baseline b/runtime/src/test/resources/arithmUInt64_error.baseline index d965a46e5..062e28005 100644 --- a/runtime/src/test/resources/arithmUInt64_error.baseline +++ b/runtime/src/test/resources/arithmUInt64_error.baseline @@ -1,29 +1,29 @@ Source: 18446744073709551615u + 1u =====> bindings: {} -error: evaluation error: range overflow on unsigned addition +error: evaluation error at test_location:22: range overflow on unsigned addition error_code: NUMERIC_OVERFLOW Source: 0u - 1u =====> bindings: {} -error: evaluation error: unsigned subtraction underflow +error: evaluation error at test_location:3: unsigned subtraction underflow error_code: NUMERIC_OVERFLOW Source: 5000000000u * 5000000000u =====> bindings: {} -error: evaluation error: multiply out of unsigned integer range +error: evaluation error at test_location:12: multiply out of unsigned integer range error_code: NUMERIC_OVERFLOW Source: 1u / 0u =====> bindings: {} -error: evaluation error: / by zero +error: evaluation error at test_location:3: / by zero error_code: DIVIDE_BY_ZERO Source: 1u % 0u =====> bindings: {} -error: evaluation error: / by zero -error_code: DIVIDE_BY_ZERO +error: evaluation error at test_location:3: / by zero +error_code: DIVIDE_BY_ZERO \ No newline at end of file diff --git a/runtime/src/test/resources/boolConversions.baseline b/runtime/src/test/resources/boolConversions.baseline new file mode 100644 index 000000000..46117866f --- /dev/null +++ b/runtime/src/test/resources/boolConversions.baseline @@ -0,0 +1,14 @@ +Source: bool(true) +=====> +bindings: {} +result: true + +Source: bool('true') && bool('TRUE') && bool('True') && bool('t') && bool('1') +=====> +bindings: {} +result: true + +Source: bool('false') || bool('FALSE') || bool('False') || bool('f') || bool('0') +=====> +bindings: {} +result: false \ No newline at end of file diff --git a/runtime/src/test/resources/boolConversions_error.baseline b/runtime/src/test/resources/boolConversions_error.baseline new file mode 100644 index 000000000..a2c6a11dd --- /dev/null +++ b/runtime/src/test/resources/boolConversions_error.baseline @@ -0,0 +1,11 @@ +Source: bool('TrUe') +=====> +bindings: {} +error: evaluation error at test_location:4: Type conversion error from 'string' to 'bool': [TrUe] +error_code: BAD_FORMAT + +Source: bool('FaLsE') +=====> +bindings: {} +error: evaluation error at test_location:4: Type conversion error from 'string' to 'bool': [FaLsE] +error_code: BAD_FORMAT \ No newline at end of file diff --git a/runtime/src/test/resources/booleans.baseline b/runtime/src/test/resources/booleans.baseline index 98bbfba9e..6c0fb5a1a 100644 --- a/runtime/src/test/resources/booleans.baseline +++ b/runtime/src/test/resources/booleans.baseline @@ -41,54 +41,6 @@ declare y { bindings: {y=0} result: true -Source: 1 / y == 1 || false -declare x { - value bool -} -declare y { - value int -} -=====> -bindings: {y=0} -error: evaluation error: / by zero -error_code: DIVIDE_BY_ZERO - -Source: false || 1 / y == 1 -declare x { - value bool -} -declare y { - value int -} -=====> -bindings: {y=0} -error: evaluation error: / by zero -error_code: DIVIDE_BY_ZERO - -Source: 1 / y == 1 && true -declare x { - value bool -} -declare y { - value int -} -=====> -bindings: {y=0} -error: evaluation error: / by zero -error_code: DIVIDE_BY_ZERO - -Source: true && 1 / y == 1 -declare x { - value bool -} -declare y { - value int -} -=====> -bindings: {y=0} -error: evaluation error: / by zero -error_code: DIVIDE_BY_ZERO - Source: 1 / y == 1 && false declare x { value bool @@ -274,5 +226,4 @@ declare y { } =====> bindings: {} -result: true - +result: true \ No newline at end of file diff --git a/runtime/src/test/resources/booleans_error.baseline b/runtime/src/test/resources/booleans_error.baseline new file mode 100644 index 000000000..e7ecf568b --- /dev/null +++ b/runtime/src/test/resources/booleans_error.baseline @@ -0,0 +1,35 @@ +Source: 1 / y == 1 || false +declare y { + value int +} +=====> +bindings: {y=0} +error: evaluation error at test_location:2: / by zero +error_code: DIVIDE_BY_ZERO + +Source: false || 1 / y == 1 +declare y { + value int +} +=====> +bindings: {y=0} +error: evaluation error at test_location:11: / by zero +error_code: DIVIDE_BY_ZERO + +Source: 1 / y == 1 && true +declare y { + value int +} +=====> +bindings: {y=0} +error: evaluation error at test_location:2: / by zero +error_code: DIVIDE_BY_ZERO + +Source: true && 1 / y == 1 +declare y { + value int +} +=====> +bindings: {y=0} +error: evaluation error at test_location:10: / by zero +error_code: DIVIDE_BY_ZERO \ No newline at end of file diff --git a/runtime/src/test/resources/bytesConversions.baseline b/runtime/src/test/resources/bytesConversions.baseline index 4c1458268..6324929ce 100644 --- a/runtime/src/test/resources/bytesConversions.baseline +++ b/runtime/src/test/resources/bytesConversions.baseline @@ -2,3 +2,8 @@ Source: bytes('abc\303') =====> bindings: {} result: abcà + +Source: bytes(bytes('abc\303')) +=====> +bindings: {} +result: abcà \ No newline at end of file diff --git a/runtime/src/test/resources/comprehension.baseline b/runtime/src/test/resources/comprehension.baseline index 45f9f211c..aa0ecc3f9 100644 --- a/runtime/src/test/resources/comprehension.baseline +++ b/runtime/src/test/resources/comprehension.baseline @@ -12,3 +12,43 @@ Source: [0, 1, 2].exists(x, x > 2) =====> bindings: {} result: false + +Source: [0].exists(x, x == 0) +declare com.x { + value int +} +=====> +bindings: {com.x=1} +result: true + +Source: [{'z': 0}].exists(y, y.z == 0) +declare cel.example.y { + value int +} +=====> +bindings: {cel.example.y={z=1}} +result: true + +Source: [{'z': 0}].exists(y, y.z == 0 && .y.z == 1) +declare y.z { + value int +} +=====> +bindings: {y.z=1} +result: true + +Source: [0].exists(x, x == 0 && .x == 1) +declare x { + value int +} +=====> +bindings: {x=1} +result: true + +Source: [0].exists(x, [x+1].exists(x, x == .x)) +declare x { + value int +} +=====> +bindings: {x=1} +result: true diff --git a/runtime/src/test/resources/containers.baseline b/runtime/src/test/resources/containers.baseline new file mode 100644 index 000000000..4e29c64f3 --- /dev/null +++ b/runtime/src/test/resources/containers.baseline @@ -0,0 +1,19 @@ +Source: test_alias.TestAllTypes{} == cel.expr.conformance.proto3.TestAllTypes{} +=====> +bindings: {} +result: true + +Source: proto2.TestAllTypes{} == cel.expr.conformance.proto2.TestAllTypes{} +=====> +bindings: {} +result: true + +Source: proto3.TestAllTypes{} == cel.expr.conformance.proto3.TestAllTypes{} +=====> +bindings: {} +result: true + +Source: SGAR +=====> +bindings: {} +result: 1 diff --git a/runtime/src/test/resources/delayedEvaluation.baseline b/runtime/src/test/resources/delayedEvaluation.baseline index 887611d2d..8604c359c 100644 --- a/runtime/src/test/resources/delayedEvaluation.baseline +++ b/runtime/src/test/resources/delayedEvaluation.baseline @@ -10,29 +10,29 @@ bindings: {} result: true Source: f_force(f_delay(1 + four)) == 5 +declare four { + value int +} declare f_delay { function f_delay (int) -> dyn } declare f_force { function f_force (dyn) -> int } -declare four { - value int -} =====> bindings: {four=4} result: true Source: [1, 2, 3].map(i, f_delay(i + four)).map(d, f_force(d)) == [5, 6, 7] +declare four { + value int +} declare f_delay { function f_delay (int) -> dyn } declare f_force { function f_force (dyn) -> int } -declare four { - value int -} =====> bindings: {four=4} result: true diff --git a/runtime/src/test/resources/doubleConversions.baseline b/runtime/src/test/resources/doubleConversions.baseline index d15dfe090..3c073ec39 100644 --- a/runtime/src/test/resources/doubleConversions.baseline +++ b/runtime/src/test/resources/doubleConversions.baseline @@ -13,8 +13,7 @@ Source: double(-1) bindings: {} result: -1.0 -Source: double('bad') +Source: double(1.5) =====> bindings: {} -error: evaluation error: For input string: "bad" -error_code: BAD_FORMAT +result: 1.5 \ No newline at end of file diff --git a/runtime/src/test/resources/doubleConversions_error.baseline b/runtime/src/test/resources/doubleConversions_error.baseline new file mode 100644 index 000000000..69f77ccf1 --- /dev/null +++ b/runtime/src/test/resources/doubleConversions_error.baseline @@ -0,0 +1,5 @@ +Source: double('bad') +=====> +bindings: {} +error: evaluation error at test_location:6: For input string: "bad" +error_code: BAD_FORMAT \ No newline at end of file diff --git a/runtime/src/test/resources/duration.baseline b/runtime/src/test/resources/duration.baseline index 9737a23c7..89d058086 100644 --- a/runtime/src/test/resources/duration.baseline +++ b/runtime/src/test/resources/duration.baseline @@ -6,11 +6,7 @@ declare d2 { value google.protobuf.Duration } =====> -bindings: {d2=seconds: 10 -nanos: 9 -} +> {d1=seconds: 10 -nanos: 10 -} +bindings: {d2=PT10.000000009S} +> {d1=PT10.00000001S} result: false Source: d1 < d2 @@ -21,11 +17,7 @@ declare d2 { value google.protobuf.Duration } =====> -bindings: {d2=seconds: 9 -nanos: 10 -} +> {d1=seconds: 10 -nanos: 10 -} +bindings: {d2=PT9.00000001S} +> {d1=PT10.00000001S} result: false Source: d1 < d2 @@ -36,11 +28,7 @@ declare d2 { value google.protobuf.Duration } =====> -bindings: {d2=seconds: 10 -nanos: 10 -} +> {d1=seconds: 10 -nanos: 10 -} +bindings: {d2=PT10.00000001S} +> {d1=PT10.00000001S} result: false Source: d1 < d2 @@ -51,11 +39,7 @@ declare d2 { value google.protobuf.Duration } =====> -bindings: {d2=seconds: 10 -nanos: 10 -} +> {d1=seconds: 10 -nanos: 9 -} +bindings: {d2=PT10.00000001S} +> {d1=PT10.000000009S} result: true Source: d1 < d2 @@ -66,11 +50,7 @@ declare d2 { value google.protobuf.Duration } =====> -bindings: {d2=seconds: 10 -nanos: 10 -} +> {d1=seconds: 9 -nanos: 10 -} +bindings: {d2=PT10.00000001S} +> {d1=PT9.00000001S} result: true Source: d1 <= d2 @@ -81,11 +61,7 @@ declare d2 { value google.protobuf.Duration } =====> -bindings: {d2=seconds: 10 -nanos: 9 -} +> {d1=seconds: 10 -nanos: 10 -} +bindings: {d2=PT10.000000009S} +> {d1=PT10.00000001S} result: false Source: d1 <= d2 @@ -96,11 +72,7 @@ declare d2 { value google.protobuf.Duration } =====> -bindings: {d2=seconds: 9 -nanos: 10 -} +> {d1=seconds: 10 -nanos: 10 -} +bindings: {d2=PT9.00000001S} +> {d1=PT10.00000001S} result: false Source: d1 <= d2 @@ -111,11 +83,7 @@ declare d2 { value google.protobuf.Duration } =====> -bindings: {d2=seconds: 10 -nanos: 10 -} +> {d1=seconds: 10 -nanos: 10 -} +bindings: {d2=PT10.00000001S} +> {d1=PT10.00000001S} result: true Source: d1 <= d2 @@ -126,11 +94,7 @@ declare d2 { value google.protobuf.Duration } =====> -bindings: {d2=seconds: 10 -nanos: 10 -} +> {d1=seconds: 10 -nanos: 9 -} +bindings: {d2=PT10.00000001S} +> {d1=PT10.000000009S} result: true Source: d1 <= d2 @@ -141,11 +105,7 @@ declare d2 { value google.protobuf.Duration } =====> -bindings: {d2=seconds: 10 -nanos: 10 -} +> {d1=seconds: 9 -nanos: 10 -} +bindings: {d2=PT10.00000001S} +> {d1=PT9.00000001S} result: true Source: d1 > d2 @@ -156,11 +116,7 @@ declare d2 { value google.protobuf.Duration } =====> -bindings: {d2=seconds: 10 -nanos: 9 -} +> {d1=seconds: 10 -nanos: 10 -} +bindings: {d2=PT10.000000009S} +> {d1=PT10.00000001S} result: true Source: d1 > d2 @@ -171,11 +127,7 @@ declare d2 { value google.protobuf.Duration } =====> -bindings: {d2=seconds: 9 -nanos: 10 -} +> {d1=seconds: 10 -nanos: 10 -} +bindings: {d2=PT9.00000001S} +> {d1=PT10.00000001S} result: true Source: d1 > d2 @@ -186,11 +138,7 @@ declare d2 { value google.protobuf.Duration } =====> -bindings: {d2=seconds: 10 -nanos: 10 -} +> {d1=seconds: 10 -nanos: 10 -} +bindings: {d2=PT10.00000001S} +> {d1=PT10.00000001S} result: false Source: d1 > d2 @@ -201,11 +149,7 @@ declare d2 { value google.protobuf.Duration } =====> -bindings: {d2=seconds: 10 -nanos: 10 -} +> {d1=seconds: 10 -nanos: 9 -} +bindings: {d2=PT10.00000001S} +> {d1=PT10.000000009S} result: false Source: d1 > d2 @@ -216,11 +160,7 @@ declare d2 { value google.protobuf.Duration } =====> -bindings: {d2=seconds: 10 -nanos: 10 -} +> {d1=seconds: 9 -nanos: 10 -} +bindings: {d2=PT10.00000001S} +> {d1=PT9.00000001S} result: false Source: d1 >= d2 @@ -231,11 +171,7 @@ declare d2 { value google.protobuf.Duration } =====> -bindings: {d2=seconds: 10 -nanos: 9 -} +> {d1=seconds: 10 -nanos: 10 -} +bindings: {d2=PT10.000000009S} +> {d1=PT10.00000001S} result: true Source: d1 >= d2 @@ -246,11 +182,7 @@ declare d2 { value google.protobuf.Duration } =====> -bindings: {d2=seconds: 9 -nanos: 10 -} +> {d1=seconds: 10 -nanos: 10 -} +bindings: {d2=PT9.00000001S} +> {d1=PT10.00000001S} result: true Source: d1 >= d2 @@ -261,11 +193,7 @@ declare d2 { value google.protobuf.Duration } =====> -bindings: {d2=seconds: 10 -nanos: 10 -} +> {d1=seconds: 10 -nanos: 10 -} +bindings: {d2=PT10.00000001S} +> {d1=PT10.00000001S} result: true Source: d1 >= d2 @@ -276,11 +204,7 @@ declare d2 { value google.protobuf.Duration } =====> -bindings: {d2=seconds: 10 -nanos: 10 -} +> {d1=seconds: 10 -nanos: 9 -} +bindings: {d2=PT10.00000001S} +> {d1=PT10.000000009S} result: false Source: d1 >= d2 @@ -291,9 +215,5 @@ declare d2 { value google.protobuf.Duration } =====> -bindings: {d2=seconds: 10 -nanos: 10 -} +> {d1=seconds: 9 -nanos: 10 -} +bindings: {d2=PT10.00000001S} +> {d1=PT9.00000001S} result: false \ No newline at end of file diff --git a/runtime/src/test/resources/durationFunctions.baseline b/runtime/src/test/resources/durationFunctions.baseline index 8e942e0d8..9bc61d4ae 100644 --- a/runtime/src/test/resources/durationFunctions.baseline +++ b/runtime/src/test/resources/durationFunctions.baseline @@ -3,9 +3,7 @@ declare d1 { value google.protobuf.Duration } =====> -bindings: {d1=seconds: 93541 -nanos: 11000000 -} +bindings: {d1=PT25H59M1.011S} result: 25 Source: d1.getHours() @@ -13,9 +11,7 @@ declare d1 { value google.protobuf.Duration } =====> -bindings: {d1=seconds: -93541 -nanos: -11000000 -} +bindings: {d1=PT-25H-59M-1.011S} result: -25 Source: d1.getMinutes() @@ -23,9 +19,7 @@ declare d1 { value google.protobuf.Duration } =====> -bindings: {d1=seconds: 93541 -nanos: 11000000 -} +bindings: {d1=PT25H59M1.011S} result: 1559 Source: d1.getMinutes() @@ -33,9 +27,7 @@ declare d1 { value google.protobuf.Duration } =====> -bindings: {d1=seconds: -93541 -nanos: -11000000 -} +bindings: {d1=PT-25H-59M-1.011S} result: -1559 Source: d1.getSeconds() @@ -43,9 +35,7 @@ declare d1 { value google.protobuf.Duration } =====> -bindings: {d1=seconds: 93541 -nanos: 11000000 -} +bindings: {d1=PT25H59M1.011S} result: 93541 Source: d1.getSeconds() @@ -53,9 +43,7 @@ declare d1 { value google.protobuf.Duration } =====> -bindings: {d1=seconds: -93541 -nanos: -11000000 -} +bindings: {d1=PT-25H-59M-1.011S} result: -93541 Source: d1.getMilliseconds() @@ -63,9 +51,7 @@ declare d1 { value google.protobuf.Duration } =====> -bindings: {d1=seconds: 93541 -nanos: 11000000 -} +bindings: {d1=PT25H59M1.011S} result: 11 Source: d1.getMilliseconds() @@ -73,9 +59,7 @@ declare d1 { value google.protobuf.Duration } =====> -bindings: {d1=seconds: -93541 -nanos: -11000000 -} +bindings: {d1=PT-25H-59M-1.011S} result: -11 Source: d1.getHours() < val @@ -86,9 +70,7 @@ declare val { value int } =====> -bindings: {val=30} +> {d1=seconds: 93541 -nanos: 11000000 -} +bindings: {val=30} +> {d1=PT25H59M1.011S} result: true Source: d1.getMinutes() > val @@ -99,9 +81,7 @@ declare val { value int } =====> -bindings: {val=30} +> {d1=seconds: 93541 -nanos: 11000000 -} +bindings: {val=30} +> {d1=PT25H59M1.011S} result: true Source: d1.getSeconds() > val @@ -112,9 +92,7 @@ declare val { value int } =====> -bindings: {val=30} +> {d1=seconds: 93541 -nanos: 11000000 -} +bindings: {val=30} +> {d1=PT25H59M1.011S} result: true Source: d1.getMilliseconds() < val @@ -125,7 +103,5 @@ declare val { value int } =====> -bindings: {val=30} +> {d1=seconds: 93541 -nanos: 11000000 -} -result: true \ No newline at end of file +bindings: {val=30} +> {d1=PT25H59M1.011S} +result: true diff --git a/runtime/src/test/resources/dyn_error.baseline b/runtime/src/test/resources/dyn_error.baseline new file mode 100644 index 000000000..00a0766cd --- /dev/null +++ b/runtime/src/test/resources/dyn_error.baseline @@ -0,0 +1,23 @@ +Source: dyn('hello').invalid +=====> +bindings: {} +error: evaluation error at test_location:12: Error resolving field 'invalid'. Field selections must be performed on messages or maps. +error_code: ATTRIBUTE_NOT_FOUND + +Source: has(dyn('hello').invalid) +=====> +bindings: {} +error: evaluation error at test_location:3: Error resolving field 'invalid'. Field selections must be performed on messages or maps. +error_code: ATTRIBUTE_NOT_FOUND + +Source: dyn([]).invalid +=====> +bindings: {} +error: evaluation error at test_location:7: Error resolving field 'invalid'. Field selections must be performed on messages or maps. +error_code: ATTRIBUTE_NOT_FOUND + +Source: has(dyn([]).invalid) +=====> +bindings: {} +error: evaluation error at test_location:3: Error resolving field 'invalid'. Field selections must be performed on messages or maps. +error_code: ATTRIBUTE_NOT_FOUND diff --git a/runtime/src/test/resources/dynamicMessage.baseline b/runtime/src/test/resources/dynamicMessage_adapted.baseline similarity index 80% rename from runtime/src/test/resources/dynamicMessage.baseline rename to runtime/src/test/resources/dynamicMessage_adapted.baseline index 21751184b..b012c3727 100644 --- a/runtime/src/test/resources/dynamicMessage.baseline +++ b/runtime/src/test/resources/dynamicMessage_adapted.baseline @@ -1,10 +1,10 @@ Source: msg.single_any declare msg { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> bindings: {msg=single_any { - type_url: "type.googleapis.com/dev.cel.testing.testdata.proto3.TestAllTypes.NestedMessage" + type_url: "type.googleapis.com/cel.expr.conformance.proto3.TestAllTypes.NestedMessage" value: "\b*" } single_duration { @@ -53,7 +53,7 @@ single_bool_wrapper { single_bytes_wrapper { value: "hi" } -single_list_value { +list_value { values { string_value: "d" } @@ -64,11 +64,11 @@ result: bb: 42 Source: msg.single_bool_wrapper declare msg { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> bindings: {msg=single_any { - type_url: "type.googleapis.com/dev.cel.testing.testdata.proto3.TestAllTypes.NestedMessage" + type_url: "type.googleapis.com/cel.expr.conformance.proto3.TestAllTypes.NestedMessage" value: "\b*" } single_duration { @@ -117,7 +117,7 @@ single_bool_wrapper { single_bytes_wrapper { value: "hi" } -single_list_value { +list_value { values { string_value: "d" } @@ -127,11 +127,11 @@ result: true Source: msg.single_bytes_wrapper declare msg { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> bindings: {msg=single_any { - type_url: "type.googleapis.com/dev.cel.testing.testdata.proto3.TestAllTypes.NestedMessage" + type_url: "type.googleapis.com/cel.expr.conformance.proto3.TestAllTypes.NestedMessage" value: "\b*" } single_duration { @@ -180,7 +180,7 @@ single_bool_wrapper { single_bytes_wrapper { value: "hi" } -single_list_value { +list_value { values { string_value: "d" } @@ -190,11 +190,11 @@ result: hi Source: msg.single_double_wrapper declare msg { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> bindings: {msg=single_any { - type_url: "type.googleapis.com/dev.cel.testing.testdata.proto3.TestAllTypes.NestedMessage" + type_url: "type.googleapis.com/cel.expr.conformance.proto3.TestAllTypes.NestedMessage" value: "\b*" } single_duration { @@ -243,7 +243,7 @@ single_bool_wrapper { single_bytes_wrapper { value: "hi" } -single_list_value { +list_value { values { string_value: "d" } @@ -253,11 +253,11 @@ result: -3.0 Source: msg.single_float_wrapper declare msg { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> bindings: {msg=single_any { - type_url: "type.googleapis.com/dev.cel.testing.testdata.proto3.TestAllTypes.NestedMessage" + type_url: "type.googleapis.com/cel.expr.conformance.proto3.TestAllTypes.NestedMessage" value: "\b*" } single_duration { @@ -306,7 +306,7 @@ single_bool_wrapper { single_bytes_wrapper { value: "hi" } -single_list_value { +list_value { values { string_value: "d" } @@ -316,11 +316,11 @@ result: 1.5 Source: msg.single_int32_wrapper declare msg { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> bindings: {msg=single_any { - type_url: "type.googleapis.com/dev.cel.testing.testdata.proto3.TestAllTypes.NestedMessage" + type_url: "type.googleapis.com/cel.expr.conformance.proto3.TestAllTypes.NestedMessage" value: "\b*" } single_duration { @@ -369,7 +369,7 @@ single_bool_wrapper { single_bytes_wrapper { value: "hi" } -single_list_value { +list_value { values { string_value: "d" } @@ -379,11 +379,11 @@ result: -12 Source: msg.single_int64_wrapper declare msg { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> bindings: {msg=single_any { - type_url: "type.googleapis.com/dev.cel.testing.testdata.proto3.TestAllTypes.NestedMessage" + type_url: "type.googleapis.com/cel.expr.conformance.proto3.TestAllTypes.NestedMessage" value: "\b*" } single_duration { @@ -432,7 +432,7 @@ single_bool_wrapper { single_bytes_wrapper { value: "hi" } -single_list_value { +list_value { values { string_value: "d" } @@ -442,11 +442,11 @@ result: -34 Source: msg.single_string_wrapper declare msg { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> bindings: {msg=single_any { - type_url: "type.googleapis.com/dev.cel.testing.testdata.proto3.TestAllTypes.NestedMessage" + type_url: "type.googleapis.com/cel.expr.conformance.proto3.TestAllTypes.NestedMessage" value: "\b*" } single_duration { @@ -495,7 +495,7 @@ single_bool_wrapper { single_bytes_wrapper { value: "hi" } -single_list_value { +list_value { values { string_value: "d" } @@ -505,11 +505,11 @@ result: hello Source: msg.single_uint32_wrapper declare msg { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> bindings: {msg=single_any { - type_url: "type.googleapis.com/dev.cel.testing.testdata.proto3.TestAllTypes.NestedMessage" + type_url: "type.googleapis.com/cel.expr.conformance.proto3.TestAllTypes.NestedMessage" value: "\b*" } single_duration { @@ -558,7 +558,7 @@ single_bool_wrapper { single_bytes_wrapper { value: "hi" } -single_list_value { +list_value { values { string_value: "d" } @@ -568,11 +568,11 @@ result: 12 Source: msg.single_uint64_wrapper declare msg { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> bindings: {msg=single_any { - type_url: "type.googleapis.com/dev.cel.testing.testdata.proto3.TestAllTypes.NestedMessage" + type_url: "type.googleapis.com/cel.expr.conformance.proto3.TestAllTypes.NestedMessage" value: "\b*" } single_duration { @@ -621,7 +621,7 @@ single_bool_wrapper { single_bytes_wrapper { value: "hi" } -single_list_value { +list_value { values { string_value: "d" } @@ -631,11 +631,11 @@ result: 34 Source: msg.single_duration declare msg { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> bindings: {msg=single_any { - type_url: "type.googleapis.com/dev.cel.testing.testdata.proto3.TestAllTypes.NestedMessage" + type_url: "type.googleapis.com/cel.expr.conformance.proto3.TestAllTypes.NestedMessage" value: "\b*" } single_duration { @@ -684,23 +684,21 @@ single_bool_wrapper { single_bytes_wrapper { value: "hi" } -single_list_value { +list_value { values { string_value: "d" } } } -result: seconds: 10 -nanos: 20 - +result: PT10.00000002S Source: msg.single_timestamp declare msg { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> bindings: {msg=single_any { - type_url: "type.googleapis.com/dev.cel.testing.testdata.proto3.TestAllTypes.NestedMessage" + type_url: "type.googleapis.com/cel.expr.conformance.proto3.TestAllTypes.NestedMessage" value: "\b*" } single_duration { @@ -749,23 +747,21 @@ single_bool_wrapper { single_bytes_wrapper { value: "hi" } -single_list_value { +list_value { values { string_value: "d" } } } -result: seconds: 100 -nanos: 200 - +result: 1970-01-01T00:01:40.000000200Z Source: msg.single_value declare msg { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> bindings: {msg=single_any { - type_url: "type.googleapis.com/dev.cel.testing.testdata.proto3.TestAllTypes.NestedMessage" + type_url: "type.googleapis.com/cel.expr.conformance.proto3.TestAllTypes.NestedMessage" value: "\b*" } single_duration { @@ -814,7 +810,7 @@ single_bool_wrapper { single_bytes_wrapper { value: "hi" } -single_list_value { +list_value { values { string_value: "d" } @@ -824,11 +820,11 @@ result: a Source: msg.single_struct declare msg { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> bindings: {msg=single_any { - type_url: "type.googleapis.com/dev.cel.testing.testdata.proto3.TestAllTypes.NestedMessage" + type_url: "type.googleapis.com/cel.expr.conformance.proto3.TestAllTypes.NestedMessage" value: "\b*" } single_duration { @@ -877,7 +873,7 @@ single_bool_wrapper { single_bytes_wrapper { value: "hi" } -single_list_value { +list_value { values { string_value: "d" } @@ -885,13 +881,13 @@ single_list_value { } result: {b=c} -Source: msg.single_list_value +Source: msg.list_value declare msg { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> bindings: {msg=single_any { - type_url: "type.googleapis.com/dev.cel.testing.testdata.proto3.TestAllTypes.NestedMessage" + type_url: "type.googleapis.com/cel.expr.conformance.proto3.TestAllTypes.NestedMessage" value: "\b*" } single_duration { @@ -940,7 +936,7 @@ single_bool_wrapper { single_bytes_wrapper { value: "hi" } -single_list_value { +list_value { values { string_value: "d" } diff --git a/runtime/src/test/resources/dynamicMessage_dynamicDescriptor.baseline b/runtime/src/test/resources/dynamicMessage_dynamicDescriptor.baseline new file mode 100644 index 000000000..8f91353b4 --- /dev/null +++ b/runtime/src/test/resources/dynamicMessage_dynamicDescriptor.baseline @@ -0,0 +1,221 @@ +Source: TestAllTypes {} +=====> +bindings: {} +result: + +Source: TestAllTypes { single_int32: 1, single_int64: 2, single_string: 'hello'} +=====> +bindings: {} +result: single_int32: 1 +single_int64: 2 +single_string: "hello" + + +Source: TestAllTypes { single_int32: 1, single_int64: 2, single_string: 'hello'}.single_string +=====> +bindings: {} +result: hello + +Source: TestAllTypes { single_int32_wrapper: 3 }.single_int32_wrapper +=====> +bindings: {} +result: 3 + +Source: TestAllTypes { single_int64_wrapper: 3 }.single_int64_wrapper +=====> +bindings: {} +result: 3 + +Source: TestAllTypes { single_bool_wrapper: true }.single_bool_wrapper +=====> +bindings: {} +result: true + +Source: TestAllTypes { single_bytes_wrapper: b'abc' }.single_bytes_wrapper +=====> +bindings: {} +result: abc + +Source: TestAllTypes { single_float_wrapper: 1.1 }.single_float_wrapper +=====> +bindings: {} +result: 1.100000023841858 + +Source: TestAllTypes { single_double_wrapper: 1.1 }.single_double_wrapper +=====> +bindings: {} +result: 1.1 + +Source: TestAllTypes { single_uint32_wrapper: 2u}.single_uint32_wrapper +=====> +bindings: {} +result: 2 + +Source: TestAllTypes { single_uint64_wrapper: 2u}.single_uint64_wrapper +=====> +bindings: {} +result: 2 + +Source: TestAllTypes { single_list_value: ['a', 1.5, true] }.single_list_value +=====> +bindings: {} +result: [a, 1.5, true] + +Source: TestAllTypes { standalone_message: TestAllTypes.NestedMessage { } }.standalone_message +=====> +bindings: {} +result: + +Source: TestAllTypes { standalone_message: TestAllTypes.NestedMessage { bb: 5} }.standalone_message.bb +=====> +bindings: {} +result: 5 + +Source: TestAllTypes { standalone_enum: TestAllTypes.NestedEnum.BAR }.standalone_enum +=====> +bindings: {} +result: 1 + +Source: TestAllTypes { map_string_string: {'key': 'value'}} +=====> +bindings: {} +result: map_string_string { + key: "key" + value: "value" +} + + +Source: TestAllTypes { map_string_string: {'key': 'value'}}.map_string_string +=====> +bindings: {} +result: {key=value} + +Source: TestAllTypes { map_string_string: {'key': 'value'}}.map_string_string['key'] +=====> +bindings: {} +result: value + +Source: TestAllTypes { single_any: dur }.single_any +declare dur { + value google.protobuf.Timestamp +} +=====> +bindings: {dur=type_url: "type.googleapis.com/google.protobuf.Duration" +value: "\bd" +} +result: PT1M40S + +Source: TestAllTypes { single_any: any_packed_test_msg }.single_any +declare any_packed_test_msg { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {any_packed_test_msg=type_url: "type.googleapis.com/cel.expr.conformance.proto3.TestAllTypes" +value: "r\005hello" +} +result: single_string: "hello" + + +Source: dynamic_msg +declare any_packed_test_msg { + value cel.expr.conformance.proto3.TestAllTypes +} +declare test_msg { + value cel.expr.conformance.proto3.TestAllTypes +} +declare dynamic_msg { + value dev.cel.testing.testdata.serialized.proto3.TestAllTypes +} +=====> +bindings: {dynamic_msg=map_string_string { + key: "foo" + value: "bar" +} +} +result: map_string_string { + key: "foo" + value: "bar" +} + + +Source: dynamic_msg.map_string_string +declare any_packed_test_msg { + value cel.expr.conformance.proto3.TestAllTypes +} +declare test_msg { + value cel.expr.conformance.proto3.TestAllTypes +} +declare dynamic_msg { + value dev.cel.testing.testdata.serialized.proto3.TestAllTypes +} +=====> +bindings: {dynamic_msg=map_string_string { + key: "foo" + value: "bar" +} +} +result: {foo=bar} + +Source: dynamic_msg.map_string_string['foo'] +declare any_packed_test_msg { + value cel.expr.conformance.proto3.TestAllTypes +} +declare test_msg { + value cel.expr.conformance.proto3.TestAllTypes +} +declare dynamic_msg { + value dev.cel.testing.testdata.serialized.proto3.TestAllTypes +} +=====> +bindings: {dynamic_msg=map_string_string { + key: "foo" + value: "bar" +} +} +result: bar + +Source: f_msg(dynamic_msg) +declare any_packed_test_msg { + value cel.expr.conformance.proto3.TestAllTypes +} +declare test_msg { + value cel.expr.conformance.proto3.TestAllTypes +} +declare dynamic_msg { + value dev.cel.testing.testdata.serialized.proto3.TestAllTypes +} +declare f_msg { + function f_msg_generated (cel.expr.conformance.proto3.TestAllTypes) -> bool + function f_msg_dynamic (dev.cel.testing.testdata.serialized.proto3.TestAllTypes) -> bool +} +=====> +bindings: {dynamic_msg=map_string_string { + key: "foo" + value: "bar" +} +, test_msg=single_int64: 10 +} +result: true + +Source: f_msg(test_msg) +declare any_packed_test_msg { + value cel.expr.conformance.proto3.TestAllTypes +} +declare test_msg { + value cel.expr.conformance.proto3.TestAllTypes +} +declare dynamic_msg { + value dev.cel.testing.testdata.serialized.proto3.TestAllTypes +} +declare f_msg { + function f_msg_generated (cel.expr.conformance.proto3.TestAllTypes) -> bool + function f_msg_dynamic (dev.cel.testing.testdata.serialized.proto3.TestAllTypes) -> bool +} +=====> +bindings: {dynamic_msg=map_string_string { + key: "foo" + value: "bar" +} +, test_msg=single_int64: 10 +} +result: true diff --git a/runtime/src/test/resources/extensionManipulation.baseline b/runtime/src/test/resources/extensionManipulation.baseline index 5c64f9bf8..00e2bba9c 100644 --- a/runtime/src/test/resources/extensionManipulation.baseline +++ b/runtime/src/test/resources/extensionManipulation.baseline @@ -5,60 +5,60 @@ Source: [y.hasI(), y.getI() == 200, !n.hasI(), n.getI() == 0, y.hasN(), y.getN().getI() == 0, !y.getN().hasN(), y.getN().getN().getI() == 0, !n.hasN(), n.assignN(y).getN().hasN(), !n.clearN().hasN(), !y.clearN().hasN(), - n.getR() == [], y.getR().map(h, h.s) == ["alpha", "beta"], - n.assignR(["a", "b"].map(s, StringHolder{s:s})).getR().map(h, h.s) == ["a", "b"], + n.getR() == [], y.getR().map(h, h.single_string) == ["alpha", "beta"], + n.assignR(["a", "b"].map(s, TestAllTypes{single_string:s})).getR().map(h, h.single_string) == ["a", "b"], y.clearR().getR() == []] declare y { - value dev.cel.testing.testdata.proto2.Proto2Message + value cel.expr.conformance.proto2.TestAllTypes } declare n { - value dev.cel.testing.testdata.proto2.Proto2Message + value cel.expr.conformance.proto2.TestAllTypes } declare getI { - function getI dev.cel.testing.testdata.proto2.Proto2Message.() -> int + function getI cel.expr.conformance.proto2.TestAllTypes.() -> int } declare hasI { - function hasI dev.cel.testing.testdata.proto2.Proto2Message.() -> bool + function hasI cel.expr.conformance.proto2.TestAllTypes.() -> bool } declare assignI { - function assignI dev.cel.testing.testdata.proto2.Proto2Message.(int) -> dev.cel.testing.testdata.proto2.Proto2Message + function assignI cel.expr.conformance.proto2.TestAllTypes.(int) -> cel.expr.conformance.proto2.TestAllTypes } declare clearI { - function clearI dev.cel.testing.testdata.proto2.Proto2Message.() -> dev.cel.testing.testdata.proto2.Proto2Message + function clearI cel.expr.conformance.proto2.TestAllTypes.() -> cel.expr.conformance.proto2.TestAllTypes } declare getN { - function getN dev.cel.testing.testdata.proto2.Proto2Message.() -> dev.cel.testing.testdata.proto2.Proto2Message + function getN cel.expr.conformance.proto2.TestAllTypes.() -> cel.expr.conformance.proto2.TestAllTypes } declare hasN { - function hasN dev.cel.testing.testdata.proto2.Proto2Message.() -> bool + function hasN cel.expr.conformance.proto2.TestAllTypes.() -> bool } declare assignN { - function assignN dev.cel.testing.testdata.proto2.Proto2Message.(dev.cel.testing.testdata.proto2.Proto2Message) -> dev.cel.testing.testdata.proto2.Proto2Message + function assignN cel.expr.conformance.proto2.TestAllTypes.(cel.expr.conformance.proto2.TestAllTypes) -> cel.expr.conformance.proto2.TestAllTypes } declare clearN { - function clearN dev.cel.testing.testdata.proto2.Proto2Message.() -> dev.cel.testing.testdata.proto2.Proto2Message + function clearN cel.expr.conformance.proto2.TestAllTypes.() -> cel.expr.conformance.proto2.TestAllTypes } declare getR { - function getR dev.cel.testing.testdata.proto2.Proto2Message.() -> list(dev.cel.testing.testdata.proto2.StringHolder) + function getR cel.expr.conformance.proto2.TestAllTypes.() -> list(cel.expr.conformance.proto2.TestAllTypes) } declare assignR { - function assignR dev.cel.testing.testdata.proto2.Proto2Message.(list(dev.cel.testing.testdata.proto2.StringHolder)) -> dev.cel.testing.testdata.proto2.Proto2Message + function assignR cel.expr.conformance.proto2.TestAllTypes.(list(cel.expr.conformance.proto2.TestAllTypes)) -> cel.expr.conformance.proto2.TestAllTypes } declare clearR { - function clearR dev.cel.testing.testdata.proto2.Proto2Message.() -> dev.cel.testing.testdata.proto2.Proto2Message + function clearR cel.expr.conformance.proto2.TestAllTypes.() -> cel.expr.conformance.proto2.TestAllTypes } =====> bindings: {y=single_int32: 100 -[dev.cel.testing.testdata.proto2.nested_ext] { +[cel.expr.conformance.proto2.int32_ext]: 200 +[cel.expr.conformance.proto2.nested_ext] { single_int32: 50 } -[dev.cel.testing.testdata.proto2.int32_ext]: 200 -[dev.cel.testing.testdata.proto2.repeated_string_holder_ext] { - s: "alpha" +[cel.expr.conformance.proto2.repeated_test_all_types] { + single_string: "alpha" } -[dev.cel.testing.testdata.proto2.repeated_string_holder_ext] { - s: "beta" +[cel.expr.conformance.proto2.repeated_test_all_types] { + single_string: "alpha" } , n=single_int32: 50 } -result: [true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true] +result: [true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, false, true, true] diff --git a/runtime/src/test/resources/fieldManipulation.baseline b/runtime/src/test/resources/fieldManipulation.baseline index acacef93f..bda3bcac6 100644 --- a/runtime/src/test/resources/fieldManipulation.baseline +++ b/runtime/src/test/resources/fieldManipulation.baseline @@ -1,18 +1,18 @@ Source: TestAllTypes{single_bool: true}.assignSingleInt64(1) == TestAllTypes{single_bool: true, single_int64: 1} declare assignSingleInt64 { - function assignSingleInt64 dev.cel.testing.testdata.proto3.TestAllTypes.(int) -> dev.cel.testing.testdata.proto3.TestAllTypes + function assignSingleInt64 cel.expr.conformance.proto3.TestAllTypes.(int) -> cel.expr.conformance.proto3.TestAllTypes } declare assignRepeatedInt64 { - function assignRepeatedInt64 dev.cel.testing.testdata.proto3.TestAllTypes.(list(int)) -> dev.cel.testing.testdata.proto3.TestAllTypes + function assignRepeatedInt64 cel.expr.conformance.proto3.TestAllTypes.(list(int)) -> cel.expr.conformance.proto3.TestAllTypes } declare assignMap { - function assignMap dev.cel.testing.testdata.proto3.TestAllTypes.(map(int, int)) -> dev.cel.testing.testdata.proto3.TestAllTypes + function assignMap cel.expr.conformance.proto3.TestAllTypes.(map(int, int)) -> cel.expr.conformance.proto3.TestAllTypes } declare clearField { - function clearField dev.cel.testing.testdata.proto3.TestAllTypes.(string) -> dev.cel.testing.testdata.proto3.TestAllTypes + function clearField cel.expr.conformance.proto3.TestAllTypes.(string) -> cel.expr.conformance.proto3.TestAllTypes } declare singletonInt64 { - function singletonInt64 (int) -> dev.cel.testing.testdata.proto3.TestAllTypes + function singletonInt64 (int) -> cel.expr.conformance.proto3.TestAllTypes } =====> bindings: {} @@ -20,19 +20,19 @@ result: true Source: TestAllTypes{repeated_int64: [1, 2]}.assignRepeatedInt64([3, 1, 4]) == TestAllTypes{repeated_int64: [3, 1, 4]} declare assignSingleInt64 { - function assignSingleInt64 dev.cel.testing.testdata.proto3.TestAllTypes.(int) -> dev.cel.testing.testdata.proto3.TestAllTypes + function assignSingleInt64 cel.expr.conformance.proto3.TestAllTypes.(int) -> cel.expr.conformance.proto3.TestAllTypes } declare assignRepeatedInt64 { - function assignRepeatedInt64 dev.cel.testing.testdata.proto3.TestAllTypes.(list(int)) -> dev.cel.testing.testdata.proto3.TestAllTypes + function assignRepeatedInt64 cel.expr.conformance.proto3.TestAllTypes.(list(int)) -> cel.expr.conformance.proto3.TestAllTypes } declare assignMap { - function assignMap dev.cel.testing.testdata.proto3.TestAllTypes.(map(int, int)) -> dev.cel.testing.testdata.proto3.TestAllTypes + function assignMap cel.expr.conformance.proto3.TestAllTypes.(map(int, int)) -> cel.expr.conformance.proto3.TestAllTypes } declare clearField { - function clearField dev.cel.testing.testdata.proto3.TestAllTypes.(string) -> dev.cel.testing.testdata.proto3.TestAllTypes + function clearField cel.expr.conformance.proto3.TestAllTypes.(string) -> cel.expr.conformance.proto3.TestAllTypes } declare singletonInt64 { - function singletonInt64 (int) -> dev.cel.testing.testdata.proto3.TestAllTypes + function singletonInt64 (int) -> cel.expr.conformance.proto3.TestAllTypes } =====> bindings: {} @@ -40,19 +40,19 @@ result: true Source: TestAllTypes{single_bool: true, single_int64: 1}.clearField("single_bool") == TestAllTypes{single_int64: 1} declare assignSingleInt64 { - function assignSingleInt64 dev.cel.testing.testdata.proto3.TestAllTypes.(int) -> dev.cel.testing.testdata.proto3.TestAllTypes + function assignSingleInt64 cel.expr.conformance.proto3.TestAllTypes.(int) -> cel.expr.conformance.proto3.TestAllTypes } declare assignRepeatedInt64 { - function assignRepeatedInt64 dev.cel.testing.testdata.proto3.TestAllTypes.(list(int)) -> dev.cel.testing.testdata.proto3.TestAllTypes + function assignRepeatedInt64 cel.expr.conformance.proto3.TestAllTypes.(list(int)) -> cel.expr.conformance.proto3.TestAllTypes } declare assignMap { - function assignMap dev.cel.testing.testdata.proto3.TestAllTypes.(map(int, int)) -> dev.cel.testing.testdata.proto3.TestAllTypes + function assignMap cel.expr.conformance.proto3.TestAllTypes.(map(int, int)) -> cel.expr.conformance.proto3.TestAllTypes } declare clearField { - function clearField dev.cel.testing.testdata.proto3.TestAllTypes.(string) -> dev.cel.testing.testdata.proto3.TestAllTypes + function clearField cel.expr.conformance.proto3.TestAllTypes.(string) -> cel.expr.conformance.proto3.TestAllTypes } declare singletonInt64 { - function singletonInt64 (int) -> dev.cel.testing.testdata.proto3.TestAllTypes + function singletonInt64 (int) -> cel.expr.conformance.proto3.TestAllTypes } =====> bindings: {} @@ -60,19 +60,19 @@ result: true Source: TestAllTypes{single_bool: false}.assignMap({13: 26, 22: 42}).map_int32_int64[22] == 42 declare assignSingleInt64 { - function assignSingleInt64 dev.cel.testing.testdata.proto3.TestAllTypes.(int) -> dev.cel.testing.testdata.proto3.TestAllTypes + function assignSingleInt64 cel.expr.conformance.proto3.TestAllTypes.(int) -> cel.expr.conformance.proto3.TestAllTypes } declare assignRepeatedInt64 { - function assignRepeatedInt64 dev.cel.testing.testdata.proto3.TestAllTypes.(list(int)) -> dev.cel.testing.testdata.proto3.TestAllTypes + function assignRepeatedInt64 cel.expr.conformance.proto3.TestAllTypes.(list(int)) -> cel.expr.conformance.proto3.TestAllTypes } declare assignMap { - function assignMap dev.cel.testing.testdata.proto3.TestAllTypes.(map(int, int)) -> dev.cel.testing.testdata.proto3.TestAllTypes + function assignMap cel.expr.conformance.proto3.TestAllTypes.(map(int, int)) -> cel.expr.conformance.proto3.TestAllTypes } declare clearField { - function clearField dev.cel.testing.testdata.proto3.TestAllTypes.(string) -> dev.cel.testing.testdata.proto3.TestAllTypes + function clearField cel.expr.conformance.proto3.TestAllTypes.(string) -> cel.expr.conformance.proto3.TestAllTypes } declare singletonInt64 { - function singletonInt64 (int) -> dev.cel.testing.testdata.proto3.TestAllTypes + function singletonInt64 (int) -> cel.expr.conformance.proto3.TestAllTypes } =====> bindings: {} @@ -80,19 +80,19 @@ result: true Source: TestAllTypes{single_bool: true, repeated_int64: [1, 2]}.clearField("repeated_int64") == TestAllTypes{single_bool: true} declare assignSingleInt64 { - function assignSingleInt64 dev.cel.testing.testdata.proto3.TestAllTypes.(int) -> dev.cel.testing.testdata.proto3.TestAllTypes + function assignSingleInt64 cel.expr.conformance.proto3.TestAllTypes.(int) -> cel.expr.conformance.proto3.TestAllTypes } declare assignRepeatedInt64 { - function assignRepeatedInt64 dev.cel.testing.testdata.proto3.TestAllTypes.(list(int)) -> dev.cel.testing.testdata.proto3.TestAllTypes + function assignRepeatedInt64 cel.expr.conformance.proto3.TestAllTypes.(list(int)) -> cel.expr.conformance.proto3.TestAllTypes } declare assignMap { - function assignMap dev.cel.testing.testdata.proto3.TestAllTypes.(map(int, int)) -> dev.cel.testing.testdata.proto3.TestAllTypes + function assignMap cel.expr.conformance.proto3.TestAllTypes.(map(int, int)) -> cel.expr.conformance.proto3.TestAllTypes } declare clearField { - function clearField dev.cel.testing.testdata.proto3.TestAllTypes.(string) -> dev.cel.testing.testdata.proto3.TestAllTypes + function clearField cel.expr.conformance.proto3.TestAllTypes.(string) -> cel.expr.conformance.proto3.TestAllTypes } declare singletonInt64 { - function singletonInt64 (int) -> dev.cel.testing.testdata.proto3.TestAllTypes + function singletonInt64 (int) -> cel.expr.conformance.proto3.TestAllTypes } =====> bindings: {} @@ -100,19 +100,19 @@ result: true Source: singletonInt64(12) == TestAllTypes{single_int64: 12} declare assignSingleInt64 { - function assignSingleInt64 dev.cel.testing.testdata.proto3.TestAllTypes.(int) -> dev.cel.testing.testdata.proto3.TestAllTypes + function assignSingleInt64 cel.expr.conformance.proto3.TestAllTypes.(int) -> cel.expr.conformance.proto3.TestAllTypes } declare assignRepeatedInt64 { - function assignRepeatedInt64 dev.cel.testing.testdata.proto3.TestAllTypes.(list(int)) -> dev.cel.testing.testdata.proto3.TestAllTypes + function assignRepeatedInt64 cel.expr.conformance.proto3.TestAllTypes.(list(int)) -> cel.expr.conformance.proto3.TestAllTypes } declare assignMap { - function assignMap dev.cel.testing.testdata.proto3.TestAllTypes.(map(int, int)) -> dev.cel.testing.testdata.proto3.TestAllTypes + function assignMap cel.expr.conformance.proto3.TestAllTypes.(map(int, int)) -> cel.expr.conformance.proto3.TestAllTypes } declare clearField { - function clearField dev.cel.testing.testdata.proto3.TestAllTypes.(string) -> dev.cel.testing.testdata.proto3.TestAllTypes + function clearField cel.expr.conformance.proto3.TestAllTypes.(string) -> cel.expr.conformance.proto3.TestAllTypes } declare singletonInt64 { - function singletonInt64 (int) -> dev.cel.testing.testdata.proto3.TestAllTypes + function singletonInt64 (int) -> cel.expr.conformance.proto3.TestAllTypes } =====> bindings: {} diff --git a/runtime/src/test/resources/has.baseline b/runtime/src/test/resources/has.baseline index 1d14601fc..a98f2474f 100644 --- a/runtime/src/test/resources/has.baseline +++ b/runtime/src/test/resources/has.baseline @@ -1,6 +1,6 @@ Source: has(x.single_int32) && !has(x.single_int64) && has(x.single_bool_wrapper) && has(x.single_int32_wrapper) && !has(x.single_int64_wrapper) && has(x.repeated_int32) && !has(x.repeated_int64) && has(x.optional_bool) && !has(x.optional_string) && has(x.oneof_bool) && !has(x.oneof_type) && has(x.map_int32_int64) && !has(x.map_string_string) && has(x.single_nested_message) && !has(x.single_duration) declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> bindings: {x=single_int32: 1 @@ -13,11 +13,11 @@ map_int32_int64 { key: 1 value: 2 } -oneof_bool: false single_int32_wrapper { value: 42 } single_bool_wrapper { } +oneof_bool: false } result: true diff --git a/runtime/src/test/resources/int64Conversions.baseline b/runtime/src/test/resources/int64Conversions.baseline index 6dcb11ce2..0066c87a1 100644 --- a/runtime/src/test/resources/int64Conversions.baseline +++ b/runtime/src/test/resources/int64Conversions.baseline @@ -8,19 +8,7 @@ Source: int(2.1) bindings: {} result: 2 -Source: int(18446744073709551615u) -=====> -bindings: {} -error: evaluation error: unsigned out of int range -error_code: NUMERIC_OVERFLOW - -Source: int(1e99) -=====> -bindings: {} -error: evaluation error: double is out of range for int -error_code: NUMERIC_OVERFLOW - Source: int(42u) =====> bindings: {} -result: 42 +result: 42 \ No newline at end of file diff --git a/runtime/src/test/resources/int64Conversions_error.baseline b/runtime/src/test/resources/int64Conversions_error.baseline new file mode 100644 index 000000000..2a4bda87f --- /dev/null +++ b/runtime/src/test/resources/int64Conversions_error.baseline @@ -0,0 +1,11 @@ +Source: int(18446744073709551615u) +=====> +bindings: {} +error: evaluation error at test_location:3: unsigned out of int range +error_code: NUMERIC_OVERFLOW + +Source: int(1e99) +=====> +bindings: {} +error: evaluation error at test_location:3: double is out of range for int +error_code: NUMERIC_OVERFLOW \ No newline at end of file diff --git a/runtime/src/test/resources/jsonConversions.baseline b/runtime/src/test/resources/jsonConversions.baseline new file mode 100644 index 000000000..3c674b1c4 --- /dev/null +++ b/runtime/src/test/resources/jsonConversions.baseline @@ -0,0 +1,12 @@ +Source: google.protobuf.Struct { fields: {'timestamp': ts, 'duration': du } } +declare ts { + value google.protobuf.Timestamp +} +declare du { + value google.protobuf.Duration +} +=====> +bindings: {ts=seconds: 100 +, du=nanos: 200000000 +} +result: {timestamp=1970-01-01T00:01:40Z, duration=0.200s} diff --git a/runtime/src/test/resources/jsonValueTypes.baseline b/runtime/src/test/resources/jsonValueTypes.baseline index b6bb53e1f..cc840b24b 100644 --- a/runtime/src/test/resources/jsonValueTypes.baseline +++ b/runtime/src/test/resources/jsonValueTypes.baseline @@ -1,6 +1,6 @@ Source: x.single_value declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> bindings: {x=single_value { @@ -11,7 +11,7 @@ result: true Source: x.single_value == double(1) declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> bindings: {x=single_value { @@ -22,7 +22,7 @@ result: true Source: x.single_value == 1.1 declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> bindings: {x=single_value { @@ -33,7 +33,7 @@ result: true Source: x.single_value == null declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> bindings: {x=single_value { @@ -44,7 +44,7 @@ result: true Source: x.single_value == 'hello' declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> bindings: {x=single_value { @@ -53,9 +53,49 @@ 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 dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> bindings: {x=single_value { @@ -77,7 +117,7 @@ result: true Source: x.single_struct.num == {'str': ['hello'], 'num': -1.1}['num'] declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> bindings: {x=single_struct { @@ -103,7 +143,7 @@ result: true Source: TestAllTypes{single_struct: TestAllTypes{single_value: {'str': ['hello']}}.single_value} declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> bindings: {} @@ -123,7 +163,7 @@ result: single_struct { Source: pair(x.single_struct.str[0], 'val') declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare pair { function pair (string, string) -> dyn @@ -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/lateBoundFunctions.baseline b/runtime/src/test/resources/lateBoundFunctions.baseline new file mode 100644 index 000000000..2ae6bddaa --- /dev/null +++ b/runtime/src/test/resources/lateBoundFunctions.baseline @@ -0,0 +1,7 @@ +Source: record('foo', 'bar') +declare record { + function record_string_dyn (string, dyn) -> dyn +} +=====> +bindings: {} +result: bar diff --git a/runtime/src/test/resources/lists.baseline b/runtime/src/test/resources/lists.baseline index d97984ed2..038594505 100644 --- a/runtime/src/test/resources/lists.baseline +++ b/runtime/src/test/resources/lists.baseline @@ -1,6 +1,6 @@ Source: ([1, 2, 3] + x.repeated_int32)[3] == 4 declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare y { value int @@ -12,7 +12,7 @@ result: true Source: !(y in [1, 2, 3]) && y in [4, 5, 6] declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare y { value int @@ -23,7 +23,7 @@ result: true Source: TestAllTypes{repeated_int32: [1,2]}.repeated_int32[1] == 2 declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare y { value int @@ -34,7 +34,7 @@ result: true Source: 1 in TestAllTypes{repeated_int32: [1,2]}.repeated_int32 declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare y { value int @@ -45,7 +45,7 @@ result: true Source: !(4 in [1, 2, 3]) && 1 in [1, 2, 3] declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare y { value int @@ -56,7 +56,7 @@ result: true Source: !(4 in list) && 1 in list declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare y { value int @@ -70,7 +70,7 @@ result: true Source: !(y in list) declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare y { value int @@ -84,7 +84,7 @@ result: true Source: y in list declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare y { value int @@ -94,19 +94,4 @@ declare list { } =====> bindings: {y=1, list=[1, 2, 3]} -result: true - -Source: list[3] -declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes -} -declare y { - value int -} -declare list { - value list(int) -} -=====> -bindings: {y=1, list=[1, 2, 3]} -error: evaluation error: Index out of bounds: 3 -error_code: INDEX_OUT_OF_BOUNDS +result: true \ No newline at end of file diff --git a/runtime/src/test/resources/lists_error.baseline b/runtime/src/test/resources/lists_error.baseline new file mode 100644 index 000000000..7fd6cedd7 --- /dev/null +++ b/runtime/src/test/resources/lists_error.baseline @@ -0,0 +1,11 @@ +Source: list[3] +declare y { + value int +} +declare list { + value list(int) +} +=====> +bindings: {y=1, list=[1, 2, 3]} +error: evaluation error at test_location:4: Index out of bounds: 3 +error_code: INDEX_OUT_OF_BOUNDS \ No newline at end of file diff --git a/runtime/src/test/resources/longComprehension.baseline b/runtime/src/test/resources/longComprehension.baseline index 66ddeb07e..cb6b64e95 100644 --- a/runtime/src/test/resources/longComprehension.baseline +++ b/runtime/src/test/resources/longComprehension.baseline @@ -7,23 +7,23 @@ bindings: {} result: true Source: size(longlist.map(x, x+1)) == 1000 -declare constantLongList { - function constantLongList () -> list(int) -} declare longlist { value list(int) } +declare constantLongList { + function constantLongList () -> list(int) +} =====> bindings: {longlist=[0, 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, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 311, 312, 313, 314, 315, 316, 317, 318, 319, 320, 321, 322, 323, 324, 325, 326, 327, 328, 329, 330, 331, 332, 333, 334, 335, 336, 337, 338, 339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 349, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377, 378, 379, 380, 381, 382, 383, 384, 385, 386, 387, 388, 389, 390, 391, 392, 393, 394, 395, 396, 397, 398, 399, 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 419, 420, 421, 422, 423, 424, 425, 426, 427, 428, 429, 430, 431, 432, 433, 434, 435, 436, 437, 438, 439, 440, 441, 442, 443, 444, 445, 446, 447, 448, 449, 450, 451, 452, 453, 454, 455, 456, 457, 458, 459, 460, 461, 462, 463, 464, 465, 466, 467, 468, 469, 470, 471, 472, 473, 474, 475, 476, 477, 478, 479, 480, 481, 482, 483, 484, 485, 486, 487, 488, 489, 490, 491, 492, 493, 494, 495, 496, 497, 498, 499, 500, 501, 502, 503, 504, 505, 506, 507, 508, 509, 510, 511, 512, 513, 514, 515, 516, 517, 518, 519, 520, 521, 522, 523, 524, 525, 526, 527, 528, 529, 530, 531, 532, 533, 534, 535, 536, 537, 538, 539, 540, 541, 542, 543, 544, 545, 546, 547, 548, 549, 550, 551, 552, 553, 554, 555, 556, 557, 558, 559, 560, 561, 562, 563, 564, 565, 566, 567, 568, 569, 570, 571, 572, 573, 574, 575, 576, 577, 578, 579, 580, 581, 582, 583, 584, 585, 586, 587, 588, 589, 590, 591, 592, 593, 594, 595, 596, 597, 598, 599, 600, 601, 602, 603, 604, 605, 606, 607, 608, 609, 610, 611, 612, 613, 614, 615, 616, 617, 618, 619, 620, 621, 622, 623, 624, 625, 626, 627, 628, 629, 630, 631, 632, 633, 634, 635, 636, 637, 638, 639, 640, 641, 642, 643, 644, 645, 646, 647, 648, 649, 650, 651, 652, 653, 654, 655, 656, 657, 658, 659, 660, 661, 662, 663, 664, 665, 666, 667, 668, 669, 670, 671, 672, 673, 674, 675, 676, 677, 678, 679, 680, 681, 682, 683, 684, 685, 686, 687, 688, 689, 690, 691, 692, 693, 694, 695, 696, 697, 698, 699, 700, 701, 702, 703, 704, 705, 706, 707, 708, 709, 710, 711, 712, 713, 714, 715, 716, 717, 718, 719, 720, 721, 722, 723, 724, 725, 726, 727, 728, 729, 730, 731, 732, 733, 734, 735, 736, 737, 738, 739, 740, 741, 742, 743, 744, 745, 746, 747, 748, 749, 750, 751, 752, 753, 754, 755, 756, 757, 758, 759, 760, 761, 762, 763, 764, 765, 766, 767, 768, 769, 770, 771, 772, 773, 774, 775, 776, 777, 778, 779, 780, 781, 782, 783, 784, 785, 786, 787, 788, 789, 790, 791, 792, 793, 794, 795, 796, 797, 798, 799, 800, 801, 802, 803, 804, 805, 806, 807, 808, 809, 810, 811, 812, 813, 814, 815, 816, 817, 818, 819, 820, 821, 822, 823, 824, 825, 826, 827, 828, 829, 830, 831, 832, 833, 834, 835, 836, 837, 838, 839, 840, 841, 842, 843, 844, 845, 846, 847, 848, 849, 850, 851, 852, 853, 854, 855, 856, 857, 858, 859, 860, 861, 862, 863, 864, 865, 866, 867, 868, 869, 870, 871, 872, 873, 874, 875, 876, 877, 878, 879, 880, 881, 882, 883, 884, 885, 886, 887, 888, 889, 890, 891, 892, 893, 894, 895, 896, 897, 898, 899, 900, 901, 902, 903, 904, 905, 906, 907, 908, 909, 910, 911, 912, 913, 914, 915, 916, 917, 918, 919, 920, 921, 922, 923, 924, 925, 926, 927, 928, 929, 930, 931, 932, 933, 934, 935, 936, 937, 938, 939, 940, 941, 942, 943, 944, 945, 946, 947, 948, 949, 950, 951, 952, 953, 954, 955, 956, 957, 958, 959, 960, 961, 962, 963, 964, 965, 966, 967, 968, 969, 970, 971, 972, 973, 974, 975, 976, 977, 978, 979, 980, 981, 982, 983, 984, 985, 986, 987, 988, 989, 990, 991, 992, 993, 994, 995, 996, 997, 998, 999]} result: true Source: f_unleash(longlist.map(x, f_slow_inc(x)))[0] == 1 -declare constantLongList { - function constantLongList () -> list(int) -} declare longlist { value list(int) } +declare constantLongList { + function constantLongList () -> list(int) +} declare f_slow_inc { function f_slow_inc (int) -> int } diff --git a/runtime/src/test/resources/maps.baseline b/runtime/src/test/resources/maps.baseline index da0f857ad..ec89b09c6 100644 --- a/runtime/src/test/resources/maps.baseline +++ b/runtime/src/test/resources/maps.baseline @@ -1,6 +1,6 @@ Source: {1: 2, 3: 4}[3] == 4 declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> bindings: {} @@ -8,7 +8,7 @@ result: true Source: 3 in {1: 2, 3: 4} && !(4 in {1: 2, 3: 4}) declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> bindings: {} @@ -16,7 +16,7 @@ result: true Source: x.map_int32_int64[22] == 23 declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> bindings: {x=map_int32_int64 { @@ -28,7 +28,7 @@ result: true Source: TestAllTypes{map_int32_int64: {21: 22, 22: 23}}.map_int32_int64[22] == 23 declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> bindings: {} @@ -36,7 +36,7 @@ result: true Source: TestAllTypes{oneof_type: NestedTestAllTypes{payload: x}}.oneof_type.payload.map_int32_int64[22] == 23 declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> bindings: {x=map_int32_int64 { @@ -48,7 +48,7 @@ result: true Source: !(4 in map) && 1 in map declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare y { value int @@ -62,7 +62,7 @@ result: true Source: !(y in {1: 4, 2: 3, 3: 2}) && y in {5: 3, 4: 2, 3: 3} declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare y { value int @@ -76,7 +76,7 @@ result: true Source: !(y in map) && (y + 3) in map declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare y { value int @@ -90,7 +90,7 @@ result: true Source: TestAllTypes{map_int64_nested_type:{42:NestedTestAllTypes{payload:TestAllTypes{}}}} declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare y { value int @@ -111,7 +111,7 @@ result: map_int64_nested_type { Source: {true: 1, false: 2, true: 3}[true] declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare y { value int @@ -121,12 +121,12 @@ declare map { } =====> bindings: {} -error: evaluation error at test_location:24: duplicate map key [true] +error: evaluation error at test_location:20: duplicate map key [true] error_code: DUPLICATE_ATTRIBUTE Source: {b: 1, !b: 2, b: 3}[true] declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare y { value int @@ -139,5 +139,5 @@ declare b { } =====> bindings: {b=true} -error: evaluation error at test_location:15: duplicate map key [true] +error: evaluation error at test_location:14: duplicate map key [true] error_code: DUPLICATE_ATTRIBUTE diff --git a/runtime/src/test/resources/maxComprehension.baseline b/runtime/src/test/resources/maxComprehension.baseline index fc0cc7555..cad955d0f 100644 --- a/runtime/src/test/resources/maxComprehension.baseline +++ b/runtime/src/test/resources/maxComprehension.baseline @@ -4,7 +4,7 @@ declare longlist { } =====> bindings: {longlist=[0, 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, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 311, 312, 313, 314, 315, 316, 317, 318, 319, 320, 321, 322, 323, 324, 325, 326, 327, 328, 329, 330, 331, 332, 333, 334, 335, 336, 337, 338, 339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 349, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377, 378, 379, 380, 381, 382, 383, 384, 385, 386, 387, 388, 389, 390, 391, 392, 393, 394, 395, 396, 397, 398, 399, 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 419, 420, 421, 422, 423, 424, 425, 426, 427, 428, 429, 430, 431, 432, 433, 434, 435, 436, 437, 438, 439, 440, 441, 442, 443, 444, 445, 446, 447, 448, 449, 450, 451, 452, 453, 454, 455, 456, 457, 458, 459, 460, 461, 462, 463, 464, 465, 466, 467, 468, 469, 470, 471, 472, 473, 474, 475, 476, 477, 478, 479, 480, 481, 482, 483, 484, 485, 486, 487, 488, 489, 490, 491, 492, 493, 494, 495, 496, 497, 498, 499, 500, 501, 502, 503, 504, 505, 506, 507, 508, 509, 510, 511, 512, 513, 514, 515, 516, 517, 518, 519, 520, 521, 522, 523, 524, 525, 526, 527, 528, 529, 530, 531, 532, 533, 534, 535, 536, 537, 538, 539, 540, 541, 542, 543, 544, 545, 546, 547, 548, 549, 550, 551, 552, 553, 554, 555, 556, 557, 558, 559, 560, 561, 562, 563, 564, 565, 566, 567, 568, 569, 570, 571, 572, 573, 574, 575, 576, 577, 578, 579, 580, 581, 582, 583, 584, 585, 586, 587, 588, 589, 590, 591, 592, 593, 594, 595, 596, 597, 598, 599, 600, 601, 602, 603, 604, 605, 606, 607, 608, 609, 610, 611, 612, 613, 614, 615, 616, 617, 618, 619, 620, 621, 622, 623, 624, 625, 626, 627, 628, 629, 630, 631, 632, 633, 634, 635, 636, 637, 638, 639, 640, 641, 642, 643, 644, 645, 646, 647, 648, 649, 650, 651, 652, 653, 654, 655, 656, 657, 658, 659, 660, 661, 662, 663, 664, 665, 666, 667, 668, 669, 670, 671, 672, 673, 674, 675, 676, 677, 678, 679, 680, 681, 682, 683, 684, 685, 686, 687, 688, 689, 690, 691, 692, 693, 694, 695, 696, 697, 698, 699, 700, 701, 702, 703, 704, 705, 706, 707, 708, 709, 710, 711, 712, 713, 714, 715, 716, 717, 718, 719, 720, 721, 722, 723, 724, 725, 726, 727, 728, 729, 730, 731, 732, 733, 734, 735, 736, 737, 738, 739, 740, 741, 742, 743, 744, 745, 746, 747, 748, 749, 750, 751, 752, 753, 754, 755, 756, 757, 758, 759, 760, 761, 762, 763, 764, 765, 766, 767, 768, 769, 770, 771, 772, 773, 774, 775, 776, 777, 778, 779, 780, 781, 782, 783, 784, 785, 786, 787, 788, 789, 790, 791, 792, 793, 794, 795, 796, 797, 798, 799, 800, 801, 802, 803, 804, 805, 806, 807, 808, 809, 810, 811, 812, 813, 814, 815, 816, 817, 818, 819, 820, 821, 822, 823, 824, 825, 826, 827, 828, 829, 830, 831, 832, 833, 834, 835, 836, 837, 838, 839, 840, 841, 842, 843, 844, 845, 846, 847, 848, 849, 850, 851, 852, 853, 854, 855, 856, 857, 858, 859, 860, 861, 862, 863, 864, 865, 866, 867, 868, 869, 870, 871, 872, 873, 874, 875, 876, 877, 878, 879, 880, 881, 882, 883, 884, 885, 886, 887, 888, 889, 890, 891, 892, 893, 894, 895, 896, 897, 898, 899, 900, 901, 902, 903, 904, 905, 906, 907, 908, 909, 910, 911, 912, 913, 914, 915, 916, 917, 918, 919, 920, 921, 922, 923, 924, 925, 926, 927, 928, 929, 930, 931, 932, 933, 934, 935, 936, 937, 938, 939, 940, 941, 942, 943, 944, 945, 946, 947, 948, 949, 950, 951, 952, 953, 954, 955, 956, 957, 958, 959, 960, 961, 962, 963, 964, 965, 966, 967, 968, 969, 970, 971, 972, 973, 974, 975, 976, 977, 978, 979, 980, 981, 982, 983, 984, 985, 986, 987, 988, 989, 990, 991, 992, 993, 994, 995, 996, 997, 998, 999, 1000]} -error: evaluation error: Iteration budget exceeded: 1000 +error: evaluation error at test_location:17: Iteration budget exceeded: 1000 error_code: ITERATION_BUDGET_EXCEEDED Source: longlist.filter(i, i % 2 == 0).map(i, i * 2).map(i, i / 2).size() == 250 @@ -21,7 +21,7 @@ declare longlist { } =====> bindings: {longlist=[0, 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, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 311, 312, 313, 314, 315, 316, 317, 318, 319, 320, 321, 322, 323, 324, 325, 326, 327, 328, 329, 330, 331, 332, 333, 334, 335, 336, 337, 338, 339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 349, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377, 378, 379, 380, 381, 382, 383, 384, 385, 386, 387, 388, 389, 390, 391, 392, 393, 394, 395, 396, 397, 398, 399, 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 419, 420, 421, 422, 423, 424, 425, 426, 427, 428, 429, 430, 431, 432, 433, 434, 435, 436, 437, 438, 439, 440, 441, 442, 443, 444, 445, 446, 447, 448, 449, 450, 451, 452, 453, 454, 455, 456, 457, 458, 459, 460, 461, 462, 463, 464, 465, 466, 467, 468, 469, 470, 471, 472, 473, 474, 475, 476, 477, 478, 479, 480, 481, 482, 483, 484, 485, 486, 487, 488, 489, 490, 491, 492, 493, 494, 495, 496, 497, 498, 499]} -error: evaluation error: Iteration budget exceeded: 1000 +error: evaluation error at test_location:56: Iteration budget exceeded: 1000 error_code: ITERATION_BUDGET_EXCEEDED Source: longlist.map(i, longlist.map(j, longlist.map(k, [i, j, k]))).size() == 9 @@ -38,5 +38,5 @@ declare longlist { } =====> bindings: {longlist=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]} -error: evaluation error: Iteration budget exceeded: 1000 +error: evaluation error at test_location:28: Iteration budget exceeded: 1000 error_code: ITERATION_BUDGET_EXCEEDED diff --git a/runtime/src/test/resources/messages.baseline b/runtime/src/test/resources/messages.baseline index df7a6579d..f870eb0c3 100644 --- a/runtime/src/test/resources/messages.baseline +++ b/runtime/src/test/resources/messages.baseline @@ -1,6 +1,6 @@ Source: x.single_nested_message.bb == 43 && has(x.single_nested_message) declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> bindings: {x=single_nested_message { @@ -11,10 +11,10 @@ result: true Source: single_nested_message.bb == 43 declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare single_nested_message { - value dev.cel.testing.testdata.proto3.TestAllTypes.NestedMessage + value cel.expr.conformance.proto3.TestAllTypes.NestedMessage } =====> bindings: {single_nested_message=bb: 43 @@ -23,10 +23,10 @@ result: true Source: TestAllTypes{single_int64: 1, single_sfixed64: 2, single_int32: 2}.single_int32 == 2 declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare single_nested_message { - value dev.cel.testing.testdata.proto3.TestAllTypes.NestedMessage + value cel.expr.conformance.proto3.TestAllTypes.NestedMessage } =====> bindings: {} diff --git a/runtime/src/test/resources/namespacedVariables.baseline b/runtime/src/test/resources/namespacedVariables.baseline index 1013e1b66..9c0db8f0b 100644 --- a/runtime/src/test/resources/namespacedVariables.baseline +++ b/runtime/src/test/resources/namespacedVariables.baseline @@ -11,7 +11,7 @@ declare ns.x { value int } declare dev.cel.testing.testdata.proto3.msgVar { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> bindings: {dev.cel.testing.testdata.proto3.msgVar=single_int32: 5 diff --git a/runtime/src/test/resources/nestedEnums.baseline b/runtime/src/test/resources/nestedEnums.baseline index 6dfee68f2..e7e2199bf 100644 --- a/runtime/src/test/resources/nestedEnums.baseline +++ b/runtime/src/test/resources/nestedEnums.baseline @@ -1,6 +1,6 @@ Source: x.single_nested_enum == TestAllTypes.NestedEnum.BAR declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> bindings: {x=single_nested_enum: BAR @@ -9,7 +9,7 @@ result: true Source: single_nested_enum == TestAllTypes.NestedEnum.BAR declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare single_nested_enum { value int @@ -20,7 +20,7 @@ result: true Source: TestAllTypes{single_nested_enum : TestAllTypes.NestedEnum.BAR}.single_nested_enum == 1 declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare single_nested_enum { value int diff --git a/runtime/src/test/resources/nonstrictQuantifierTests.baseline b/runtime/src/test/resources/nonstrictQuantifierTests.baseline index 3a7a05b2b..728e4bfc9 100644 --- a/runtime/src/test/resources/nonstrictQuantifierTests.baseline +++ b/runtime/src/test/resources/nonstrictQuantifierTests.baseline @@ -38,4 +38,12 @@ declare four { } =====> bindings: {four=4} +result: true + +Source: [0, 1].exists(x, x > four || true) +declare four { + value int +} +=====> +bindings: {} result: true \ 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/optional.baseline b/runtime/src/test/resources/optional.baseline new file mode 100644 index 000000000..1e32c2696 --- /dev/null +++ b/runtime/src/test/resources/optional.baseline @@ -0,0 +1,12 @@ +Source: optional.unwrap([]) +=====> +bindings: {} +result: [] + +Source: optional.unwrap([optional.none(), optional.of(1), optional.of(str)]) +declare str { + value string +} +=====> +bindings: {str=foo} +result: [1, foo] diff --git a/runtime/src/test/resources/optional_errors.baseline b/runtime/src/test/resources/optional_errors.baseline new file mode 100644 index 000000000..18b2846fe --- /dev/null +++ b/runtime/src/test/resources/optional_errors.baseline @@ -0,0 +1,5 @@ +Source: optional.unwrap([dyn(1)]) +=====> +bindings: {} +error: evaluation error at test_location:15: Function 'optional_unwrap_list' failed with arg(s) '[1]' +error_code: INTERNAL_ERROR \ No newline at end of file diff --git a/runtime/src/test/resources/packUnpackAny.baseline b/runtime/src/test/resources/packUnpackAny.baseline index 44466f853..0ebfa02ae 100644 --- a/runtime/src/test/resources/packUnpackAny.baseline +++ b/runtime/src/test/resources/packUnpackAny.baseline @@ -6,7 +6,10 @@ declare d { value google.protobuf.Duration } declare message { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes +} +declare list { + value list(dyn) } =====> bindings: {d=seconds: 100 @@ -23,7 +26,10 @@ declare d { value google.protobuf.Duration } declare message { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes +} +declare list { + value list(dyn) } =====> bindings: {message=single_any { @@ -43,7 +49,10 @@ declare d { value google.protobuf.Duration } declare message { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes +} +declare list { + value list(dyn) } =====> bindings: {message=single_any { @@ -62,7 +71,10 @@ declare d { value google.protobuf.Duration } declare message { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes +} +declare list { + value list(dyn) } =====> bindings: {any=single_int64: 1 @@ -77,7 +89,10 @@ declare d { value google.protobuf.Duration } declare message { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes +} +declare list { + value list(dyn) } =====> bindings: {any=type_url: "type.googleapis.com/google.protobuf.Int64Value" @@ -85,6 +100,29 @@ value: "\b\001" } result: true +Source: list[0] == message +declare any { + value any +} +declare d { + value google.protobuf.Duration +} +declare message { + value cel.expr.conformance.proto3.TestAllTypes +} +declare list { + value list(dyn) +} +=====> +bindings: {list=[type_url: "type.googleapis.com/cel.expr.conformance.proto3.TestAllTypes" +value: "\242\0062\n,type.googleapis.com/google.protobuf.Duration\022\002\bd" +], message=single_any { + type_url: "type.googleapis.com/google.protobuf.Duration" + value: "\bd" +} +} +result: true + Source: TestAllTypes{single_any: d} declare any { value any @@ -93,7 +131,10 @@ declare d { value google.protobuf.Duration } declare message { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes +} +declare list { + value list(dyn) } =====> bindings: {d=seconds: 100 @@ -112,7 +153,10 @@ declare d { value google.protobuf.Duration } declare message { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes +} +declare list { + value list(dyn) } =====> bindings: {message=single_int64: -1 @@ -131,7 +175,10 @@ declare d { value google.protobuf.Duration } declare message { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes +} +declare list { + value list(dyn) } =====> bindings: {message=single_uint64: 1 @@ -150,7 +197,10 @@ declare d { value google.protobuf.Duration } declare message { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes +} +declare list { + value list(dyn) } =====> bindings: {} @@ -168,7 +218,10 @@ declare d { value google.protobuf.Duration } declare message { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes +} +declare list { + value list(dyn) } =====> bindings: {} @@ -186,7 +239,10 @@ declare d { value google.protobuf.Duration } declare message { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes +} +declare list { + value list(dyn) } =====> bindings: {} @@ -204,7 +260,10 @@ declare d { value google.protobuf.Duration } declare message { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes +} +declare list { + value list(dyn) } =====> bindings: {message=single_bytes: "happy" 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/regexpMatches_error.baseline b/runtime/src/test/resources/regexpMatches_error.baseline index a9d3174ef..38ff64033 100644 --- a/runtime/src/test/resources/regexpMatches_error.baseline +++ b/runtime/src/test/resources/regexpMatches_error.baseline @@ -1,11 +1,11 @@ Source: matches("alpha", "**") =====> bindings: {} -error: evaluation error: error parsing regexp: missing argument to repetition operator: `*` +error: evaluation error at test_location:7: error parsing regexp: missing argument to repetition operator: `*` error_code: INVALID_ARGUMENT Source: "alpha".matches("**") =====> bindings: {} -error: evaluation error: error parsing regexp: missing argument to repetition operator: `*` -error_code: INVALID_ARGUMENT +error: evaluation error at test_location:15: error parsing regexp: missing argument to repetition operator: `*` +error_code: INVALID_ARGUMENT \ No newline at end of file diff --git a/runtime/src/test/resources/stringConversions.baseline b/runtime/src/test/resources/stringConversions.baseline index 7e20cff19..9dec559ae 100644 --- a/runtime/src/test/resources/stringConversions.baseline +++ b/runtime/src/test/resources/stringConversions.baseline @@ -13,6 +13,11 @@ Source: string(-1) bindings: {} result: -1 +Source: string(true) +=====> +bindings: {} +result: true + Source: string(b'abc\303\203') =====> bindings: {} @@ -32,3 +37,8 @@ Source: string(duration('1000000s')) =====> bindings: {} result: 1000000s + +Source: string('hello') +=====> +bindings: {} +result: hello \ No newline at end of file diff --git a/runtime/src/test/resources/stringConversions_error.baseline b/runtime/src/test/resources/stringConversions_error.baseline new file mode 100644 index 000000000..174a70b91 --- /dev/null +++ b/runtime/src/test/resources/stringConversions_error.baseline @@ -0,0 +1,5 @@ +Source: string(b'\xff') +=====> +bindings: {} +error: evaluation error at test_location:6: invalid UTF-8 in bytes, cannot convert to string +error_code: BAD_FORMAT diff --git a/runtime/src/test/resources/timeConversions.baseline b/runtime/src/test/resources/timeConversions.baseline index 18b8fdcb0..12a521441 100644 --- a/runtime/src/test/resources/timeConversions.baseline +++ b/runtime/src/test/resources/timeConversions.baseline @@ -4,9 +4,7 @@ declare t1 { } =====> bindings: {} -result: seconds: 63126020 -nanos: 21000000 - +result: 1972-01-01T15:00:20.021Z Source: timestamp(123) declare t1 { @@ -14,8 +12,7 @@ declare t1 { } =====> bindings: {} -result: seconds: 123 - +result: 1970-01-01T00:02:03Z Source: duration("15.11s") declare t1 { @@ -23,17 +20,14 @@ declare t1 { } =====> bindings: {} -result: seconds: 15 -nanos: 110000000 - +result: PT15.11S Source: int(t1) == 100 declare t1 { value google.protobuf.Timestamp } =====> -bindings: {t1=seconds: 100 -} +bindings: {t1=1970-01-01T00:01:40Z} result: true Source: duration("1h2m3.4s") @@ -42,15 +36,20 @@ declare t1 { } =====> bindings: {} -result: seconds: 3723 -nanos: 400000000 +result: PT1H2M3.4S +Source: duration(duration('15.0s')) +declare t1 { + value google.protobuf.Timestamp +} +=====> +bindings: {} +result: PT15S -Source: duration('inf') +Source: timestamp(timestamp(123)) declare t1 { value google.protobuf.Timestamp } =====> bindings: {} -error: evaluation error: invalid duration format -error_code: BAD_FORMAT +result: 1970-01-01T00:02:03Z \ No newline at end of file diff --git a/runtime/src/test/resources/timeConversions_error.baseline b/runtime/src/test/resources/timeConversions_error.baseline new file mode 100644 index 000000000..3b331a3d8 --- /dev/null +++ b/runtime/src/test/resources/timeConversions_error.baseline @@ -0,0 +1,5 @@ +Source: duration('inf') +=====> +bindings: {} +error: evaluation error at test_location:8: invalid duration format +error_code: BAD_FORMAT \ No newline at end of file diff --git a/runtime/src/test/resources/timestamp.baseline b/runtime/src/test/resources/timestamp.baseline index c215b581f..828e7f912 100644 --- a/runtime/src/test/resources/timestamp.baseline +++ b/runtime/src/test/resources/timestamp.baseline @@ -6,11 +6,7 @@ declare t2 { value google.protobuf.Timestamp } =====> -bindings: {t2=seconds: 10 -nanos: 9 -} +> {t1=seconds: 10 -nanos: 10 -} +bindings: {t2=1970-01-01T00:00:10.000000009Z} +> {t1=1970-01-01T00:00:10.000000010Z} result: false Source: t1 < t2 @@ -21,11 +17,7 @@ declare t2 { value google.protobuf.Timestamp } =====> -bindings: {t2=seconds: 9 -nanos: 10 -} +> {t1=seconds: 10 -nanos: 10 -} +bindings: {t2=1970-01-01T00:00:09.000000010Z} +> {t1=1970-01-01T00:00:10.000000010Z} result: false Source: t1 < t2 @@ -36,11 +28,7 @@ declare t2 { value google.protobuf.Timestamp } =====> -bindings: {t2=seconds: 10 -nanos: 10 -} +> {t1=seconds: 10 -nanos: 10 -} +bindings: {t2=1970-01-01T00:00:10.000000010Z} +> {t1=1970-01-01T00:00:10.000000010Z} result: false Source: t1 < t2 @@ -51,11 +39,7 @@ declare t2 { value google.protobuf.Timestamp } =====> -bindings: {t2=seconds: 10 -nanos: 10 -} +> {t1=seconds: 10 -nanos: 9 -} +bindings: {t2=1970-01-01T00:00:10.000000010Z} +> {t1=1970-01-01T00:00:10.000000009Z} result: true Source: t1 < t2 @@ -66,11 +50,7 @@ declare t2 { value google.protobuf.Timestamp } =====> -bindings: {t2=seconds: 10 -nanos: 10 -} +> {t1=seconds: 9 -nanos: 10 -} +bindings: {t2=1970-01-01T00:00:10.000000010Z} +> {t1=1970-01-01T00:00:09.000000010Z} result: true Source: t1 <= t2 @@ -81,11 +61,7 @@ declare t2 { value google.protobuf.Timestamp } =====> -bindings: {t2=seconds: 10 -nanos: 9 -} +> {t1=seconds: 10 -nanos: 10 -} +bindings: {t2=1970-01-01T00:00:10.000000009Z} +> {t1=1970-01-01T00:00:10.000000010Z} result: false Source: t1 <= t2 @@ -96,11 +72,7 @@ declare t2 { value google.protobuf.Timestamp } =====> -bindings: {t2=seconds: 9 -nanos: 10 -} +> {t1=seconds: 10 -nanos: 10 -} +bindings: {t2=1970-01-01T00:00:09.000000010Z} +> {t1=1970-01-01T00:00:10.000000010Z} result: false Source: t1 <= t2 @@ -111,11 +83,7 @@ declare t2 { value google.protobuf.Timestamp } =====> -bindings: {t2=seconds: 10 -nanos: 10 -} +> {t1=seconds: 10 -nanos: 10 -} +bindings: {t2=1970-01-01T00:00:10.000000010Z} +> {t1=1970-01-01T00:00:10.000000010Z} result: true Source: t1 <= t2 @@ -126,11 +94,7 @@ declare t2 { value google.protobuf.Timestamp } =====> -bindings: {t2=seconds: 10 -nanos: 10 -} +> {t1=seconds: 10 -nanos: 9 -} +bindings: {t2=1970-01-01T00:00:10.000000010Z} +> {t1=1970-01-01T00:00:10.000000009Z} result: true Source: t1 <= t2 @@ -141,11 +105,7 @@ declare t2 { value google.protobuf.Timestamp } =====> -bindings: {t2=seconds: 10 -nanos: 10 -} +> {t1=seconds: 9 -nanos: 10 -} +bindings: {t2=1970-01-01T00:00:10.000000010Z} +> {t1=1970-01-01T00:00:09.000000010Z} result: true Source: t1 > t2 @@ -156,11 +116,7 @@ declare t2 { value google.protobuf.Timestamp } =====> -bindings: {t2=seconds: 10 -nanos: 9 -} +> {t1=seconds: 10 -nanos: 10 -} +bindings: {t2=1970-01-01T00:00:10.000000009Z} +> {t1=1970-01-01T00:00:10.000000010Z} result: true Source: t1 > t2 @@ -171,11 +127,7 @@ declare t2 { value google.protobuf.Timestamp } =====> -bindings: {t2=seconds: 9 -nanos: 10 -} +> {t1=seconds: 10 -nanos: 10 -} +bindings: {t2=1970-01-01T00:00:09.000000010Z} +> {t1=1970-01-01T00:00:10.000000010Z} result: true Source: t1 > t2 @@ -186,11 +138,7 @@ declare t2 { value google.protobuf.Timestamp } =====> -bindings: {t2=seconds: 10 -nanos: 10 -} +> {t1=seconds: 10 -nanos: 10 -} +bindings: {t2=1970-01-01T00:00:10.000000010Z} +> {t1=1970-01-01T00:00:10.000000010Z} result: false Source: t1 > t2 @@ -201,11 +149,7 @@ declare t2 { value google.protobuf.Timestamp } =====> -bindings: {t2=seconds: 10 -nanos: 10 -} +> {t1=seconds: 10 -nanos: 9 -} +bindings: {t2=1970-01-01T00:00:10.000000010Z} +> {t1=1970-01-01T00:00:10.000000009Z} result: false Source: t1 > t2 @@ -216,11 +160,7 @@ declare t2 { value google.protobuf.Timestamp } =====> -bindings: {t2=seconds: 10 -nanos: 10 -} +> {t1=seconds: 9 -nanos: 10 -} +bindings: {t2=1970-01-01T00:00:10.000000010Z} +> {t1=1970-01-01T00:00:09.000000010Z} result: false Source: t1 >= t2 @@ -231,11 +171,7 @@ declare t2 { value google.protobuf.Timestamp } =====> -bindings: {t2=seconds: 10 -nanos: 9 -} +> {t1=seconds: 10 -nanos: 10 -} +bindings: {t2=1970-01-01T00:00:10.000000009Z} +> {t1=1970-01-01T00:00:10.000000010Z} result: true Source: t1 >= t2 @@ -246,11 +182,7 @@ declare t2 { value google.protobuf.Timestamp } =====> -bindings: {t2=seconds: 9 -nanos: 10 -} +> {t1=seconds: 10 -nanos: 10 -} +bindings: {t2=1970-01-01T00:00:09.000000010Z} +> {t1=1970-01-01T00:00:10.000000010Z} result: true Source: t1 >= t2 @@ -261,11 +193,7 @@ declare t2 { value google.protobuf.Timestamp } =====> -bindings: {t2=seconds: 10 -nanos: 10 -} +> {t1=seconds: 10 -nanos: 10 -} +bindings: {t2=1970-01-01T00:00:10.000000010Z} +> {t1=1970-01-01T00:00:10.000000010Z} result: true Source: t1 >= t2 @@ -276,11 +204,7 @@ declare t2 { value google.protobuf.Timestamp } =====> -bindings: {t2=seconds: 10 -nanos: 10 -} +> {t1=seconds: 10 -nanos: 9 -} +bindings: {t2=1970-01-01T00:00:10.000000010Z} +> {t1=1970-01-01T00:00:10.000000009Z} result: false Source: t1 >= t2 @@ -291,9 +215,5 @@ declare t2 { value google.protobuf.Timestamp } =====> -bindings: {t2=seconds: 10 -nanos: 10 -} +> {t1=seconds: 9 -nanos: 10 -} -result: false +bindings: {t2=1970-01-01T00:00:10.000000010Z} +> {t1=1970-01-01T00:00:09.000000010Z} +result: false \ No newline at end of file diff --git a/runtime/src/test/resources/timestampFunctions.baseline b/runtime/src/test/resources/timestampFunctions.baseline index 322137892..e932867e8 100644 --- a/runtime/src/test/resources/timestampFunctions.baseline +++ b/runtime/src/test/resources/timestampFunctions.baseline @@ -3,9 +3,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: 1 -nanos: 11000000 -} +bindings: {ts1=1970-01-01T00:00:01.011Z} result: 1969 Source: ts1.getFullYear() @@ -13,9 +11,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: 1 -nanos: 11000000 -} +bindings: {ts1=1970-01-01T00:00:01.011Z} result: 1970 Source: ts1.getFullYear() @@ -23,8 +19,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: -1 -} +bindings: {ts1=1969-12-31T23:59:59Z} result: 1969 Source: ts1.getFullYear("Indian/Cocos") @@ -32,9 +27,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: 1 -nanos: 11000000 -} +bindings: {ts1=1970-01-01T00:00:01.011Z} result: 1970 Source: ts1.getFullYear("2:00") @@ -42,9 +35,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: 1 -nanos: 11000000 -} +bindings: {ts1=1970-01-01T00:00:01.011Z} result: 1970 Source: ts1.getMonth("America/Los_Angeles") @@ -52,9 +43,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: 1 -nanos: 11000000 -} +bindings: {ts1=1970-01-01T00:00:01.011Z} result: 11 Source: ts1.getMonth() @@ -62,9 +51,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: 1 -nanos: 11000000 -} +bindings: {ts1=1970-01-01T00:00:01.011Z} result: 0 Source: ts1.getMonth() @@ -72,8 +59,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: -1 -} +bindings: {ts1=1969-12-31T23:59:59Z} result: 11 Source: ts1.getMonth("Indian/Cocos") @@ -81,9 +67,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: 1 -nanos: 11000000 -} +bindings: {ts1=1970-01-01T00:00:01.011Z} result: 0 Source: ts1.getMonth("-8:15") @@ -91,9 +75,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: 1 -nanos: 11000000 -} +bindings: {ts1=1970-01-01T00:00:01.011Z} result: 11 Source: ts1.getDayOfYear("America/Los_Angeles") @@ -101,9 +83,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: 1 -nanos: 11000000 -} +bindings: {ts1=1970-01-01T00:00:01.011Z} result: 364 Source: ts1.getDayOfYear() @@ -111,9 +91,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: 1 -nanos: 11000000 -} +bindings: {ts1=1970-01-01T00:00:01.011Z} result: 0 Source: ts1.getDayOfYear() @@ -121,8 +99,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: -1 -} +bindings: {ts1=1969-12-31T23:59:59Z} result: 364 Source: ts1.getDayOfYear("Indian/Cocos") @@ -130,9 +107,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: 1 -nanos: 11000000 -} +bindings: {ts1=1970-01-01T00:00:01.011Z} result: 0 Source: ts1.getDayOfYear("-9:00") @@ -140,9 +115,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: 1 -nanos: 11000000 -} +bindings: {ts1=1970-01-01T00:00:01.011Z} result: 364 Source: ts1.getDayOfMonth("America/Los_Angeles") @@ -150,9 +123,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: 1 -nanos: 11000000 -} +bindings: {ts1=1970-01-01T00:00:01.011Z} result: 30 Source: ts1.getDayOfMonth() @@ -160,9 +131,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: 1 -nanos: 11000000 -} +bindings: {ts1=1970-01-01T00:00:01.011Z} result: 0 Source: ts1.getDayOfMonth() @@ -170,8 +139,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: -1 -} +bindings: {ts1=1969-12-31T23:59:59Z} result: 30 Source: ts1.getDayOfMonth("Indian/Cocos") @@ -179,9 +147,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: 1 -nanos: 11000000 -} +bindings: {ts1=1970-01-01T00:00:01.011Z} result: 0 Source: ts1.getDayOfMonth("8:00") @@ -189,9 +155,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: 1 -nanos: 11000000 -} +bindings: {ts1=1970-01-01T00:00:01.011Z} result: 0 Source: ts1.getDate("America/Los_Angeles") @@ -199,9 +163,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: 1 -nanos: 11000000 -} +bindings: {ts1=1970-01-01T00:00:01.011Z} result: 31 Source: ts1.getDate() @@ -209,9 +171,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: 1 -nanos: 11000000 -} +bindings: {ts1=1970-01-01T00:00:01.011Z} result: 1 Source: ts1.getDate() @@ -219,8 +179,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: -1 -} +bindings: {ts1=1969-12-31T23:59:59Z} result: 31 Source: ts1.getDate("Indian/Cocos") @@ -228,9 +187,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: 1 -nanos: 11000000 -} +bindings: {ts1=1970-01-01T00:00:01.011Z} result: 1 Source: ts1.getDate("9:30") @@ -238,9 +195,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: 1 -nanos: 11000000 -} +bindings: {ts1=1970-01-01T00:00:01.011Z} result: 1 Source: ts1.getDayOfWeek("America/Los_Angeles") @@ -248,8 +203,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: 259200 -} +bindings: {ts1=1970-01-04T00:00:00Z} result: 6 Source: ts1.getDayOfWeek() @@ -257,8 +211,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: 259200 -} +bindings: {ts1=1970-01-04T00:00:00Z} result: 0 Source: ts1.getDayOfWeek() @@ -266,8 +219,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: -1 -} +bindings: {ts1=1969-12-31T23:59:59Z} result: 3 Source: ts1.getDayOfWeek("Indian/Cocos") @@ -275,8 +227,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: 259200 -} +bindings: {ts1=1970-01-04T00:00:00Z} result: 0 Source: ts1.getDayOfWeek("-9:30") @@ -284,8 +235,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: 259200 -} +bindings: {ts1=1970-01-04T00:00:00Z} result: 6 Source: ts1.getHours("America/Los_Angeles") @@ -293,9 +243,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: 1 -nanos: 11000000 -} +bindings: {ts1=1970-01-01T00:00:01.011Z} result: 16 Source: ts1.getHours() @@ -303,9 +251,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: 1 -nanos: 11000000 -} +bindings: {ts1=1970-01-01T00:00:01.011Z} result: 0 Source: ts1.getHours() @@ -313,8 +259,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: -1 -} +bindings: {ts1=1969-12-31T23:59:59Z} result: 23 Source: ts1.getHours("Indian/Cocos") @@ -322,9 +267,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: 1 -nanos: 11000000 -} +bindings: {ts1=1970-01-01T00:00:01.011Z} result: 6 Source: ts1.getHours("6:30") @@ -332,9 +275,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: 1 -nanos: 11000000 -} +bindings: {ts1=1970-01-01T00:00:01.011Z} result: 6 Source: ts1.getMinutes("America/Los_Angeles") @@ -342,9 +283,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: 1 -nanos: 11000000 -} +bindings: {ts1=1970-01-01T00:00:01.011Z} result: 0 Source: ts1.getMinutes() @@ -352,9 +291,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: 1 -nanos: 11000000 -} +bindings: {ts1=1970-01-01T00:00:01.011Z} result: 0 Source: ts1.getMinutes() @@ -362,8 +299,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: -1 -} +bindings: {ts1=1969-12-31T23:59:59Z} result: 59 Source: ts1.getMinutes("Indian/Cocos") @@ -371,9 +307,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: 1 -nanos: 11000000 -} +bindings: {ts1=1970-01-01T00:00:01.011Z} result: 30 Source: ts1.getMinutes("-8:00") @@ -381,9 +315,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: 1 -nanos: 11000000 -} +bindings: {ts1=1970-01-01T00:00:01.011Z} result: 0 Source: ts1.getSeconds("America/Los_Angeles") @@ -391,9 +323,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: 1 -nanos: 11000000 -} +bindings: {ts1=1970-01-01T00:00:01.011Z} result: 1 Source: ts1.getSeconds() @@ -401,9 +331,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: 1 -nanos: 11000000 -} +bindings: {ts1=1970-01-01T00:00:01.011Z} result: 1 Source: ts1.getSeconds() @@ -411,8 +339,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: -1 -} +bindings: {ts1=1969-12-31T23:59:59Z} result: 59 Source: ts1.getSeconds("Indian/Cocos") @@ -420,9 +347,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: 1 -nanos: 11000000 -} +bindings: {ts1=1970-01-01T00:00:01.011Z} result: 1 Source: ts1.getSeconds("-8:00") @@ -430,9 +355,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: 1 -nanos: 11000000 -} +bindings: {ts1=1970-01-01T00:00:01.011Z} result: 1 Source: ts1.getMilliseconds("America/Los_Angeles") @@ -440,9 +363,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: 1 -nanos: 11000000 -} +bindings: {ts1=1970-01-01T00:00:01.011Z} result: 11 Source: ts1.getMilliseconds() @@ -450,9 +371,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: 1 -nanos: 11000000 -} +bindings: {ts1=1970-01-01T00:00:01.011Z} result: 11 Source: ts1.getMilliseconds("Indian/Cocos") @@ -460,9 +379,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: 1 -nanos: 11000000 -} +bindings: {ts1=1970-01-01T00:00:01.011Z} result: 11 Source: ts1.getMilliseconds("-8:00") @@ -470,9 +387,7 @@ declare ts1 { value google.protobuf.Timestamp } =====> -bindings: {ts1=seconds: 1 -nanos: 11000000 -} +bindings: {ts1=1970-01-01T00:00:01.011Z} result: 11 Source: ts1.getFullYear() < val @@ -483,9 +398,7 @@ declare val { value int } =====> -bindings: {val=2013} +> {ts1=seconds: 1 -nanos: 11000000 -} +bindings: {val=2013} +> {ts1=1970-01-01T00:00:01.011Z} result: true Source: ts1.getMonth() < val @@ -496,9 +409,7 @@ declare val { value int } =====> -bindings: {val=12} +> {ts1=seconds: 1 -nanos: 11000000 -} +bindings: {val=12} +> {ts1=1970-01-01T00:00:01.011Z} result: true Source: ts1.getDayOfYear() < val @@ -509,9 +420,7 @@ declare val { value int } =====> -bindings: {val=13} +> {ts1=seconds: 1 -nanos: 11000000 -} +bindings: {val=13} +> {ts1=1970-01-01T00:00:01.011Z} result: true Source: ts1.getDayOfMonth() < val @@ -522,9 +431,7 @@ declare val { value int } =====> -bindings: {val=10} +> {ts1=seconds: 1 -nanos: 11000000 -} +bindings: {val=10} +> {ts1=1970-01-01T00:00:01.011Z} result: true Source: ts1.getDate() < val @@ -535,9 +442,7 @@ declare val { value int } =====> -bindings: {val=15} +> {ts1=seconds: 1 -nanos: 11000000 -} +bindings: {val=15} +> {ts1=1970-01-01T00:00:01.011Z} result: true Source: ts1.getDayOfWeek() < val @@ -548,9 +453,7 @@ declare val { value int } =====> -bindings: {val=15} +> {ts1=seconds: 1 -nanos: 11000000 -} +bindings: {val=15} +> {ts1=1970-01-01T00:00:01.011Z} result: true Source: ts1.getHours() < val @@ -561,9 +464,7 @@ declare val { value int } =====> -bindings: {val=15} +> {ts1=seconds: 1 -nanos: 11000000 -} +bindings: {val=15} +> {ts1=1970-01-01T00:00:01.011Z} result: true Source: ts1.getMinutes() < val @@ -574,9 +475,7 @@ declare val { value int } =====> -bindings: {val=15} +> {ts1=seconds: 1 -nanos: 11000000 -} +bindings: {val=15} +> {ts1=1970-01-01T00:00:01.011Z} result: true Source: ts1.getSeconds() < val @@ -587,9 +486,7 @@ declare val { value int } =====> -bindings: {val=15} +> {ts1=seconds: 1 -nanos: 11000000 -} +bindings: {val=15} +> {ts1=1970-01-01T00:00:01.011Z} result: true Source: ts1.getMilliseconds() < val @@ -600,7 +497,5 @@ declare val { value int } =====> -bindings: {val=15} +> {ts1=seconds: 1 -nanos: 11000000 -} +bindings: {val=15} +> {ts1=1970-01-01T00:00:01.011Z} result: true \ No newline at end of file diff --git a/runtime/src/test/resources/typeComparisons.baseline b/runtime/src/test/resources/typeComparisons.baseline index 3af41c3da..e8fc3473f 100644 --- a/runtime/src/test/resources/typeComparisons.baseline +++ b/runtime/src/test/resources/typeComparisons.baseline @@ -23,7 +23,7 @@ Source: type(duration('10s')) == google.protobuf.Duration bindings: {} result: true -Source: type(TestAllTypes{}) == TestAllTypes && type(TestAllTypes{}) == proto3.TestAllTypes && type(TestAllTypes{}) == .dev.cel.testing.testdata.proto3.TestAllTypes && type(proto3.TestAllTypes{}) == TestAllTypes && type(proto3.TestAllTypes{}) == proto3.TestAllTypes && type(proto3.TestAllTypes{}) == .dev.cel.testing.testdata.proto3.TestAllTypes && type(.dev.cel.testing.testdata.proto3.TestAllTypes{}) == TestAllTypes && type(.dev.cel.testing.testdata.proto3.TestAllTypes{}) == proto3.TestAllTypes && type(.dev.cel.testing.testdata.proto3.TestAllTypes{}) == .dev.cel.testing.testdata.proto3.TestAllTypes +Source: type(TestAllTypes{}) == TestAllTypes && type(TestAllTypes{}) == proto3.TestAllTypes && type(TestAllTypes{}) == .cel.expr.conformance.proto3.TestAllTypes && type(proto3.TestAllTypes{}) == TestAllTypes && type(proto3.TestAllTypes{}) == proto3.TestAllTypes && type(proto3.TestAllTypes{}) == .cel.expr.conformance.proto3.TestAllTypes && type(.cel.expr.conformance.proto3.TestAllTypes{}) == TestAllTypes && type(.cel.expr.conformance.proto3.TestAllTypes{}) == proto3.TestAllTypes && type(.cel.expr.conformance.proto3.TestAllTypes{}) == .cel.expr.conformance.proto3.TestAllTypes =====> bindings: {} result: true @@ -42,3 +42,14 @@ Source: type(null) == null_type =====> bindings: {} result: true + +Source: type(duration) == google.protobuf.Duration && type(timestamp) == google.protobuf.Timestamp +declare duration { + value dyn +} +declare timestamp { + value dyn +} +=====> +bindings: {duration=PT0S, timestamp=1970-01-01T00:00:00Z} +result: true diff --git a/runtime/src/test/resources/uint64Conversions.baseline b/runtime/src/test/resources/uint64Conversions.baseline index cfdc61974..9f858f474 100644 --- a/runtime/src/test/resources/uint64Conversions.baseline +++ b/runtime/src/test/resources/uint64Conversions.baseline @@ -8,30 +8,22 @@ Source: uint(2.1) bindings: {} result: 2 -Source: uint(-1) -=====> -bindings: {} -error: evaluation error: int out of uint range -error_code: NUMERIC_OVERFLOW - Source: uint(1e19) =====> bindings: {} result: 10000000000000000000 -Source: uint(6.022e23) +Source: uint(42) =====> bindings: {} -error: evaluation error: double out of uint range -error_code: NUMERIC_OVERFLOW +result: 42 -Source: uint(42) +Source: uint(1u) =====> bindings: {} -result: 42 +result: 1 -Source: uint('f1') +Source: uint(dyn(1u)) =====> bindings: {} -error: evaluation error: f1 -error_code: BAD_FORMAT +result: 1 \ No newline at end of file diff --git a/runtime/src/test/resources/uint64Conversions_error.baseline b/runtime/src/test/resources/uint64Conversions_error.baseline new file mode 100644 index 000000000..0a47ccf88 --- /dev/null +++ b/runtime/src/test/resources/uint64Conversions_error.baseline @@ -0,0 +1,17 @@ +Source: uint(-1) +=====> +bindings: {} +error: evaluation error at test_location:4: int out of uint range +error_code: NUMERIC_OVERFLOW + +Source: uint(6.022e23) +=====> +bindings: {} +error: evaluation error at test_location:4: double out of uint range +error_code: NUMERIC_OVERFLOW + +Source: uint('f1') +=====> +bindings: {} +error: evaluation error at test_location:4: f1 +error_code: BAD_FORMAT \ No newline at end of file diff --git a/runtime/src/test/resources/unknownField.baseline b/runtime/src/test/resources/unknownField.baseline index 2831bfd9b..8e4598bef 100644 --- a/runtime/src/test/resources/unknownField.baseline +++ b/runtime/src/test/resources/unknownField.baseline @@ -1,563 +1,55 @@ Source: x.single_int32 declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_nested_message { -} -repeated_nested_message { - bb: 14 -} -single_duration { - seconds: 15 -} -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: map_int32_int64, - paths: single_int32, - paths: single_nested_message.bb, - paths: repeated_nested_message, - paths: single_duration.seconds -} -} -result: unknown { - exprs: 2 -} - +bindings: {} +result: CelUnknownSet{attributes=[], unknownExprIds=[1]} Source: x.map_int32_int64[22] declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_nested_message { -} -repeated_nested_message { - bb: 14 -} -single_duration { - seconds: 15 -} -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: map_int32_int64, - paths: single_int32, - paths: single_nested_message.bb, - paths: repeated_nested_message, - paths: single_duration.seconds -} -} -result: unknown { - exprs: 2 -} - +bindings: {} +result: CelUnknownSet{attributes=[], unknownExprIds=[1]} Source: x.repeated_nested_message[1] declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_nested_message { -} -repeated_nested_message { - bb: 14 -} -single_duration { - seconds: 15 -} -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: map_int32_int64, - paths: single_int32, - paths: single_nested_message.bb, - paths: repeated_nested_message, - paths: single_duration.seconds -} -} -result: unknown { - exprs: 2 -} - +bindings: {} +result: CelUnknownSet{attributes=[], unknownExprIds=[1]} Source: x.single_timestamp.getSeconds() declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_nested_message { -} -repeated_nested_message { - bb: 14 -} -single_duration { - seconds: 15 -} -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: map_int32_int64, - paths: single_int32, - paths: single_nested_message.bb, - paths: repeated_nested_message, - paths: single_duration.seconds -} -} -result: 15 - -Source: x.single_duration.getMilliseconds() -declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes -} -=====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_nested_message { -} -repeated_nested_message { - bb: 14 -} -single_duration { - seconds: 15 -} -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: map_int32_int64, - paths: single_int32, - paths: single_nested_message.bb, - paths: repeated_nested_message, - paths: single_duration.seconds -} -} -error: evaluation error: Incomplete data does not support function calls. -error_code: INVALID_ARGUMENT - -Source: x.single_duration + x.single_duration -declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes -} -=====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_nested_message { -} -repeated_nested_message { - bb: 14 -} -single_duration { - seconds: 15 -} -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: map_int32_int64, - paths: single_int32, - paths: single_nested_message.bb, - paths: repeated_nested_message, - paths: single_duration.seconds -} -} -error: evaluation error: Incomplete data does not support function calls. -error_code: INVALID_ARGUMENT +bindings: {} +result: CelUnknownSet{attributes=[], unknownExprIds=[1]} Source: x.single_nested_message.bb declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_nested_message { -} -repeated_nested_message { - bb: 14 -} -single_duration { - seconds: 15 -} -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: map_int32_int64, - paths: single_int32, - paths: single_nested_message.bb, - paths: repeated_nested_message, - paths: single_duration.seconds -} -} -result: unknown { - exprs: 3 -} - - -Source: x.single_nested_message -declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes -} -=====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_nested_message { -} -repeated_nested_message { - bb: 14 -} -single_duration { - seconds: 15 -} -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: map_int32_int64, - paths: single_int32, - paths: single_nested_message.bb, - paths: repeated_nested_message, - paths: single_duration.seconds -} -} -error: evaluation error: Incomplete data cannot be returned as a result. -error_code: INVALID_ARGUMENT - -Source: TestAllTypes{single_nested_message: x.single_nested_message} -declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes -} -=====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_nested_message { -} -repeated_nested_message { - bb: 14 -} -single_duration { - seconds: 15 -} -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: map_int32_int64, - paths: single_int32, - paths: single_nested_message.bb, - paths: repeated_nested_message, - paths: single_duration.seconds -} -} -error: evaluation error: Incomplete data cannot be a field of a message. -error_code: INVALID_ARGUMENT +bindings: {} +result: CelUnknownSet{attributes=[], unknownExprIds=[1]} Source: {1: x.single_int32} declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_nested_message { -} -repeated_nested_message { - bb: 14 -} -single_duration { - seconds: 15 -} -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: map_int32_int64, - paths: single_int32, - paths: single_nested_message.bb, - paths: repeated_nested_message, - paths: single_duration.seconds -} -} -result: unknown { - exprs: 5 -} - - -Source: {1: x.single_int64} -declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes -} -=====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_nested_message { -} -repeated_nested_message { - bb: 14 -} -single_duration { - seconds: 15 -} -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: map_int32_int64, - paths: single_int32, - paths: single_nested_message.bb, - paths: repeated_nested_message, - paths: single_duration.seconds -} -} -result: {1=0} - -Source: {1: x.single_nested_message} -declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes -} -=====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_nested_message { -} -repeated_nested_message { - bb: 14 -} -single_duration { - seconds: 15 -} -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: map_int32_int64, - paths: single_int32, - paths: single_nested_message.bb, - paths: repeated_nested_message, - paths: single_duration.seconds -} -} -error: evaluation error: Incomplete data cannot be a value of a map. -error_code: INVALID_ARGUMENT +bindings: {} +result: CelUnknownSet{attributes=[], unknownExprIds=[4]} Source: [1, x.single_int32] declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes -} -=====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_nested_message { -} -repeated_nested_message { - bb: 14 -} -single_duration { - seconds: 15 -} -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: map_int32_int64, - paths: single_int32, - paths: single_nested_message.bb, - paths: repeated_nested_message, - paths: single_duration.seconds -} -} -result: unknown { - exprs: 4 -} - - -Source: [x.single_nested_message] -declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes -} -=====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_nested_message { -} -repeated_nested_message { - bb: 14 -} -single_duration { - seconds: 15 -} -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: map_int32_int64, - paths: single_int32, - paths: single_nested_message.bb, - paths: repeated_nested_message, - paths: single_duration.seconds -} -} -error: evaluation error: Incomplete data cannot be an elem of a list. -error_code: INVALID_ARGUMENT - -Source: x.single_nested_message.bb -declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes -} -=====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_nested_message { -} -repeated_nested_message { - bb: 14 -} -single_duration { - seconds: 15 -} -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: single_nested_message -} -} -result: unknown { - exprs: 2 -} - - -Source: (x.single_nested_message.bb == 42) || true -declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_nested_message { -} -repeated_nested_message { - bb: 14 -} -single_duration { - seconds: 15 -} -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: single_nested_message -} -} -result: true - -Source: (x.single_nested_message.bb == 42) || false -declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes -} -=====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_nested_message { -} -repeated_nested_message { - bb: 14 -} -single_duration { - seconds: 15 -} -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: single_nested_message -} -} -result: unknown { - exprs: 2 -} - - -Source: (x.single_nested_message.bb == 42) && true -declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes -} -=====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_nested_message { -} -repeated_nested_message { - bb: 14 -} -single_duration { - seconds: 15 -} -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: single_nested_message -} -} -result: unknown { - exprs: 2 -} - - -Source: (x.single_nested_message.bb == 42) && false -declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes -} -=====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_nested_message { -} -repeated_nested_message { - bb: 14 -} -single_duration { - seconds: 15 -} -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: single_nested_message -} -} -result: false +bindings: {} +result: CelUnknownSet{attributes=[], unknownExprIds=[3]} \ No newline at end of file diff --git a/runtime/src/test/resources/unknownResultSet.baseline b/runtime/src/test/resources/unknownResultSet.baseline index aa9d032ff..ad97004f3 100644 --- a/runtime/src/test/resources/unknownResultSet.baseline +++ b/runtime/src/test/resources/unknownResultSet.baseline @@ -1,990 +1,431 @@ -Source: x.single_int32 == 1 && x.single_string == "test" +Source: x.single_int32 == 1 && true declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_string: "test" -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: single_int32, - paths: single_int64, - paths: map_int32_int64 -} -} -result: unknown { - exprs: 2 -} - +bindings: {} +result: CelUnknownSet{attributes=[], unknownExprIds=[1]} -Source: x.single_int32 == 1 && x.single_string != "test" +Source: x.single_int32 == 1 && false declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_string: "test" -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: single_int32, - paths: single_int64, - paths: map_int32_int64 -} -} +bindings: {} result: false Source: x.single_int32 == 1 && x.single_int64 == 1 declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_string: "test" -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: single_int32, - paths: single_int64, - paths: map_int32_int64 -} -} -result: unknown { - exprs: 2 - exprs: 7 -} - +bindings: {} +result: CelUnknownSet{attributes=[], unknownExprIds=[1, 6]} Source: x.single_int32 == 1 && x.single_timestamp <= timestamp("bad timestamp string") declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_string: "test" -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: single_int32, - paths: single_int64, - paths: map_int32_int64 -} -} -result: unknown { - exprs: 2 -} - +bindings: {} +result: CelUnknownSet{attributes=[], unknownExprIds=[1]} -Source: x.single_string == "test" && x.single_int32 == 1 +Source: true && x.single_int32 == 1 declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_string: "test" -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: single_int32, - paths: single_int64, - paths: map_int32_int64 -} -} -result: unknown { - exprs: 7 -} +bindings: {} +result: CelUnknownSet{attributes=[], unknownExprIds=[3]} - -Source: x.single_string != "test" && x.single_int32 == 1 +Source: false && x.single_int32 == 1 declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_string: "test" -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: single_int32, - paths: single_int64, - paths: map_int32_int64 -} -} +bindings: {} result: false Source: x.single_timestamp <= timestamp("bad timestamp string") && x.single_int32 == 1 declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_string: "test" -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: single_int32, - paths: single_int64, - paths: map_int32_int64 -} -} -result: unknown { - exprs: 8 -} - +bindings: {} +result: CelUnknownSet{attributes=[], unknownExprIds=[7]} Source: x.single_timestamp <= timestamp("bad timestamp string") && x.single_timestamp > timestamp("another bad timestamp string") declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_string: "test" -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: single_int32, - paths: single_int64, - paths: map_int32_int64 -} -} -error: evaluation error: Failed to parse timestamp: invalid timestamp "bad timestamp string" +bindings: {} +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_string == "test" declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_string: "test" -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: single_int32, - paths: single_int64, - paths: map_int32_int64 -} -} -result: true +bindings: {} +result: CelUnknownSet{attributes=[], unknownExprIds=[1, 6]} Source: x.single_int32 == 1 || x.single_string != "test" declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_string: "test" -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: single_int32, - paths: single_int64, - paths: map_int32_int64 -} -} -result: unknown { - exprs: 2 -} - +bindings: {} +result: CelUnknownSet{attributes=[], unknownExprIds=[1, 6]} Source: x.single_int32 == 1 || x.single_int64 == 1 declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_string: "test" -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: single_int32, - paths: single_int64, - paths: map_int32_int64 -} -} -result: unknown { - exprs: 2 - exprs: 7 -} - +bindings: {} +result: CelUnknownSet{attributes=[], unknownExprIds=[1, 6]} Source: x.single_int32 == 1 || x.single_timestamp <= timestamp("bad timestamp string") declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_string: "test" -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: single_int32, - paths: single_int64, - paths: map_int32_int64 -} -} -result: unknown { - exprs: 2 -} - +bindings: {} +result: CelUnknownSet{attributes=[], unknownExprIds=[1]} -Source: x.single_string == "test" || x.single_int32 == 1 +Source: true || x.single_int32 == 1 declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_string: "test" -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: single_int32, - paths: single_int64, - paths: map_int32_int64 -} -} +bindings: {} result: true -Source: x.single_string != "test" || x.single_int32 == 1 +Source: false || x.single_int32 == 1 declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_string: "test" -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: single_int32, - paths: single_int64, - paths: map_int32_int64 -} -} -result: unknown { - exprs: 7 -} - +bindings: {} +result: CelUnknownSet{attributes=[], unknownExprIds=[3]} Source: x.single_timestamp <= timestamp("bad timestamp string") || x.single_int32 == 1 declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_string: "test" -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: single_int32, - paths: single_int64, - paths: map_int32_int64 -} -} -result: unknown { - exprs: 8 -} - +bindings: {} +result: CelUnknownSet{attributes=[], unknownExprIds=[7]} Source: x.single_timestamp <= timestamp("bad timestamp string") || x.single_timestamp > timestamp("another bad timestamp string") declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_string: "test" -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: single_int32, - paths: single_int64, - paths: map_int32_int64 -} -} -error: evaluation error: Failed to parse timestamp: invalid timestamp "bad timestamp string" +bindings: {} +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.f(1) declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare f { function f int.(int) -> bool } =====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_string: "test" -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: single_int32, - paths: single_int64, - paths: map_int32_int64 -} -} -result: unknown { - exprs: 2 -} - +bindings: {} +result: CelUnknownSet{attributes=[], unknownExprIds=[1]} Source: 1.f(x.single_int32) declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare f { function f int.(int) -> bool } =====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_string: "test" -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: single_int32, - paths: single_int64, - paths: map_int32_int64 -} -} -result: unknown { - exprs: 4 -} - +bindings: {} +result: CelUnknownSet{attributes=[], unknownExprIds=[3]} Source: x.single_int64.f(x.single_int32) declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare f { function f int.(int) -> bool } =====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_string: "test" -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: single_int32, - paths: single_int64, - paths: map_int32_int64 -} -} -result: unknown { - exprs: 2 - exprs: 5 -} - +bindings: {} +result: CelUnknownSet{attributes=[], unknownExprIds=[1, 4]} Source: x declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare f { function f int.(int) -> bool } =====> -bindings: {y=class dev.cel.runtime.PartialMessage{ -message: { -single_string: "test" +bindings: {y=single_string: "test" single_timestamp { seconds: 15 } -}, -fieldMask: { - paths: single_int32, - paths: single_int64, - paths: map_int32_int64 -} } -result: unknown { - exprs: 1 -} - +result: CelUnknownSet{attributes=[], unknownExprIds=[1]} Source: x declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare f { function f int.(int) -> bool } =====> -bindings: {x=unknown { -} -} -result: unknown { - exprs: 1 -} - +bindings: {x=CelUnknownSet{attributes=[], unknownExprIds=[1]}} +result: CelUnknownSet{attributes=[], unknownExprIds=[1]} Source: x.map_int32_int64.map(x, x > 0, x + 1) declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare f { function f int.(int) -> bool } =====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_string: "test" -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: single_int32, - paths: single_int64, - paths: map_int32_int64 -} -} -result: unknown { - exprs: 2 -} - +bindings: {} +result: CelUnknownSet{attributes=[], unknownExprIds=[1]} Source: [0, 2, 4].exists(z, z == 2 || z == x.single_int32) declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare f { function f int.(int) -> bool } =====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_string: "test" -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: single_int32, - paths: single_int64, - paths: map_int32_int64 -} -} +bindings: {} result: true Source: [0, 2, 4].exists(z, z == x.single_int32) declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare f { function f int.(int) -> bool } =====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_string: "test" -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: single_int32, - paths: single_int64, - paths: map_int32_int64 -} -} -result: unknown { - exprs: 10 -} - +bindings: {} +result: CelUnknownSet{attributes=[], unknownExprIds=[9]} Source: [0, 2, 4].exists_one(z, z == 0 || (z == 2 && z == x.single_int32) || (z == 4 && z == x.single_int64)) declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare f { function f int.(int) -> bool } =====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_string: "test" -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: single_int32, - paths: single_int64, - paths: map_int32_int64 -} -} -result: unknown { - exprs: 27 -} - +bindings: {} +result: CelUnknownSet{attributes=[], unknownExprIds=[26]} Source: [0, 2].all(z, z == 2 || z == x.single_int32) declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare f { function f int.(int) -> bool } =====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_string: "test" -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: single_int32, - paths: single_int64, - paths: map_int32_int64 -} -} -result: unknown { - exprs: 13 -} - +bindings: {} +result: CelUnknownSet{attributes=[], unknownExprIds=[12]} Source: [0, 2, 4].filter(z, z == 0 || (z == 2 && z == x.single_int32) || (z == 4 && z == x.single_int64)) declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare f { function f int.(int) -> bool } =====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_string: "test" -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: single_int32, - paths: single_int64, - paths: map_int32_int64 -} -} -result: unknown { - exprs: 27 -} - +bindings: {} +result: CelUnknownSet{attributes=[], unknownExprIds=[26]} Source: [0, 2, 4].map(z, z == 0 || (z == 2 && z == x.single_int32) || (z == 4 && z == x.single_int64)) declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare f { function f int.(int) -> bool } =====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_string: "test" -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: single_int32, - paths: single_int64, - paths: map_int32_int64 -} -} -result: unknown { - exprs: 18 - exprs: 27 -} - +bindings: {} +result: CelUnknownSet{attributes=[], unknownExprIds=[17, 26]} Source: x.single_int32 == 1 ? 1 : 2 declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare f { function f int.(int) -> bool } =====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_string: "test" -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: single_int32, - paths: single_int64, - paths: map_int32_int64 -} -} -result: unknown { - exprs: 2 -} +bindings: {} +result: CelUnknownSet{attributes=[], unknownExprIds=[1]} - -Source: x.single_string == "test" ? x.single_int32 : 2 +Source: true ? x.single_int32 : 2 declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare f { function f int.(int) -> bool } =====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_string: "test" -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: single_int32, - paths: single_int64, - paths: map_int32_int64 -} -} -result: unknown { - exprs: 7 -} +bindings: {} +result: CelUnknownSet{attributes=[], unknownExprIds=[3]} - -Source: x.single_string == "test" ? 1 : x.single_int32 +Source: true ? 1 : x.single_int32 declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare f { function f int.(int) -> bool } =====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_string: "test" -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: single_int32, - paths: single_int64, - paths: map_int32_int64 -} -} +bindings: {} result: 1 -Source: x.single_string != "test" ? x.single_int32 : 2 +Source: false ? x.single_int32 : 2 declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare f { function f int.(int) -> bool } =====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_string: "test" -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: single_int32, - paths: single_int64, - paths: map_int32_int64 -} -} +bindings: {} result: 2 -Source: x.single_string != "test" ? 1 : x.single_int32 +Source: false ? 1 : x.single_int32 declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare f { function f int.(int) -> bool } =====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_string: "test" -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: single_int32, - paths: single_int64, - paths: map_int32_int64 -} -} -result: unknown { - exprs: 8 -} - +bindings: {} +result: CelUnknownSet{attributes=[], unknownExprIds=[4]} Source: x.single_int64 == 1 ? x.single_int32 : x.single_int32 declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare f { function f int.(int) -> bool } =====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_string: "test" -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: single_int32, - paths: single_int64, - paths: map_int32_int64 -} -} -result: unknown { - exprs: 2 -} - +bindings: {} +result: CelUnknownSet{attributes=[], unknownExprIds=[1]} Source: {x.single_int32: 2, 3: 4} declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare f { function f int.(int) -> bool } =====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_string: "test" -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: single_int32, - paths: single_int64, - paths: map_int32_int64 -} -} -result: unknown { - exprs: 4 -} - +bindings: {} +result: CelUnknownSet{attributes=[], unknownExprIds=[3]} Source: {1: x.single_int32, 3: 4} declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare f { function f int.(int) -> bool } =====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_string: "test" -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: single_int32, - paths: single_int64, - paths: map_int32_int64 -} -} -result: unknown { - exprs: 5 -} - +bindings: {} +result: CelUnknownSet{attributes=[], unknownExprIds=[4]} Source: {1: x.single_int32, x.single_int64: 4} declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare f { function f int.(int) -> bool } =====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_string: "test" -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: single_int32, - paths: single_int64, - paths: map_int32_int64 -} -} -result: unknown { - exprs: 5 - exprs: 8 -} - +bindings: {} +result: CelUnknownSet{attributes=[], unknownExprIds=[4, 7]} Source: [1, x.single_int32, 3, 4] declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare f { function f int.(int) -> bool } =====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_string: "test" -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: single_int32, - paths: single_int64, - paths: map_int32_int64 -} -} -result: unknown { - exprs: 4 -} - +bindings: {} +result: CelUnknownSet{attributes=[], unknownExprIds=[3]} Source: [1, x.single_int32, x.single_int64, 4] declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare f { function f int.(int) -> bool } =====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_string: "test" -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: single_int32, - paths: single_int64, - paths: map_int32_int64 -} -} -result: unknown { - exprs: 4 - exprs: 6 -} - +bindings: {} +result: CelUnknownSet{attributes=[], unknownExprIds=[3, 5]} Source: TestAllTypes{single_int32: x.single_int32}.single_int32 == 2 declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare f { function f int.(int) -> bool } =====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_string: "test" -single_timestamp { - seconds: 15 -} -}, -fieldMask: { - paths: single_int32, - paths: single_int64, - paths: map_int32_int64 -} -} -result: unknown { - exprs: 4 -} - +bindings: {} +result: CelUnknownSet{attributes=[], unknownExprIds=[3]} Source: TestAllTypes{single_int32: x.single_int32, single_int64: x.single_int64} declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } declare f { function f int.(int) -> bool } =====> -bindings: {x=class dev.cel.runtime.PartialMessage{ -message: { -single_string: "test" -single_timestamp { - seconds: 15 +bindings: {} +result: CelUnknownSet{attributes=[], unknownExprIds=[3, 6]} + +Source: type(x.single_int32) +declare x { + value cel.expr.conformance.proto3.TestAllTypes } -}, -fieldMask: { - paths: single_int32, - paths: single_int64, - paths: map_int32_int64 +declare f { + function f int.(int) -> bool } +=====> +bindings: {} +result: CelUnknownSet{attributes=[], unknownExprIds=[2]} + +Source: type(1 / 0 > 2) +declare x { + value cel.expr.conformance.proto3.TestAllTypes } -result: unknown { - exprs: 4 - exprs: 7 +declare f { + function f int.(int) -> bool } - - +=====> +bindings: {} +error: evaluation error at test_location:7: / by zero +error_code: DIVIDE_BY_ZERO \ No newline at end of file diff --git a/runtime/src/test/resources/wrappers.baseline b/runtime/src/test/resources/wrappers.baseline index 3305bbde4..d8212059b 100644 --- a/runtime/src/test/resources/wrappers.baseline +++ b/runtime/src/test/resources/wrappers.baseline @@ -1,6 +1,6 @@ Source: x.single_bool_wrapper == true && x.single_bytes_wrapper == b'hi' && x.single_double_wrapper == -3.0 && x.single_float_wrapper == 1.5 && x.single_int32_wrapper == -12 && x.single_int64_wrapper == -34 && x.single_string_wrapper == 'hello' && x.single_uint32_wrapper == 12u && x.single_uint64_wrapper == 34u declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> bindings: {x=single_int64_wrapper { @@ -35,7 +35,7 @@ result: true Source: x.single_bool_wrapper == google.protobuf.BoolValue{} && x.single_bytes_wrapper == google.protobuf.BytesValue{value: b'hi'} && x.single_double_wrapper == google.protobuf.DoubleValue{value: -3.0} && x.single_float_wrapper == google.protobuf.FloatValue{value: 1.5} && x.single_int32_wrapper == google.protobuf.Int32Value{value: -12} && x.single_int64_wrapper == google.protobuf.Int64Value{value: -34} && x.single_string_wrapper == google.protobuf.StringValue{} && x.single_uint32_wrapper == google.protobuf.UInt32Value{value: 12u} && x.single_uint64_wrapper == google.protobuf.UInt64Value{value: 34u} declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> bindings: {x=single_int64_wrapper { @@ -66,10 +66,136 @@ single_bytes_wrapper { } result: true +Source: x.repeated_int32_wrapper == [1,2] && x.repeated_int64_wrapper == [3] && x.repeated_float_wrapper == [1.5, 2.5] && x.repeated_double_wrapper == [3.5, 4.5] && x.repeated_string_wrapper == ['foo', 'bar'] && x.repeated_bool_wrapper == [true] && x.repeated_uint32_wrapper == [1u, 2u] && x.repeated_uint64_wrapper == [] +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {x=single_int64_wrapper { + value: -34 +} +single_int32_wrapper { + value: -12 +} +single_double_wrapper { + value: -3.0 +} +single_float_wrapper { + value: 1.5 +} +single_uint64_wrapper { + value: 34 +} +single_uint32_wrapper { + value: 12 +} +single_string_wrapper { +} +single_bool_wrapper { +} +single_bytes_wrapper { + value: "hi" +} +repeated_int64_wrapper { + value: 3 +} +repeated_int32_wrapper { + value: 1 +} +repeated_int32_wrapper { + value: 2 +} +repeated_double_wrapper { + value: 3.5 +} +repeated_double_wrapper { + value: 4.5 +} +repeated_float_wrapper { + value: 1.5 +} +repeated_float_wrapper { + value: 2.5 +} +repeated_uint32_wrapper { + value: 1 +} +repeated_uint32_wrapper { + value: 2 +} +repeated_string_wrapper { + value: "foo" +} +repeated_string_wrapper { + value: "bar" +} +repeated_bool_wrapper { + value: true +} +} +result: true + Source: x.single_bool_wrapper == null && x.single_bytes_wrapper == null && x.single_double_wrapper == null && x.single_float_wrapper == null && x.single_int32_wrapper == null && x.single_int64_wrapper == null && x.single_string_wrapper == null && x.single_uint32_wrapper == null && x.single_uint64_wrapper == null declare x { - value dev.cel.testing.testdata.proto3.TestAllTypes + value cel.expr.conformance.proto3.TestAllTypes } =====> bindings: {x=} -result: true \ No newline at end of file +result: true + +Source: dyn_var +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +declare dyn_var { + value dyn +} +=====> +bindings: {dyn_var=NULL_VALUE} +result: NULL_VALUE + +Source: TestAllTypes{repeated_int32: int32_list}.repeated_int32 == [1] && TestAllTypes{repeated_int64: int64_list}.repeated_int64 == [2] && TestAllTypes{repeated_uint32: uint32_list}.repeated_uint32 == [3u] && TestAllTypes{repeated_uint64: uint64_list}.repeated_uint64 == [4u] && TestAllTypes{repeated_float: float_list}.repeated_float == [5.5] && TestAllTypes{repeated_double: double_list}.repeated_double == [6.6] && TestAllTypes{repeated_bool: bool_list}.repeated_bool == [true] && TestAllTypes{repeated_string: string_list}.repeated_string == ['hello'] && TestAllTypes{repeated_bytes: bytes_list}.repeated_bytes == [b'world'] +declare int32_list { + value list(int) +} +declare int64_list { + value list(int) +} +declare uint32_list { + value list(uint) +} +declare uint64_list { + value list(uint) +} +declare float_list { + value list(double) +} +declare double_list { + value list(double) +} +declare bool_list { + value list(bool) +} +declare string_list { + value list(string) +} +declare bytes_list { + value list(bytes) +} +=====> +bindings: {int32_list=[value: 1 +], int64_list=[value: 2 +], uint32_list=[value: 3 +], uint64_list=[value: 4 +], float_list=[value: 5.5 +], double_list=[value: 6.6 +], bool_list=[value: true +], string_list=[value: "hello" +], bytes_list=[value: "world" +]} +result: true + +Source: google.protobuf.Timestamp{ seconds: 253402300800 } +=====> +bindings: {} +result: +10000-01-01T00:00:00Z diff --git a/runtime/standard/BUILD.bazel b/runtime/standard/BUILD.bazel new file mode 100644 index 000000000..c1c040f1d --- /dev/null +++ b/runtime/standard/BUILD.bazel @@ -0,0 +1,437 @@ +load("@rules_java//java:defs.bzl", "java_library") +load("//:cel_android_rules.bzl", "cel_android_library") + +package( + default_applicable_licenses = ["//:license"], + default_visibility = ["//visibility:public"], +) + +java_library( + name = "standard_function", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:standard_function"], +) + +cel_android_library( + name = "standard_function_android", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:standard_function_android"], +) + +java_library( + name = "add", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:add"], +) + +cel_android_library( + name = "add_android", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:add_android"], +) + +java_library( + name = "subtract", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:subtract"], +) + +cel_android_library( + name = "subtract_android", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:subtract_android"], +) + +java_library( + name = "bool", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:bool"], +) + +cel_android_library( + name = "bool_android", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:bool_android"], +) + +java_library( + name = "bytes", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:bytes"], +) + +cel_android_library( + name = "bytes_android", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:bytes_android"], +) + +java_library( + name = "contains", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:contains"], +) + +cel_android_library( + name = "contains_android", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:contains_android"], +) + +java_library( + name = "double", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:double"], +) + +cel_android_library( + name = "double_android", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:double_android"], +) + +java_library( + name = "duration", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:duration"], +) + +cel_android_library( + name = "duration_android", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:duration_android"], +) + +java_library( + name = "dyn", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:dyn"], +) + +cel_android_library( + name = "dyn_android", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:dyn_android"], +) + +java_library( + name = "ends_with", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:ends_with"], +) + +cel_android_library( + name = "ends_with_android", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:ends_with_android"], +) + +java_library( + name = "equals", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:equals"], +) + +cel_android_library( + name = "equals_android", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:equals_android"], +) + +java_library( + name = "get_day_of_year", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:get_day_of_year"], +) + +cel_android_library( + name = "get_day_of_year_android", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:get_day_of_year_android"], +) + +java_library( + name = "get_day_of_month", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:get_day_of_month"], +) + +cel_android_library( + name = "get_day_of_month_android", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:get_day_of_month_android"], +) + +java_library( + name = "get_day_of_week", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:get_day_of_week"], +) + +cel_android_library( + name = "get_day_of_week_android", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:get_day_of_week_android"], +) + +java_library( + name = "get_date", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:get_date"], +) + +cel_android_library( + name = "get_date_android", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:get_date_android"], +) + +java_library( + name = "get_full_year", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:get_full_year"], +) + +cel_android_library( + name = "get_full_year_android", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:get_full_year_android"], +) + +java_library( + name = "get_hours", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:get_hours"], +) + +cel_android_library( + name = "get_hours_android", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:get_hours_android"], +) + +java_library( + name = "get_milliseconds", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:get_milliseconds"], +) + +cel_android_library( + name = "get_milliseconds_android", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:get_milliseconds_android"], +) + +java_library( + name = "get_minutes", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:get_minutes"], +) + +cel_android_library( + name = "get_minutes_android", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:get_minutes_android"], +) + +java_library( + name = "get_month", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:get_month"], +) + +cel_android_library( + name = "get_month_android", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:get_month_android"], +) + +java_library( + name = "get_seconds", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:get_seconds"], +) + +cel_android_library( + name = "get_seconds_android", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:get_seconds_android"], +) + +java_library( + name = "greater", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:greater"], +) + +cel_android_library( + name = "greater_android", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:greater_android"], +) + +java_library( + name = "greater_equals", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:greater_equals"], +) + +cel_android_library( + name = "greater_equals_android", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:greater_equals_android"], +) + +java_library( + name = "in", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:in"], +) + +cel_android_library( + name = "in_android", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:in_android"], +) + +java_library( + name = "index", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:index"], +) + +cel_android_library( + name = "index_android", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:index_android"], +) + +java_library( + name = "int", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:int"], +) + +cel_android_library( + name = "int_android", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:int_android"], +) + +java_library( + name = "less", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:less"], +) + +cel_android_library( + name = "less_android", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:less_android"], +) + +java_library( + name = "less_equals", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:less_equals"], +) + +cel_android_library( + name = "less_equals_android", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:less_equals_android"], +) + +java_library( + name = "logical_not", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:logical_not"], +) + +cel_android_library( + name = "logical_not_android", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:logical_not_android"], +) + +java_library( + name = "matches", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:matches"], +) + +cel_android_library( + name = "matches_android", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:matches_android"], +) + +java_library( + name = "modulo", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:modulo"], +) + +cel_android_library( + name = "modulo_android", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:modulo_android"], +) + +java_library( + name = "multiply", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:multiply"], +) + +cel_android_library( + name = "multiply_android", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:multiply_android"], +) + +java_library( + name = "divide", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:divide"], +) + +cel_android_library( + name = "divide_android", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:divide_android"], +) + +java_library( + name = "negate", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:negate"], +) + +cel_android_library( + name = "negate_android", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:negate_android"], +) + +java_library( + name = "not_equals", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:not_equals"], +) + +cel_android_library( + name = "not_equals_android", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:not_equals_android"], +) + +java_library( + name = "size", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:size"], +) + +cel_android_library( + name = "size_android", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:size_android"], +) + +java_library( + name = "starts_with", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:starts_with"], +) + +cel_android_library( + name = "starts_with_android", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:starts_with_android"], +) + +java_library( + name = "string", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:string"], +) + +cel_android_library( + name = "string_android", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:string_android"], +) + +java_library( + name = "timestamp", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:timestamp"], +) + +cel_android_library( + name = "timestamp_android", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:timestamp_android"], +) + +java_library( + name = "uint", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:uint"], +) + +cel_android_library( + name = "uint_android", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:uint_android"], +) + +java_library( + name = "type", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:type"], +) + +cel_android_library( + name = "type_android", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:type_android"], +) + +java_library( + name = "not_strictly_false", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:not_strictly_false"], +) + +cel_android_library( + name = "not_strictly_false_android", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:not_strictly_false_android"], +) + +java_library( + name = "standard_overload", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:standard_overload"], +) + +cel_android_library( + name = "standard_overload_android", + exports = ["//runtime/src/main/java/dev/cel/runtime/standard:standard_overload_android"], +) diff --git a/runtime/testdata/BUILD.bazel b/runtime/testdata/BUILD.bazel index f4bc22608..743878225 100644 --- a/runtime/testdata/BUILD.bazel +++ b/runtime/testdata/BUILD.bazel @@ -1,12 +1,7 @@ package( default_applicable_licenses = ["//:license"], default_testonly = True, - default_visibility = ["//visibility:public"], -) - -alias( - name = "test_java_proto", - actual = "//runtime/src/test/resources:test_java_proto", + default_visibility = ["//:internal"], ) alias( diff --git a/testing.bzl b/testing.bzl index 026cdcb13..5425f7719 100644 --- a/testing.bzl +++ b/testing.bzl @@ -13,9 +13,10 @@ # limitations under the License. # From: https://github.com/google/guice/blob/master/test_defs.bzl - """starlark macros to generate test suites.""" +load("@rules_java//java:defs.bzl", "java_test") + _TEMPLATE = """package {VAR_PACKAGE}; import org.junit.runners.Suite; import org.junit.runner.RunWith; @@ -71,7 +72,11 @@ def junit4_test_suites( # "common/src/test/java/dev/cel/common/internal" becomes "dev/cel/common/internal" package_name = package_name.rpartition("/test/java/")[2] - test_files = srcs or native.glob(["**/*Test.java"]) + 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"], + ) test_classes = [] for src in test_files: test_name = src.replace(".java", "") @@ -84,7 +89,7 @@ def junit4_test_suites( package_name = package_name.replace("/", "."), ) - native.java_test( + java_test( name = "AllTestsSuite", test_class = (package_name + "/" + suite_name).replace("/", "."), srcs = [":" + suite_name], diff --git a/testing/BUILD.bazel b/testing/BUILD.bazel index bd958decc..cc389fed1 100644 --- a/testing/BUILD.bazel +++ b/testing/BUILD.bazel @@ -1,7 +1,9 @@ +load("@rules_java//java:defs.bzl", "java_library") + package( default_applicable_licenses = ["//:license"], default_testonly = True, - default_visibility = ["//visibility:public"], + default_visibility = ["//:internal"], ) java_library( @@ -9,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"], @@ -19,32 +26,27 @@ java_library( exports = ["//testing/src/main/java/dev/cel/testing:baseline_test_case"], ) -java_library( - name = "test_decls", - exports = ["//testing/src/main/java/dev/cel/testing:test_decls"], -) - java_library( name = "cel_baseline_test_case", exports = ["//testing/src/main/java/dev/cel/testing:cel_baseline_test_case"], ) java_library( - name = "sync", - exports = ["//testing/src/main/java/dev/cel/testing:sync"], + name = "base_interpreter_test", + exports = ["//testing/src/main/java/dev/cel/testing:base_interpreter_test"], ) -java_library( - name = "cel_value_sync", - exports = ["//testing/src/main/java/dev/cel/testing:cel_value_sync"], +alias( + name = "policy_test_resources", + actual = "//testing/src/test/resources/policy:policy_yaml_files", ) java_library( - name = "eval", - exports = ["//testing/src/main/java/dev/cel/testing:eval"], + name = "expr_value_utils", + exports = ["//testing/src/main/java/dev/cel/testing/utils:expr_value_utils"], ) java_library( - name = "base_interpreter_test", - exports = ["//testing/src/main/java/dev/cel/testing:base_interpreter_test"], + name = "proto_descriptor_utils", + exports = ["//testing/src/main/java/dev/cel/testing/utils:proto_descriptor_utils"], ) diff --git a/testing/compiled/BUILD.bazel b/testing/compiled/BUILD.bazel new file mode 100644 index 000000000..2040ba9ca --- /dev/null +++ b/testing/compiled/BUILD.bazel @@ -0,0 +1,15 @@ +package( + default_applicable_licenses = ["//:license"], + default_testonly = True, + default_visibility = ["//:internal"], +) + +alias( + name = "compiled_expr_utils", + actual = "//testing/src/main/java/dev/cel/testing/compiled:compiled_expr_utils", +) + +alias( + name = "compiled_expr_utils_android", + actual = "//testing/src/main/java/dev/cel/testing/compiled:compiled_expr_utils_android", +) diff --git a/testing/environment/BUILD.bazel b/testing/environment/BUILD.bazel new file mode 100644 index 000000000..5b0127db3 --- /dev/null +++ b/testing/environment/BUILD.bazel @@ -0,0 +1,45 @@ +package( + default_applicable_licenses = ["//:license"], + default_testonly = True, + default_visibility = ["//:internal"], +) + +alias( + name = "dump_env", + actual = "//testing/src/test/resources/environment:dump_env", +) + +alias( + name = "extended_env", + actual = "//testing/src/test/resources/environment:extended_env", +) + +alias( + name = "all_extensions", + actual = "//testing/src/test/resources/environment:all_extensions", +) + +alias( + name = "primitive_variables", + actual = "//testing/src/test/resources/environment:primitive_variables", +) + +alias( + name = "custom_functions", + actual = "//testing/src/test/resources/environment:custom_functions", +) + +alias( + name = "library_subset_env", + actual = "//testing/src/test/resources/environment:library_subset_env", +) + +alias( + name = "proto2_message_variables", + actual = "//testing/src/test/resources/environment:proto2_message_variables", +) + +alias( + name = "proto3_message_variables", + actual = "//testing/src/test/resources/environment:proto3_message_variables", +) diff --git a/testing/protos/BUILD.bazel b/testing/protos/BUILD.bazel new file mode 100644 index 000000000..17706ca45 --- /dev/null +++ b/testing/protos/BUILD.bazel @@ -0,0 +1,60 @@ +package( + default_applicable_licenses = ["//:license"], + default_testonly = True, + default_visibility = ["//:internal"], +) + +alias( + name = "single_file_java_proto", + 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", +) + +alias( + name = "multi_file_cel_java_proto", + actual = "//testing/src/test/resources/protos:multi_file_cel_java_proto", +) + +alias( + name = "message_with_enum_java_proto", + actual = "//testing/src/test/resources/protos:message_with_enum_java_proto", +) + +alias( + name = "multi_file_cel_java_proto_lite", + actual = "//testing/src/test/resources/protos:multi_file_cel_java_proto_lite", +) + +alias( + name = "test_all_types_cel_java_proto2_lite", + actual = "//testing/src/test/resources/protos:test_all_types_cel_java_proto2_lite", +) + +alias( + name = "test_all_types_cel_java_proto3_lite", + actual = "//testing/src/test/resources/protos:test_all_types_cel_java_proto3_lite", +) + +alias( + name = "test_all_types_cel_java_proto2", + actual = "//testing/src/test/resources/protos:test_all_types_cel_java_proto2", +) + +alias( + name = "test_all_types_cel_java_proto3", + actual = "//testing/src/test/resources/protos:test_all_types_cel_java_proto3", +) + +alias( + name = "message_with_enum_cel_java_proto", + actual = "//testing/src/test/resources/protos:message_with_enum_cel_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 f641dd56f..69765b549 100644 --- a/testing/src/main/java/dev/cel/testing/BUILD.bazel +++ b/testing/src/main/java/dev/cel/testing/BUILD.bazel @@ -1,17 +1,11 @@ +load("@rules_java//java:defs.bzl", "java_library") + package( default_applicable_licenses = ["//:license"], default_testonly = True, default_visibility = ["//testing:__pkg__"], ) -TEST_DECL_SOURCES = [ - "TestCelFunctionDeclWrapper.java", - "TestCelVariableDeclWrapper.java", - "TestDecl.java", - "TestProtoFunctionDeclWrapper.java", - "TestProtoVariableDeclWrapper.java", -] - java_library( name = "baseline_test_case", srcs = [ @@ -41,20 +35,7 @@ java_library( "CelDebug.java", ], deps = [ - "@cel_spec//proto/cel/expr:expr_java_proto", - "@maven//:com_google_guava_guava", - ], -) - -java_library( - name = "test_decls", - srcs = TEST_DECL_SOURCES, - deps = [ - "//common:compiler_common", - "//common/types:cel_types", - "//common/types:type_providers", - "//compiler:compiler_builder", - "@cel_spec//proto/cel/expr:expr_java_proto", + "@cel_spec//proto/cel/expr:syntax_java_proto", "@maven//:com_google_guava_guava", ], ) @@ -64,72 +45,18 @@ java_library( srcs = ["CelBaselineTestCase.java"], deps = [ ":baseline_test_case", - ":test_decls", "//:java_truth", - "//common", + "//common:cel_ast", "//common:compiler_common", + "//common:container", "//common:options", "//common/types:cel_types", "//common/types:message_type_provider", "//common/types:type_providers", "//compiler", "//compiler:compiler_builder", + "//extensions", "//parser:macro", - "@cel_spec//proto/cel/expr:expr_java_proto", - "@maven//:com_google_guava_guava", - "@maven//:com_google_protobuf_protobuf_java", - ], -) - -java_library( - name = "cel_value_sync", - testonly = 1, - srcs = ["EvalCelValueSync.java"], - deps = [ - ":eval", - "//common", - "//common:options", - "//common/internal:cel_descriptor_pools", - "//common/internal:default_message_factory", - "//common/internal:dynamic_proto", - "//common/internal:proto_message_factory", - "//common/values:cel_value_provider", - "//common/values:proto_message_value_provider", - "//runtime:interpreter", - "//runtime:runtime_type_provider_legacy", - "@maven//:com_google_guava_guava", - "@maven//:com_google_protobuf_protobuf_java", - ], -) - -java_library( - name = "sync", - testonly = 1, - srcs = ["EvalSync.java"], - deps = [ - ":eval", - "//common", - "//common:options", - "//common/internal:cel_descriptor_pools", - "//common/internal:default_message_factory", - "//runtime:interpreter", - "@maven//:com_google_guava_guava", - "@maven//:com_google_protobuf_protobuf_java", - ], -) - -java_library( - name = "eval", - testonly = 1, - srcs = [ - "Eval.java", - ], - deps = [ - "//common", - "//common:options", - "//runtime:base", - "//runtime:interpreter", - "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", ], @@ -141,18 +68,35 @@ java_library( srcs = [ "BaseInterpreterTest.java", ], - resources = ["//runtime/testdata"], + resources = [ + "//common/resources/testdata/proto3:test_all_types_file_descriptor_set", + "//runtime/testdata", + ], deps = [ ":cel_baseline_test_case", - ":eval", "//:java_truth", - "//common", + "//common:cel_ast", + "//common:container", + "//common:options", + "//common:proto_ast", "//common/internal:cel_descriptor_pools", + "//common/internal:file_descriptor_converter", + "//common/internal:proto_time_utils", "//common/resources/testdata/proto3:standalone_global_enum_java_proto", - "//common/resources/testdata/proto3:test_all_types_java_proto", - "//common/types:cel_types", - "//runtime:interpreter", - "@cel_spec//proto/cel/expr:expr_java_proto", + "//common/types", + "//common/types:message_type_provider", + "//common/types:type_providers", + "//common/values:cel_byte_string", + "//extensions", + "//extensions:optional_library", + "//runtime", + "//runtime:function_binding", + "//runtime:partial_vars", + "//runtime:unknown_attributes", + "@cel_spec//proto/cel/expr:checked_java_proto", + "@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_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", @@ -160,3 +104,13 @@ java_library( "@maven//:junit_junit", ], ) + +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 88a4fd8ba..bda56a19e 100644 --- a/testing/src/main/java/dev/cel/testing/BaseInterpreterTest.java +++ b/testing/src/main/java/dev/cel/testing/BaseInterpreterTest.java @@ -16,378 +16,534 @@ import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.truth.Truth.assertThat; +import static dev.cel.runtime.CelVariableResolver.hierarchicalVariableResolver; +import static java.nio.charset.StandardCharsets.UTF_8; import dev.cel.expr.CheckedExpr; -import dev.cel.expr.ExprValue; +import dev.cel.expr.ParsedExpr; import dev.cel.expr.Type; -import dev.cel.expr.Type.AbstractType; -import dev.cel.expr.UnknownSet; +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.common.primitives.UnsignedLong; import com.google.errorprone.annotations.CanIgnoreReturnValue; +import com.google.errorprone.annotations.Immutable; import com.google.protobuf.Any; import com.google.protobuf.BoolValue; import com.google.protobuf.ByteString; import com.google.protobuf.BytesValue; +import com.google.protobuf.DescriptorProtos.FileDescriptorSet; +import com.google.protobuf.Descriptors.Descriptor; import com.google.protobuf.Descriptors.FileDescriptor; import com.google.protobuf.DoubleValue; -import com.google.protobuf.Duration; 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; import com.google.protobuf.ListValue; +import com.google.protobuf.Message; import com.google.protobuf.NullValue; import com.google.protobuf.StringValue; import com.google.protobuf.Struct; +import com.google.protobuf.TextFormat; import com.google.protobuf.Timestamp; import com.google.protobuf.UInt32Value; import com.google.protobuf.UInt64Value; +import com.google.protobuf.UnredactedDebugFormatForTest; import com.google.protobuf.Value; -import com.google.protobuf.util.Durations; -import com.google.protobuf.util.Timestamps; +import com.google.protobuf.util.JsonFormat; import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelContainer; +import dev.cel.common.CelOptions; import dev.cel.common.CelProtoAbstractSyntaxTree; import dev.cel.common.internal.DefaultDescriptorPool; -import dev.cel.common.types.CelTypes; -import dev.cel.runtime.Activation; -import dev.cel.runtime.InterpreterException; -import dev.cel.runtime.PartialMessage; +import dev.cel.common.internal.FileDescriptorSetConverter; +import dev.cel.common.internal.ProtoTimeUtils; +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.OpaqueType; +import dev.cel.common.types.ProtoMessageTypeProvider; +import dev.cel.common.types.SimpleType; +import dev.cel.common.types.StructTypeReference; +import dev.cel.common.types.TypeParamType; +import dev.cel.common.values.CelByteString; +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; +import dev.cel.runtime.CelRuntime; +import dev.cel.runtime.CelRuntimeBuilder; +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 dev.cel.testing.testdata.proto3.TestAllTypesProto.TestAllTypes; -import dev.cel.testing.testdata.proto3.TestAllTypesProto.TestAllTypes.NestedEnum; -import dev.cel.testing.testdata.proto3.TestAllTypesProto.TestAllTypes.NestedMessage; +import java.io.IOException; +import java.time.Duration; +import java.time.Instant; import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.stream.LongStream; import org.junit.Test; /** Base class for evaluation outputs that can be stored and used as a baseline test. */ public abstract class BaseInterpreterTest extends CelBaselineTestCase { + protected static final Descriptor TEST_ALL_TYPE_DYNAMIC_DESCRIPTOR = + getDeserializedTestAllTypeDescriptor(); + protected static final ImmutableList TEST_FILE_DESCRIPTORS = ImmutableList.of( - TestAllTypes.getDescriptor().getFile(), StandaloneGlobalEnum.getDescriptor().getFile()); + dev.cel.expr.conformance.proto2.TestAllTypes.getDescriptor().getFile(), + TestAllTypes.getDescriptor().getFile(), + StandaloneGlobalEnum.getDescriptor().getFile(), + TEST_ALL_TYPE_DYNAMIC_DESCRIPTOR.getFile()); + + private static final CelOptions BASE_CEL_OPTIONS = + CelOptions.current() + .enableHeterogeneousNumericComparisons(true) + .enableOptionalSyntax(true) + .comprehensionMaxIterations(1_000) + .build(); + protected CelRuntime celRuntime; + private CelOptions celOptions; + + protected BaseInterpreterTest() { + this.celOptions = BASE_CEL_OPTIONS; + this.celRuntime = newBaseRuntimeBuilder(celOptions).build(); + } - private final Eval eval; + protected BaseInterpreterTest(CelRuntime celRuntime) { + this.celRuntime = celRuntime; + this.celOptions = BASE_CEL_OPTIONS; + } - public BaseInterpreterTest(boolean declareWithCelType, Eval eval) { - super(declareWithCelType); - this.eval = eval; + protected CelRuntimeBuilder newBaseRuntimeBuilder(CelOptions celOptions) { + return CelRuntimeFactory.standardCelRuntimeBuilder() + .addLibraries(CelOptionalLibrary.INSTANCE) + .addFileTypes(TEST_FILE_DESCRIPTORS) + .setOptions(celOptions); } - /** Helper to run a test for configured instance variables. */ - @CanIgnoreReturnValue // Test generates a file to diff against baseline. Ignoring Intermediary - // evaluation is not a concern. - private Object runTest(Activation activation) throws Exception { - CelAbstractSyntaxTree ast = prepareTest(eval.fileDescriptors()); + @Override + protected CelAbstractSyntaxTree prepareTest(List descriptors) { + return prepareTest( + ProtoMessageTypeProvider.newBuilder() + .addFileDescriptors(descriptors) + .setAllowJsonFieldNames(celOptions.enableJsonFieldNames()) + .build()); + } + + @Override + protected void prepareCompiler(CelTypeProvider typeProvider) { + super.prepareCompiler(typeProvider); + this.celCompiler = + celCompiler + .toCompilerBuilder() + .addLibraries(CelOptionalLibrary.INSTANCE, CelExtensions.bindings()) + .setOptions(celOptions) + .build(); + } + + protected void setContainer(CelContainer container) { + this.container = container; + } + + private CelAbstractSyntaxTree compileTestCase() { + CelAbstractSyntaxTree ast = prepareTest(TEST_FILE_DESCRIPTORS); if (ast == null) { return null; } assertAstRoundTrip(ast); - testOutput().println("bindings: " + activation); + return ast; + } + + @CanIgnoreReturnValue + private Object runTest() { + return runTest(ImmutableMap.of()); + } + + @CanIgnoreReturnValue + private Object runTest(CelVariableResolver variableResolver) { + return runTestInternal(variableResolver, Optional.empty()); + } + + /** Helper to run a test for configured instance variables. */ + @CanIgnoreReturnValue + private Object runTest(Map input) { + return runTestInternal(input, Optional.empty()); + } + + /** Helper to run a test for configured instance variables. */ + @CanIgnoreReturnValue + private Object runTest(Map input, CelLateFunctionBindings lateFunctionBindings) { + 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}. + */ + 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, patterns); Object result = null; try { - result = eval.eval(ast, activation); - if (result instanceof ByteString) { + CelRuntime.Program program = celRuntime.createProgram(ast); + 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)); + result = program.eval(variableResolver, lateFunctionBindings.get()); + } else { + result = program.eval((CelVariableResolver) input, lateFunctionBindings.get()); + } + } else { + result = + input instanceof Map + ? program.eval(((Map) input)) + : program.eval((CelVariableResolver) input); + } + if (result instanceof CelByteString) { // Note: this call may fail for printing byte sequences that are not valid UTF-8, but works // pretty well for test purposes. - result = ((ByteString) result).toStringUtf8(); + result = ((CelByteString) result).toStringUtf8(); } - testOutput().println("result: " + result); - } catch (InterpreterException e) { - testOutput().println("error: " + e.getMessage()); - testOutput().println("error_code: " + e.getErrorCode()); + println("result: " + UnredactedDebugFormatForTest.unredactedToString(result)); + } catch (CelEvaluationException e) { + println("error: " + e.getMessage()); + println("error_code: " + e.getErrorCode()); } - testOutput().println(); + println(""); return result; } - /** - * Checks that the CheckedExpr produced by CelCompiler is equal to the one reproduced by the - * native CelAbstractSyntaxTree - */ - private void assertAstRoundTrip(CelAbstractSyntaxTree ast) { - CheckedExpr checkedExpr = CelProtoAbstractSyntaxTree.fromCelAst(ast).toCheckedExpr(); - CelProtoAbstractSyntaxTree protoAst = CelProtoAbstractSyntaxTree.fromCelAst(ast); - assertThat(checkedExpr).isEqualTo(protoAst.toCheckedExpr()); - } - @Test - public void arithmInt64() throws Exception { + public void arithmInt64() { source = "1 < 2 && 1 <= 1 && 2 > 1 && 1 >= 1 && 1 == 1 && 2 != 1"; - runTest(Activation.EMPTY); + runTest(); - declareVariable("x", CelTypes.INT64); + declareVariable("x", SimpleType.INT); source = "1 + 2 - x * 3 / x + (x % 3)"; - runTest(Activation.of("x", -5L)); + runTest(ImmutableMap.of("x", -5L)); - declareVariable("y", CelTypes.DYN); + declareVariable("y", SimpleType.DYN); source = "x + y == 1"; - runTest(Activation.of("x", -5L).extend(Activation.of("y", 6))); + runTest(extend(ImmutableMap.of("x", -5L), ImmutableMap.of("y", 6L))); } @Test - public void arithmInt64_error() throws Exception { + public void arithmInt64_error() { source = "9223372036854775807 + 1"; - runTest(Activation.EMPTY); + runTest(); source = "-9223372036854775808 - 1"; - runTest(Activation.EMPTY); + runTest(); source = "-(-9223372036854775808)"; - runTest(Activation.EMPTY); + runTest(); source = "5000000000 * 5000000000"; - runTest(Activation.EMPTY); + runTest(); source = "(-9223372036854775808)/-1"; - runTest(Activation.EMPTY); + runTest(); source = "1 / 0"; - runTest(Activation.EMPTY); + runTest(); source = "1 % 0"; - runTest(Activation.EMPTY); + runTest(); } @Test - public void arithmUInt64() throws Exception { + public void arithmUInt64() { source = "1u < 2u && 1u <= 1u && 2u > 1u && 1u >= 1u && 1u == 1u && 2u != 1u"; - runTest(Activation.EMPTY); + runTest(); - boolean useUnsignedLongs = eval.celOptions().enableUnsignedLongs(); - declareVariable("x", CelTypes.UINT64); + boolean useUnsignedLongs = BASE_CEL_OPTIONS.enableUnsignedLongs(); + declareVariable("x", SimpleType.UINT); source = "1u + 2u + x * 3u / x + (x % 3u)"; - runTest(Activation.of("x", useUnsignedLongs ? UnsignedLong.valueOf(5L) : 5L)); + runTest(ImmutableMap.of("x", useUnsignedLongs ? UnsignedLong.valueOf(5L) : 5L)); - declareVariable("y", CelTypes.DYN); + declareVariable("y", SimpleType.DYN); source = "x + y == 11u"; runTest( - Activation.of("x", useUnsignedLongs ? UnsignedLong.valueOf(5L) : 5L) - .extend(Activation.of("y", useUnsignedLongs ? UnsignedLong.valueOf(6L) : 6))); + extend( + ImmutableMap.of("x", useUnsignedLongs ? UnsignedLong.valueOf(5L) : 5L), + ImmutableMap.of("y", useUnsignedLongs ? UnsignedLong.valueOf(6L) : 6))); source = "x - y == 1u"; runTest( - Activation.of("x", useUnsignedLongs ? UnsignedLong.valueOf(6L) : 6L) - .extend(Activation.of("y", useUnsignedLongs ? UnsignedLong.valueOf(5) : 5))); + extend( + ImmutableMap.of("x", useUnsignedLongs ? UnsignedLong.valueOf(6L) : 6L), + ImmutableMap.of("y", useUnsignedLongs ? UnsignedLong.valueOf(5) : 5))); } @Test - public void arithmUInt64_error() throws Exception { + public void arithmUInt64_error() { source = "18446744073709551615u + 1u"; - runTest(Activation.EMPTY); + runTest(); source = "0u - 1u"; - runTest(Activation.EMPTY); + runTest(); source = "5000000000u * 5000000000u"; - runTest(Activation.EMPTY); + runTest(); source = "1u / 0u"; - runTest(Activation.EMPTY); + runTest(); source = "1u % 0u"; - runTest(Activation.EMPTY); + runTest(); } @Test - public void arithmDouble() throws Exception { + public void arithmDouble() { + source = "0.0 == -0.0"; + runTest(); + source = "1.9 < 2.0 && 1.1 <= 1.1 && 2.0 > 1.9 && 1.1 >= 1.1 && 1.1 == 1.1 && 2.0 != 1.9"; - runTest(Activation.EMPTY); + runTest(); - declareVariable("x", CelTypes.DOUBLE); + declareVariable("x", SimpleType.DOUBLE); source = "1.0 + 2.3 + x * 3.0 / x"; - runTest(Activation.of("x", 3.33)); + runTest(ImmutableMap.of("x", 3.33)); - declareVariable("y", CelTypes.DYN); + declareVariable("y", SimpleType.DYN); source = "x + y == 9.99"; - runTest(Activation.of("x", 3.33d).extend(Activation.of("y", 6.66))); + runTest(extend(ImmutableMap.of("x", 3.33d), ImmutableMap.of("y", 6.66))); } @Test - public void quantifiers() throws Exception { + public void quantifiers() { source = "[1,-2,3].exists_one(x, x > 0)"; - runTest(Activation.EMPTY); + runTest(); source = "[-1,-2,3].exists_one(x, x > 0)"; - runTest(Activation.EMPTY); + runTest(); source = "[-1,-2,-3].exists(x, x > 0)"; - runTest(Activation.EMPTY); + runTest(); source = "[1,-2,3].exists(x, x > 0)"; - runTest(Activation.EMPTY); + runTest(); source = "[1,-2,3].all(x, x > 0)"; - runTest(Activation.EMPTY); + runTest(); source = "[1,2,3].all(x, x > 0)"; - runTest(Activation.EMPTY); + runTest(); } @Test - public void arithmTimestamp() throws Exception { - container = Type.getDescriptor().getFile().getPackage(); - declareVariable("ts1", CelTypes.TIMESTAMP); - declareVariable("ts2", CelTypes.TIMESTAMP); - declareVariable("d1", CelTypes.DURATION); - Duration d1 = Duration.newBuilder().setSeconds(15).setNanos(25).build(); - Timestamp ts1 = Timestamp.newBuilder().setSeconds(25).setNanos(35).build(); - Timestamp ts2 = Timestamp.newBuilder().setSeconds(10).setNanos(10).build(); - Activation activation = - Activation.of("d1", d1).extend(Activation.of("ts1", ts1)).extend(Activation.of("ts2", ts2)); + public void arithmTimestamp() { + setContainer(CelContainer.ofName(Type.getDescriptor().getFile().getPackage())); + declareVariable("ts1", SimpleType.TIMESTAMP); + declareVariable("ts2", SimpleType.TIMESTAMP); + declareVariable("d1", SimpleType.DURATION); + Duration d1 = Duration.ofSeconds(15, 25); + Instant ts1 = Instant.ofEpochSecond(25, 35); + Instant ts2 = Instant.ofEpochSecond(10, 10); + CelVariableResolver resolver = + extend( + extend(ImmutableMap.of("d1", d1), ImmutableMap.of("ts1", ts1)), + ImmutableMap.of("ts2", ts2)); source = "ts1 - ts2 == d1"; - runTest(activation); + runTest(resolver); source = "ts1 - d1 == ts2"; - runTest(activation); + runTest(resolver); source = "ts2 + d1 == ts1"; - runTest(activation); + runTest(resolver); source = "d1 + ts2 == ts1"; - runTest(activation); + runTest(resolver); } @Test - public void arithmDuration() throws Exception { - container = Type.getDescriptor().getFile().getPackage(); - declareVariable("d1", CelTypes.DURATION); - declareVariable("d2", CelTypes.DURATION); - declareVariable("d3", CelTypes.DURATION); - Duration d1 = Duration.newBuilder().setSeconds(15).setNanos(25).build(); - Duration d2 = Duration.newBuilder().setSeconds(10).setNanos(20).build(); - Duration d3 = Duration.newBuilder().setSeconds(25).setNanos(45).build(); - Activation activation = - Activation.of("d1", d1).extend(Activation.of("d2", d2)).extend(Activation.of("d3", d3)); + public void arithmDuration() { + setContainer(CelContainer.ofName(Type.getDescriptor().getFile().getPackage())); + declareVariable("d1", SimpleType.DURATION); + declareVariable("d2", SimpleType.DURATION); + declareVariable("d3", SimpleType.DURATION); + java.time.Duration d1 = java.time.Duration.ofSeconds(15, 25); + java.time.Duration d2 = java.time.Duration.ofSeconds(10, 20); + java.time.Duration d3 = java.time.Duration.ofSeconds(25, 45); + + CelVariableResolver resolver = + extend( + extend(ImmutableMap.of("d1", d1), ImmutableMap.of("d2", d2)), + ImmutableMap.of("d3", d3)); source = "d1 + d2 == d3"; - runTest(activation); + runTest(resolver); source = "d3 - d1 == d2"; - runTest(activation); + runTest(resolver); } @Test - public void arithCrossNumericTypes() throws Exception { - if (!eval.celOptions().enableUnsignedLongs()) { + public void arithCrossNumericTypes() { + if (!BASE_CEL_OPTIONS.enableUnsignedLongs()) { skipBaselineVerification(); return; } source = "1.9 < 2 && 1 < 1.1 && 2u < 2.9 && 1.1 < 2u && 1 < 2u && 2u < 3"; - runTest(Activation.EMPTY); + runTest(); source = "1.9 <= 2 && 1 <= 1.1 && 2u <= 2.9 && 1.1 <= 2u && 2 <= 2u && 2u <= 2"; - runTest(Activation.EMPTY); + runTest(); source = "1.9 > 2 && 1 > 1.1 && 2u > 2.9 && 1.1 > 2u && 2 > 2u && 2u > 2"; - runTest(Activation.EMPTY); + runTest(); source = "1.9 >= 2 && 1 >= 1.1 && 2u >= 2.9 && 1.1 >= 2u && 2 >= 2u && 2u >= 2"; - runTest(Activation.EMPTY); + runTest(); } @Test - public void booleans() throws Exception { - declareVariable("x", CelTypes.BOOL); + public void booleans() { + declareVariable("x", SimpleType.BOOL); source = "x ? 1 : 0"; - runTest(Activation.of("x", true)); - runTest(Activation.of("x", false)); + runTest(ImmutableMap.of("x", true)); + runTest(ImmutableMap.of("x", false)); source = "(1 / 0 == 0 && false) == (false && 1 / 0 == 0)"; - runTest(Activation.EMPTY); + runTest(); source = "(1 / 0 == 0 || true) == (true || 1 / 0 == 0)"; - runTest(Activation.EMPTY); + runTest(); - declareVariable("y", CelTypes.INT64); + declareVariable("y", SimpleType.INT); source = "1 / y == 1 || true"; - runTest(Activation.of("y", 0L)); - - source = "1 / y == 1 || false"; - runTest(Activation.of("y", 0L)); - - source = "false || 1 / y == 1"; - runTest(Activation.of("y", 0L)); - - source = "1 / y == 1 && true"; - runTest(Activation.of("y", 0L)); - - source = "true && 1 / y == 1"; - runTest(Activation.of("y", 0L)); + runTest(ImmutableMap.of("y", 0L)); source = "1 / y == 1 && false"; - runTest(Activation.of("y", 0L)); + runTest(ImmutableMap.of("y", 0L)); source = "(true > false) == true"; - runTest(Activation.EMPTY); + runTest(); source = "(true > true) == false"; - runTest(Activation.EMPTY); + runTest(); source = "(false > true) == false"; - runTest(Activation.EMPTY); + runTest(); source = "(false > false) == false"; - runTest(Activation.EMPTY); + runTest(); source = "(true >= false) == true"; - runTest(Activation.EMPTY); + runTest(); source = "(true >= true) == true"; - runTest(Activation.EMPTY); + runTest(); source = "(false >= false) == true"; - runTest(Activation.EMPTY); + runTest(); source = "(false >= true) == false"; - runTest(Activation.EMPTY); + runTest(); source = "(false < true) == true"; - runTest(Activation.EMPTY); + runTest(); source = "(false < false) == false"; - runTest(Activation.EMPTY); + runTest(); source = "(true < false) == false"; - runTest(Activation.EMPTY); + runTest(); source = "(true < true) == false"; - runTest(Activation.EMPTY); + runTest(); source = "(false <= true) == true"; - runTest(Activation.EMPTY); + runTest(); source = "(false <= false) == true"; - runTest(Activation.EMPTY); + runTest(); source = "(true <= false) == false"; - runTest(Activation.EMPTY); + runTest(); source = "(true <= true) == true"; - runTest(Activation.EMPTY); + runTest(); + } + + @Test + public void booleans_error() { + declareVariable("y", SimpleType.INT); + + source = "1 / y == 1 || false"; + runTest(ImmutableMap.of("y", 0L)); + + source = "false || 1 / y == 1"; + runTest(ImmutableMap.of("y", 0L)); + + source = "1 / y == 1 && true"; + runTest(ImmutableMap.of("y", 0L)); + + source = "true && 1 / y == 1"; + runTest(ImmutableMap.of("y", 0L)); } @Test public void strings() throws Exception { source = "'a' < 'b' && 'a' <= 'b' && 'b' > 'a' && 'a' >= 'a' && 'a' == 'a' && 'a' != 'b'"; - runTest(Activation.EMPTY); + runTest(); - declareVariable("x", CelTypes.STRING); + declareVariable("x", SimpleType.STRING); source = "'abc' + x == 'abcdef' && " + "x.endsWith('ef') && " + "x.startsWith('d') && " + "x.contains('de') && " + "!x.contains('abcdef')"; - runTest(Activation.of("x", "def")); + runTest(ImmutableMap.of("x", "def")); } @Test @@ -396,29 +552,67 @@ public void messages() throws Exception { TestAllTypes.newBuilder() .setSingleNestedMessage(NestedMessage.newBuilder().setBb(43)) .build(); - declareVariable("x", CelTypes.createMessage(TestAllTypes.getDescriptor().getFullName())); + declareVariable("x", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())); source = "x.single_nested_message.bb == 43 && has(x.single_nested_message)"; - runTest(Activation.of("x", nestedMessage)); + runTest(ImmutableMap.of("x", nestedMessage)); declareVariable( "single_nested_message", - CelTypes.createMessage(NestedMessage.getDescriptor().getFullName())); + StructTypeReference.create(NestedMessage.getDescriptor().getFullName())); source = "single_nested_message.bb == 43"; - runTest(Activation.of("single_nested_message", nestedMessage.getSingleNestedMessage())); + runTest(ImmutableMap.of("single_nested_message", nestedMessage.getSingleNestedMessage())); source = "TestAllTypes{single_int64: 1, single_sfixed64: 2, single_int32: 2}.single_int32 == 2"; - container = TestAllTypes.getDescriptor().getFile().getPackage(); - runTest(Activation.EMPTY); + setContainer(CelContainer.ofName(TestAllTypes.getDescriptor().getFile().getPackage())); + runTest(); } @Test - public void messages_error() throws Exception { + public void messages_error() { source = "TestAllTypes{single_int32_wrapper: 12345678900}"; - container = TestAllTypes.getDescriptor().getFile().getPackage(); - runTest(Activation.EMPTY); + setContainer(CelContainer.ofName(TestAllTypes.getDescriptor().getFile().getPackage())); + runTest(); source = "TestAllTypes{}.map_string_string.a"; - runTest(Activation.EMPTY); + runTest(); + } + + @Test + public void optional() { + // TODO: Move existing optional tests here to also test CelValue runtime + source = "optional.unwrap([])"; + runTest(); + + declareVariable("str", SimpleType.STRING); + source = "optional.unwrap([optional.none(), optional.of(1), optional.of(str)])"; + runTest(ImmutableMap.of("str", "foo")); + } + + @Test + public void optional_errors() { + source = "optional.unwrap([dyn(1)])"; + runTest(); + } + + @Test + public void containers() { + setContainer( + CelContainer.newBuilder() + .setName("dev.cel.testing.testdata.proto3.StandaloneGlobalEnum") + .addAlias("test_alias", TestAllTypes.getDescriptor().getFile().getPackage()) + .addAbbreviations("cel.expr.conformance.proto2", "cel.expr.conformance.proto3") + .build()); + source = "test_alias.TestAllTypes{} == cel.expr.conformance.proto3.TestAllTypes{}"; + runTest(); + + source = "proto2.TestAllTypes{} == cel.expr.conformance.proto2.TestAllTypes{}"; + runTest(); + + source = "proto3.TestAllTypes{} == cel.expr.conformance.proto3.TestAllTypes{}"; + runTest(); + + source = "SGAR"; // From StandaloneGlobalEnum + runTest(); } @Test @@ -435,7 +629,7 @@ public void has() throws Exception { .putMapInt32Int64(1, 2L) .setSingleNestedMessage(NestedMessage.newBuilder().setBb(43)) .build(); - declareVariable("x", CelTypes.createMessage(TestAllTypes.getDescriptor().getFullName())); + declareVariable("x", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())); source = "has(x.single_int32) && !has(x.single_int64) && has(x.single_bool_wrapper)" + " && has(x.single_int32_wrapper) && !has(x.single_int64_wrapper)" @@ -444,1137 +638,1136 @@ public void has() throws Exception { + " && has(x.oneof_bool) && !has(x.oneof_type)" + " && has(x.map_int32_int64) && !has(x.map_string_string)" + " && has(x.single_nested_message) && !has(x.single_duration)"; - runTest(Activation.of("x", nestedMessage)); + runTest(ImmutableMap.of("x", nestedMessage)); } @Test public void duration() throws Exception { - declareVariable("d1", CelTypes.DURATION); - declareVariable("d2", CelTypes.DURATION); - Duration d1010 = Duration.newBuilder().setSeconds(10).setNanos(10).build(); - Duration d1009 = Duration.newBuilder().setSeconds(10).setNanos(9).build(); - Duration d0910 = Duration.newBuilder().setSeconds(9).setNanos(10).build(); - container = Type.getDescriptor().getFile().getPackage(); + declareVariable("d1", SimpleType.DURATION); + declareVariable("d2", SimpleType.DURATION); + java.time.Duration d1010 = java.time.Duration.ofSeconds(10, 10); + java.time.Duration d1009 = java.time.Duration.ofSeconds(10, 9); + java.time.Duration d0910 = java.time.Duration.ofSeconds(9, 10); + setContainer(CelContainer.ofName(Type.getDescriptor().getFile().getPackage())); source = "d1 < d2"; - runTest(Activation.of("d1", d1010).extend(Activation.of("d2", d1009))); - runTest(Activation.of("d1", d1010).extend(Activation.of("d2", d0910))); - runTest(Activation.of("d1", d1010).extend(Activation.of("d2", d1010))); - runTest(Activation.of("d1", d1009).extend(Activation.of("d2", d1010))); - runTest(Activation.of("d1", d0910).extend(Activation.of("d2", d1010))); + runTest(extend(ImmutableMap.of("d1", d1010), ImmutableMap.of("d2", d1009))); + runTest(extend(ImmutableMap.of("d1", d1010), ImmutableMap.of("d2", d0910))); + runTest(extend(ImmutableMap.of("d1", d1010), ImmutableMap.of("d2", d1010))); + runTest(extend(ImmutableMap.of("d1", d1009), ImmutableMap.of("d2", d1010))); + runTest(extend(ImmutableMap.of("d1", d0910), ImmutableMap.of("d2", d1010))); source = "d1 <= d2"; - runTest(Activation.of("d1", d1010).extend(Activation.of("d2", d1009))); - runTest(Activation.of("d1", d1010).extend(Activation.of("d2", d0910))); - runTest(Activation.of("d1", d1010).extend(Activation.of("d2", d1010))); - runTest(Activation.of("d1", d1009).extend(Activation.of("d2", d1010))); - runTest(Activation.of("d1", d0910).extend(Activation.of("d2", d1010))); + runTest(extend(ImmutableMap.of("d1", d1010), ImmutableMap.of("d2", d1009))); + runTest(extend(ImmutableMap.of("d1", d1010), ImmutableMap.of("d2", d0910))); + runTest(extend(ImmutableMap.of("d1", d1010), ImmutableMap.of("d2", d1010))); + runTest(extend(ImmutableMap.of("d1", d1009), ImmutableMap.of("d2", d1010))); + runTest(extend(ImmutableMap.of("d1", d0910), ImmutableMap.of("d2", d1010))); source = "d1 > d2"; - runTest(Activation.of("d1", d1010).extend(Activation.of("d2", d1009))); - runTest(Activation.of("d1", d1010).extend(Activation.of("d2", d0910))); - runTest(Activation.of("d1", d1010).extend(Activation.of("d2", d1010))); - runTest(Activation.of("d1", d1009).extend(Activation.of("d2", d1010))); - runTest(Activation.of("d1", d0910).extend(Activation.of("d2", d1010))); + runTest(extend(ImmutableMap.of("d1", d1010), ImmutableMap.of("d2", d1009))); + runTest(extend(ImmutableMap.of("d1", d1010), ImmutableMap.of("d2", d0910))); + runTest(extend(ImmutableMap.of("d1", d1010), ImmutableMap.of("d2", d1010))); + runTest(extend(ImmutableMap.of("d1", d1009), ImmutableMap.of("d2", d1010))); + runTest(extend(ImmutableMap.of("d1", d0910), ImmutableMap.of("d2", d1010))); source = "d1 >= d2"; - runTest(Activation.of("d1", d1010).extend(Activation.of("d2", d1009))); - runTest(Activation.of("d1", d1010).extend(Activation.of("d2", d0910))); - runTest(Activation.of("d1", d1010).extend(Activation.of("d2", d1010))); - runTest(Activation.of("d1", d1009).extend(Activation.of("d2", d1010))); - runTest(Activation.of("d1", d0910).extend(Activation.of("d2", d1010))); + runTest(extend(ImmutableMap.of("d1", d1010), ImmutableMap.of("d2", d1009))); + runTest(extend(ImmutableMap.of("d1", d1010), ImmutableMap.of("d2", d0910))); + runTest(extend(ImmutableMap.of("d1", d1010), ImmutableMap.of("d2", d1010))); + runTest(extend(ImmutableMap.of("d1", d1009), ImmutableMap.of("d2", d1010))); + runTest(extend(ImmutableMap.of("d1", d0910), ImmutableMap.of("d2", d1010))); } @Test public void timestamp() throws Exception { - declareVariable("t1", CelTypes.TIMESTAMP); - declareVariable("t2", CelTypes.TIMESTAMP); - Timestamp ts1010 = Timestamp.newBuilder().setSeconds(10).setNanos(10).build(); - Timestamp ts1009 = Timestamp.newBuilder().setSeconds(10).setNanos(9).build(); - Timestamp ts0910 = Timestamp.newBuilder().setSeconds(9).setNanos(10).build(); - container = Type.getDescriptor().getFile().getPackage(); + declareVariable("t1", SimpleType.TIMESTAMP); + declareVariable("t2", SimpleType.TIMESTAMP); + Instant ts1010 = Instant.ofEpochSecond(10, 10); + Instant ts1009 = Instant.ofEpochSecond(10, 9); + Instant ts0910 = Instant.ofEpochSecond(9, 10); + setContainer(CelContainer.ofName(Type.getDescriptor().getFile().getPackage())); source = "t1 < t2"; - runTest(Activation.of("t1", ts1010).extend(Activation.of("t2", ts1009))); - runTest(Activation.of("t1", ts1010).extend(Activation.of("t2", ts0910))); - runTest(Activation.of("t1", ts1010).extend(Activation.of("t2", ts1010))); - runTest(Activation.of("t1", ts1009).extend(Activation.of("t2", ts1010))); - runTest(Activation.of("t1", ts0910).extend(Activation.of("t2", ts1010))); + runTest(extend(ImmutableMap.of("t1", ts1010), ImmutableMap.of("t2", ts1009))); + runTest(extend(ImmutableMap.of("t1", ts1010), ImmutableMap.of("t2", ts0910))); + runTest(extend(ImmutableMap.of("t1", ts1010), ImmutableMap.of("t2", ts1010))); + runTest(extend(ImmutableMap.of("t1", ts1009), ImmutableMap.of("t2", ts1010))); + runTest(extend(ImmutableMap.of("t1", ts0910), ImmutableMap.of("t2", ts1010))); source = "t1 <= t2"; - runTest(Activation.of("t1", ts1010).extend(Activation.of("t2", ts1009))); - runTest(Activation.of("t1", ts1010).extend(Activation.of("t2", ts0910))); - runTest(Activation.of("t1", ts1010).extend(Activation.of("t2", ts1010))); - runTest(Activation.of("t1", ts1009).extend(Activation.of("t2", ts1010))); - runTest(Activation.of("t1", ts0910).extend(Activation.of("t2", ts1010))); + runTest(extend(ImmutableMap.of("t1", ts1010), ImmutableMap.of("t2", ts1009))); + runTest(extend(ImmutableMap.of("t1", ts1010), ImmutableMap.of("t2", ts0910))); + runTest(extend(ImmutableMap.of("t1", ts1010), ImmutableMap.of("t2", ts1010))); + runTest(extend(ImmutableMap.of("t1", ts1009), ImmutableMap.of("t2", ts1010))); + runTest(extend(ImmutableMap.of("t1", ts0910), ImmutableMap.of("t2", ts1010))); source = "t1 > t2"; - runTest(Activation.of("t1", ts1010).extend(Activation.of("t2", ts1009))); - runTest(Activation.of("t1", ts1010).extend(Activation.of("t2", ts0910))); - runTest(Activation.of("t1", ts1010).extend(Activation.of("t2", ts1010))); - runTest(Activation.of("t1", ts1009).extend(Activation.of("t2", ts1010))); - runTest(Activation.of("t1", ts0910).extend(Activation.of("t2", ts1010))); + runTest(extend(ImmutableMap.of("t1", ts1010), ImmutableMap.of("t2", ts1009))); + runTest(extend(ImmutableMap.of("t1", ts1010), ImmutableMap.of("t2", ts0910))); + runTest(extend(ImmutableMap.of("t1", ts1010), ImmutableMap.of("t2", ts1010))); + runTest(extend(ImmutableMap.of("t1", ts1009), ImmutableMap.of("t2", ts1010))); + runTest(extend(ImmutableMap.of("t1", ts0910), ImmutableMap.of("t2", ts1010))); source = "t1 >= t2"; - runTest(Activation.of("t1", ts1010).extend(Activation.of("t2", ts1009))); - runTest(Activation.of("t1", ts1010).extend(Activation.of("t2", ts0910))); - runTest(Activation.of("t1", ts1010).extend(Activation.of("t2", ts1010))); - runTest(Activation.of("t1", ts1009).extend(Activation.of("t2", ts1010))); - runTest(Activation.of("t1", ts0910).extend(Activation.of("t2", ts1010))); + runTest(extend(ImmutableMap.of("t1", ts1010), ImmutableMap.of("t2", ts1009))); + runTest(extend(ImmutableMap.of("t1", ts1010), ImmutableMap.of("t2", ts0910))); + runTest(extend(ImmutableMap.of("t1", ts1010), ImmutableMap.of("t2", ts1010))); + runTest(extend(ImmutableMap.of("t1", ts1009), ImmutableMap.of("t2", ts1010))); + runTest(extend(ImmutableMap.of("t1", ts0910), ImmutableMap.of("t2", ts1010))); } @Test - // TODO: Support JSON type pack/unpack google.protobuf.Any. - public void packUnpackAny() throws Exception { + public void packUnpackAny() { // The use of long values results in the incorrect type being serialized for a uint value. - if (!eval.celOptions().enableUnsignedLongs()) { + if (!BASE_CEL_OPTIONS.enableUnsignedLongs()) { skipBaselineVerification(); return; } - container = TestAllTypes.getDescriptor().getFile().getPackage(); - declareVariable("any", CelTypes.ANY); - declareVariable("d", CelTypes.DURATION); - declareVariable("message", CelTypes.createMessage(TestAllTypes.getDescriptor().getFullName())); - Duration duration = Durations.fromSeconds(100); + setContainer(CelContainer.ofName(TestAllTypes.getDescriptor().getFile().getPackage())); + declareVariable("any", SimpleType.ANY); + declareVariable("d", SimpleType.DURATION); + declareVariable( + "message", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())); + declareVariable("list", ListType.create(SimpleType.DYN)); + com.google.protobuf.Duration duration = ProtoTimeUtils.fromSecondsToDuration(100); Any any = Any.pack(duration); TestAllTypes message = TestAllTypes.newBuilder().setSingleAny(any).build(); // unpack any source = "any == d"; - runTest(Activation.of("any", any).extend(Activation.of("d", duration))); + runTest(extend(ImmutableMap.of("any", any), ImmutableMap.of("d", duration))); source = "any == message.single_any"; - runTest(Activation.of("any", any).extend(Activation.of("message", message))); + runTest(extend(ImmutableMap.of("any", any), ImmutableMap.of("message", message))); source = "d == message.single_any"; - runTest(Activation.of("d", duration).extend(Activation.of("message", message))); + runTest(extend(ImmutableMap.of("d", duration), ImmutableMap.of("message", message))); source = "any.single_int64 == 1"; - runTest(Activation.of("any", TestAllTypes.newBuilder().setSingleInt64(1).build())); + runTest(ImmutableMap.of("any", TestAllTypes.newBuilder().setSingleInt64(1).build())); source = "any == 1"; - runTest(Activation.of("any", Any.pack(Int64Value.of(1)))); + runTest(ImmutableMap.of("any", Any.pack(Int64Value.of(1)))); + source = "list[0] == message"; + runTest(ImmutableMap.of("list", ImmutableList.of(Any.pack(message)), "message", message)); // pack any source = "TestAllTypes{single_any: d}"; - runTest(Activation.of("d", duration)); + runTest(ImmutableMap.of("d", duration)); source = "TestAllTypes{single_any: message.single_int64}"; - runTest(Activation.of("message", TestAllTypes.newBuilder().setSingleInt64(-1).build())); + runTest(ImmutableMap.of("message", TestAllTypes.newBuilder().setSingleInt64(-1).build())); source = "TestAllTypes{single_any: message.single_uint64}"; - runTest(Activation.of("message", TestAllTypes.newBuilder().setSingleUint64(1).build())); + runTest(ImmutableMap.of("message", TestAllTypes.newBuilder().setSingleUint64(1).build())); source = "TestAllTypes{single_any: 1.0}"; - runTest(Activation.EMPTY); + runTest(); source = "TestAllTypes{single_any: true}"; - runTest(Activation.EMPTY); + runTest(); source = "TestAllTypes{single_any: \"happy\"}"; - runTest(Activation.EMPTY); + runTest(); source = "TestAllTypes{single_any: message.single_bytes}"; runTest( - Activation.of( + ImmutableMap.of( "message", TestAllTypes.newBuilder().setSingleBytes(ByteString.copyFromUtf8("happy")).build())); } @Test - public void nestedEnums() throws Exception { + public void nestedEnums() { TestAllTypes nestedEnum = TestAllTypes.newBuilder().setSingleNestedEnum(NestedEnum.BAR).build(); - declareVariable("x", CelTypes.createMessage(TestAllTypes.getDescriptor().getFullName())); - container = TestAllTypes.getDescriptor().getFile().getPackage(); + declareVariable("x", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())); + setContainer(CelContainer.ofName(TestAllTypes.getDescriptor().getFile().getPackage())); source = "x.single_nested_enum == TestAllTypes.NestedEnum.BAR"; - runTest(Activation.of("x", nestedEnum)); + runTest(ImmutableMap.of("x", nestedEnum)); - declareVariable("single_nested_enum", CelTypes.INT64); + declareVariable("single_nested_enum", SimpleType.INT); source = "single_nested_enum == TestAllTypes.NestedEnum.BAR"; - runTest(Activation.of("single_nested_enum", nestedEnum.getSingleNestedEnumValue())); + runTest(ImmutableMap.of("single_nested_enum", nestedEnum.getSingleNestedEnumValue())); source = "TestAllTypes{single_nested_enum : TestAllTypes.NestedEnum.BAR}.single_nested_enum == 1"; - runTest(Activation.EMPTY); + runTest(); } @Test - public void globalEnums() throws Exception { - declareVariable("x", CelTypes.INT64); + public void globalEnums() { + declareVariable("x", SimpleType.INT); source = "x == dev.cel.testing.testdata.proto3.StandaloneGlobalEnum.SGAR"; - runTest(Activation.of("x", StandaloneGlobalEnum.SGAR.getNumber())); + runTest(ImmutableMap.of("x", StandaloneGlobalEnum.SGAR.getNumber())); } @Test public void lists() throws Exception { - declareVariable("x", CelTypes.createMessage(TestAllTypes.getDescriptor().getFullName())); - declareVariable("y", CelTypes.INT64); - container = TestAllTypes.getDescriptor().getFile().getPackage(); + declareVariable("x", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())); + declareVariable("y", SimpleType.INT); + setContainer(CelContainer.ofName(TestAllTypes.getDescriptor().getFile().getPackage())); source = "([1, 2, 3] + x.repeated_int32)[3] == 4"; - runTest(Activation.of("x", TestAllTypes.newBuilder().addRepeatedInt32(4).build())); + runTest(ImmutableMap.of("x", TestAllTypes.newBuilder().addRepeatedInt32(4).build())); source = "!(y in [1, 2, 3]) && y in [4, 5, 6]"; - runTest(Activation.of("y", 4L)); + runTest(ImmutableMap.of("y", 4L)); source = "TestAllTypes{repeated_int32: [1,2]}.repeated_int32[1] == 2"; - runTest(Activation.EMPTY); + runTest(); source = "1 in TestAllTypes{repeated_int32: [1,2]}.repeated_int32"; - runTest(Activation.EMPTY); + runTest(); source = "!(4 in [1, 2, 3]) && 1 in [1, 2, 3]"; - runTest(Activation.EMPTY); + runTest(); - declareVariable("list", CelTypes.createList(CelTypes.INT64)); + declareVariable("list", ListType.create(SimpleType.INT)); source = "!(4 in list) && 1 in list"; - runTest(Activation.of("list", ImmutableList.of(1L, 2L, 3L))); + runTest(ImmutableMap.of("list", ImmutableList.of(1L, 2L, 3L))); source = "!(y in list)"; - runTest(Activation.copyOf(ImmutableMap.of("y", 4L, "list", ImmutableList.of(1L, 2L, 3L)))); + runTest(ImmutableMap.of("y", 4L, "list", ImmutableList.of(1L, 2L, 3L))); source = "y in list"; - runTest(Activation.copyOf(ImmutableMap.of("y", 1L, "list", ImmutableList.of(1L, 2L, 3L)))); + runTest(ImmutableMap.of("y", 1L, "list", ImmutableList.of(1L, 2L, 3L))); + } + + @Test + public void lists_error() { + declareVariable("y", SimpleType.INT); + declareVariable("list", ListType.create(SimpleType.INT)); source = "list[3]"; - runTest(Activation.copyOf(ImmutableMap.of("y", 1L, "list", ImmutableList.of(1L, 2L, 3L)))); + runTest(ImmutableMap.of("y", 1L, "list", ImmutableList.of(1L, 2L, 3L))); } @Test public void maps() throws Exception { - declareVariable("x", CelTypes.createMessage(TestAllTypes.getDescriptor().getFullName())); - container = TestAllTypes.getDescriptor().getFile().getPackage(); + declareVariable("x", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())); + setContainer(CelContainer.ofName(TestAllTypes.getDescriptor().getFile().getPackage())); source = "{1: 2, 3: 4}[3] == 4"; - runTest(Activation.EMPTY); + runTest(); // Constant key in constant map. source = "3 in {1: 2, 3: 4} && !(4 in {1: 2, 3: 4})"; - runTest(Activation.EMPTY); + runTest(); source = "x.map_int32_int64[22] == 23"; - runTest(Activation.of("x", TestAllTypes.newBuilder().putMapInt32Int64(22, 23).build())); + runTest(ImmutableMap.of("x", TestAllTypes.newBuilder().putMapInt32Int64(22, 23).build())); source = "TestAllTypes{map_int32_int64: {21: 22, 22: 23}}.map_int32_int64[22] == 23"; - runTest(Activation.EMPTY); + runTest(); source = "TestAllTypes{oneof_type: NestedTestAllTypes{payload: x}}" + ".oneof_type.payload.map_int32_int64[22] == 23"; - runTest(Activation.of("x", TestAllTypes.newBuilder().putMapInt32Int64(22, 23).build())); + runTest(ImmutableMap.of("x", TestAllTypes.newBuilder().putMapInt32Int64(22, 23).build())); - declareVariable("y", CelTypes.INT64); - declareVariable("map", CelTypes.createMap(CelTypes.INT64, CelTypes.INT64)); + declareVariable("y", SimpleType.INT); + declareVariable("map", MapType.create(SimpleType.INT, SimpleType.INT)); // Constant key in variable map. source = "!(4 in map) && 1 in map"; - runTest(Activation.of("map", ImmutableMap.of(1L, 4L, 2L, 3L, 3L, 2L))); + runTest(ImmutableMap.of("map", ImmutableMap.of(1L, 4L, 2L, 3L, 3L, 2L))); // Variable key in constant map. source = "!(y in {1: 4, 2: 3, 3: 2}) && y in {5: 3, 4: 2, 3: 3}"; - runTest(Activation.of("y", 4L)); + runTest(ImmutableMap.of("y", 4L)); // Variable key in variable map. source = "!(y in map) && (y + 3) in map"; - runTest( - Activation.copyOf( - ImmutableMap.of("y", 1L, "map", ImmutableMap.of(4L, 1L, 5L, 2L, 6L, 3L)))); + runTest(ImmutableMap.of("y", 1L, "map", ImmutableMap.of(4L, 1L, 5L, 2L, 6L, 3L))); // Message value in map source = "TestAllTypes{map_int64_nested_type:{42:NestedTestAllTypes{payload:TestAllTypes{}}}}"; - runTest(Activation.EMPTY); + runTest(); // Repeated key - constant source = "{true: 1, false: 2, true: 3}[true]"; - runTest(Activation.EMPTY); + runTest(); // Repeated key - expressions - declareVariable("b", CelTypes.BOOL); + declareVariable("b", SimpleType.BOOL); source = "{b: 1, !b: 2, b: 3}[true]"; - runTest(Activation.of("b", true)); + runTest(ImmutableMap.of("b", true)); } @Test public void comprehension() throws Exception { source = "[0, 1, 2].map(x, x > 0, x + 1) == [2, 3]"; - runTest(Activation.EMPTY); + runTest(); source = "[0, 1, 2].exists(x, x > 0)"; - runTest(Activation.EMPTY); + runTest(); source = "[0, 1, 2].exists(x, x > 2)"; - runTest(Activation.EMPTY); + runTest(); + + declareVariable("com.x", SimpleType.INT); + setContainer(CelContainer.ofName("com")); + source = "[0].exists(x, x == 0)"; + runTest(ImmutableMap.of("com.x", 1)); + + clearAllDeclarations(); + declareVariable("cel.example.y", SimpleType.INT); + setContainer(CelContainer.ofName("cel.example")); + source = "[{'z': 0}].exists(y, y.z == 0)"; + runTest(ImmutableMap.of("cel.example.y", ImmutableMap.of("z", 1))); + + clearAllDeclarations(); + declareVariable("y.z", SimpleType.INT); + setContainer(CelContainer.ofName("y")); + source = "[{'z': 0}].exists(y, y.z == 0 && .y.z == 1)"; + runTest(ImmutableMap.of("y.z", 1)); + + clearAllDeclarations(); + declareVariable("x", SimpleType.INT); + source = "[0].exists(x, x == 0 && .x == 1)"; + runTest(ImmutableMap.of("x", 1)); + + source = "[0].exists(x, [x+1].exists(x, x == .x))"; + runTest(ImmutableMap.of("x", 1)); } @Test - public void abstractType() throws Exception { - Type typeParam = CelTypes.createTypeParam("T"); - Type abstractType = - Type.newBuilder() - .setAbstractType( - AbstractType.newBuilder().setName("vector").addParameterTypes(typeParam)) - .build(); + public void abstractType() { + CelType typeParam = TypeParamType.create("T"); + CelType abstractType = OpaqueType.create("vector", typeParam); + // Declare a function to create a vector. declareFunction( "vector", - globalOverload( - "vector", - ImmutableList.of(CelTypes.createList(typeParam)), - ImmutableList.of("T"), - abstractType)); - eval.registrar() - .add( + globalOverload("vector", ImmutableList.of(ListType.create(typeParam)), abstractType)); + // Declare a function to access element of a vector. + declareFunction( + "at", memberOverload("at", ImmutableList.of(abstractType, SimpleType.INT), typeParam)); + // Add function bindings for above + addFunctionBinding( + CelFunctionBinding.from( "vector", ImmutableList.of(List.class), (Object[] args) -> { List list = (List) args[0]; return list.toArray(new Object[0]); - }); - // Declare a function to access element of a vector. - declareFunction( - "at", - memberOverload( - "at", - ImmutableList.of(abstractType, CelTypes.INT64), - ImmutableList.of("T"), - typeParam)); - eval.registrar() - .add( + }), + CelFunctionBinding.from( "at", ImmutableList.of(Object[].class, Long.class), (Object[] args) -> { Object[] array = (Object[]) args[0]; return array[(int) (long) args[1]]; - }); + })); source = "vector([1,2,3]).at(1) == 2"; - runTest(Activation.EMPTY); + runTest(); source = "vector([1,2,3]).at(1) + vector([7]).at(0)"; - runTest(Activation.EMPTY); + runTest(); } @Test - public void namespacedFunctions() throws Exception { + public void namespacedFunctions() { declareFunction( "ns.func", - globalOverload("ns_func_overload", ImmutableList.of(CelTypes.STRING), CelTypes.INT64)); + globalOverload("ns_func_overload", ImmutableList.of(SimpleType.STRING), SimpleType.INT)); declareFunction( "member", memberOverload( "ns_member_overload", - ImmutableList.of(CelTypes.INT64, CelTypes.INT64), - CelTypes.INT64)); - eval.registrar().add("ns_func_overload", String.class, s -> (long) s.length()); - eval.registrar().add("ns_member_overload", Long.class, Long.class, Long::sum); + ImmutableList.of(SimpleType.INT, SimpleType.INT), + SimpleType.INT)); + addFunctionBinding( + CelFunctionBinding.fromOverloads( + "ns.func", + CelFunctionBinding.from("ns_func_overload", String.class, s -> (long) s.length()))); + addFunctionBinding( + CelFunctionBinding.fromOverloads( + "member", + CelFunctionBinding.from("ns_member_overload", Long.class, Long.class, Long::sum))); + source = "ns.func('hello')"; - runTest(Activation.EMPTY); + runTest(); source = "ns.func('hello').member(ns.func('test'))"; - runTest(Activation.EMPTY); + runTest(); source = "{ns.func('test'): 2}"; - runTest(Activation.EMPTY); + runTest(); source = "{2: ns.func('test')}"; - runTest(Activation.EMPTY); + runTest(); source = "[ns.func('test'), 2]"; - runTest(Activation.EMPTY); + runTest(); source = "[ns.func('test')].map(x, x * 2)"; - runTest(Activation.EMPTY); + runTest(); source = "[1, 2].map(x, x * ns.func('test'))"; - runTest(Activation.EMPTY); + runTest(); - container = "ns"; + setContainer(CelContainer.ofName("ns")); // Call with the container set as the function's namespace source = "ns.func('hello')"; - runTest(Activation.EMPTY); + runTest(); source = "func('hello')"; - runTest(Activation.EMPTY); + runTest(); source = "func('hello').member(func('test'))"; - runTest(Activation.EMPTY); + runTest(); } @Test - public void namespacedVariables() throws Exception { - container = "ns"; - declareVariable("ns.x", CelTypes.INT64); + public void namespacedVariables() { + setContainer(CelContainer.ofName("ns")); + declareVariable("ns.x", SimpleType.INT); source = "x"; - runTest(Activation.of("ns.x", 2)); + runTest(ImmutableMap.of("ns.x", 2)); - container = "dev.cel.testing.testdata.proto3"; - Type messageType = CelTypes.createMessage("dev.cel.testing.testdata.proto3.TestAllTypes"); + setContainer(CelContainer.ofName("dev.cel.testing.testdata.proto3")); + CelType messageType = StructTypeReference.create("cel.expr.conformance.proto3.TestAllTypes"); declareVariable("dev.cel.testing.testdata.proto3.msgVar", messageType); source = "msgVar.single_int32"; runTest( - Activation.of( + ImmutableMap.of( "dev.cel.testing.testdata.proto3.msgVar", TestAllTypes.newBuilder().setSingleInt32(5).build())); } @Test - public void durationFunctions() throws Exception { - declareVariable("d1", CelTypes.DURATION); - Duration d1 = - Duration.newBuilder().setSeconds(25 * 3600 + 59 * 60 + 1).setNanos(11000000).build(); - Duration d2 = - Duration.newBuilder().setSeconds(-(25 * 3600 + 59 * 60 + 1)).setNanos(-11000000).build(); - container = Type.getDescriptor().getFile().getPackage(); + public void durationFunctions() { + declareVariable("d1", SimpleType.DURATION); + long totalSeconds = 25 * 3600 + 59 * 60 + 1; + long nanos = 11000000; + Duration d1 = Duration.ofSeconds(totalSeconds, nanos); + Duration d2 = Duration.ofSeconds(-totalSeconds, -nanos); + + setContainer(CelContainer.ofName(Type.getDescriptor().getFile().getPackage())); source = "d1.getHours()"; - runTest(Activation.of("d1", d1)); - runTest(Activation.of("d1", d2)); + runTest(ImmutableMap.of("d1", d1)); + runTest(ImmutableMap.of("d1", d2)); source = "d1.getMinutes()"; - runTest(Activation.of("d1", d1)); - runTest(Activation.of("d1", d2)); + runTest(ImmutableMap.of("d1", d1)); + runTest(ImmutableMap.of("d1", d2)); source = "d1.getSeconds()"; - runTest(Activation.of("d1", d1)); - runTest(Activation.of("d1", d2)); + runTest(ImmutableMap.of("d1", d1)); + runTest(ImmutableMap.of("d1", d2)); source = "d1.getMilliseconds()"; - runTest(Activation.of("d1", d1)); - runTest(Activation.of("d1", d2)); + runTest(ImmutableMap.of("d1", d1)); + runTest(ImmutableMap.of("d1", d2)); - declareVariable("val", CelTypes.INT64); + declareVariable("val", SimpleType.INT); source = "d1.getHours() < val"; - runTest(Activation.of("d1", d1).extend(Activation.of("val", 30L))); + runTest(extend(ImmutableMap.of("d1", d1), ImmutableMap.of("val", 30L))); source = "d1.getMinutes() > val"; - runTest(Activation.of("d1", d1).extend(Activation.of("val", 30L))); + runTest(extend(ImmutableMap.of("d1", d1), ImmutableMap.of("val", 30L))); source = "d1.getSeconds() > val"; - runTest(Activation.of("d1", d1).extend(Activation.of("val", 30L))); + runTest(extend(ImmutableMap.of("d1", d1), ImmutableMap.of("val", 30L))); source = "d1.getMilliseconds() < val"; - runTest(Activation.of("d1", d1).extend(Activation.of("val", 30L))); + runTest(extend(ImmutableMap.of("d1", d1), ImmutableMap.of("val", 30L))); } @Test - public void timestampFunctions() throws Exception { - declareVariable("ts1", CelTypes.TIMESTAMP); - container = Type.getDescriptor().getFile().getPackage(); - Timestamp ts1 = Timestamp.newBuilder().setSeconds(1).setNanos(11000000).build(); - Timestamp ts2 = Timestamps.fromSeconds(-1); + public void timestampFunctions() { + declareVariable("ts1", SimpleType.TIMESTAMP); + setContainer(CelContainer.ofName(Type.getDescriptor().getFile().getPackage())); + Instant ts1 = Instant.ofEpochSecond(1, 11000000); + Instant ts2 = Instant.ofEpochSecond(-1, 0); source = "ts1.getFullYear(\"America/Los_Angeles\")"; - runTest(Activation.of("ts1", ts1)); + runTest(ImmutableMap.of("ts1", ts1)); source = "ts1.getFullYear()"; - runTest(Activation.of("ts1", ts1)); - runTest(Activation.of("ts1", ts2)); + runTest(ImmutableMap.of("ts1", ts1)); + runTest(ImmutableMap.of("ts1", ts2)); source = "ts1.getFullYear(\"Indian/Cocos\")"; - runTest(Activation.of("ts1", ts1)); + runTest(ImmutableMap.of("ts1", ts1)); source = "ts1.getFullYear(\"2:00\")"; - runTest(Activation.of("ts1", ts1)); + runTest(ImmutableMap.of("ts1", ts1)); source = "ts1.getMonth(\"America/Los_Angeles\")"; - runTest(Activation.of("ts1", ts1)); + runTest(ImmutableMap.of("ts1", ts1)); source = "ts1.getMonth()"; - runTest(Activation.of("ts1", ts1)); - runTest(Activation.of("ts1", ts2)); + runTest(ImmutableMap.of("ts1", ts1)); + runTest(ImmutableMap.of("ts1", ts2)); source = "ts1.getMonth(\"Indian/Cocos\")"; - runTest(Activation.of("ts1", ts1)); + runTest(ImmutableMap.of("ts1", ts1)); source = "ts1.getMonth(\"-8:15\")"; - runTest(Activation.of("ts1", ts1)); + runTest(ImmutableMap.of("ts1", ts1)); source = "ts1.getDayOfYear(\"America/Los_Angeles\")"; - runTest(Activation.of("ts1", ts1)); + runTest(ImmutableMap.of("ts1", ts1)); source = "ts1.getDayOfYear()"; - runTest(Activation.of("ts1", ts1)); - runTest(Activation.of("ts1", ts2)); + runTest(ImmutableMap.of("ts1", ts1)); + runTest(ImmutableMap.of("ts1", ts2)); source = "ts1.getDayOfYear(\"Indian/Cocos\")"; - runTest(Activation.of("ts1", ts1)); + runTest(ImmutableMap.of("ts1", ts1)); source = "ts1.getDayOfYear(\"-9:00\")"; - runTest(Activation.of("ts1", ts1)); + runTest(ImmutableMap.of("ts1", ts1)); source = "ts1.getDayOfMonth(\"America/Los_Angeles\")"; - runTest(Activation.of("ts1", ts1)); + runTest(ImmutableMap.of("ts1", ts1)); source = "ts1.getDayOfMonth()"; - runTest(Activation.of("ts1", ts1)); - runTest(Activation.of("ts1", ts2)); + runTest(ImmutableMap.of("ts1", ts1)); + runTest(ImmutableMap.of("ts1", ts2)); source = "ts1.getDayOfMonth(\"Indian/Cocos\")"; - runTest(Activation.of("ts1", ts1)); + runTest(ImmutableMap.of("ts1", ts1)); source = "ts1.getDayOfMonth(\"8:00\")"; - runTest(Activation.of("ts1", ts1)); + runTest(ImmutableMap.of("ts1", ts1)); source = "ts1.getDate(\"America/Los_Angeles\")"; - runTest(Activation.of("ts1", ts1)); + runTest(ImmutableMap.of("ts1", ts1)); source = "ts1.getDate()"; - runTest(Activation.of("ts1", ts1)); - runTest(Activation.of("ts1", ts2)); + runTest(ImmutableMap.of("ts1", ts1)); + runTest(ImmutableMap.of("ts1", ts2)); source = "ts1.getDate(\"Indian/Cocos\")"; - runTest(Activation.of("ts1", ts1)); + runTest(ImmutableMap.of("ts1", ts1)); source = "ts1.getDate(\"9:30\")"; - runTest(Activation.of("ts1", ts1)); + runTest(ImmutableMap.of("ts1", ts1)); - Timestamp tsSunday = Timestamps.fromSeconds(3 * 24 * 3600); + Instant tsSunday = Instant.ofEpochSecond(3 * 24 * 3600); source = "ts1.getDayOfWeek(\"America/Los_Angeles\")"; - runTest(Activation.of("ts1", tsSunday)); + runTest(ImmutableMap.of("ts1", tsSunday)); source = "ts1.getDayOfWeek()"; - runTest(Activation.of("ts1", tsSunday)); - runTest(Activation.of("ts1", ts2)); + runTest(ImmutableMap.of("ts1", tsSunday)); + runTest(ImmutableMap.of("ts1", ts2)); source = "ts1.getDayOfWeek(\"Indian/Cocos\")"; - runTest(Activation.of("ts1", tsSunday)); + runTest(ImmutableMap.of("ts1", tsSunday)); source = "ts1.getDayOfWeek(\"-9:30\")"; - runTest(Activation.of("ts1", tsSunday)); + runTest(ImmutableMap.of("ts1", tsSunday)); source = "ts1.getHours(\"America/Los_Angeles\")"; - runTest(Activation.of("ts1", ts1)); + runTest(ImmutableMap.of("ts1", ts1)); source = "ts1.getHours()"; - runTest(Activation.of("ts1", ts1)); - runTest(Activation.of("ts1", ts2)); + runTest(ImmutableMap.of("ts1", ts1)); + runTest(ImmutableMap.of("ts1", ts2)); source = "ts1.getHours(\"Indian/Cocos\")"; - runTest(Activation.of("ts1", ts1)); + runTest(ImmutableMap.of("ts1", ts1)); source = "ts1.getHours(\"6:30\")"; - runTest(Activation.of("ts1", ts1)); + runTest(ImmutableMap.of("ts1", ts1)); source = "ts1.getMinutes(\"America/Los_Angeles\")"; - runTest(Activation.of("ts1", ts1)); + runTest(ImmutableMap.of("ts1", ts1)); source = "ts1.getMinutes()"; - runTest(Activation.of("ts1", ts1)); - runTest(Activation.of("ts1", ts2)); + runTest(ImmutableMap.of("ts1", ts1)); + runTest(ImmutableMap.of("ts1", ts2)); source = "ts1.getMinutes(\"Indian/Cocos\")"; - runTest(Activation.of("ts1", ts1)); + runTest(ImmutableMap.of("ts1", ts1)); source = "ts1.getMinutes(\"-8:00\")"; - runTest(Activation.of("ts1", ts1)); + runTest(ImmutableMap.of("ts1", ts1)); source = "ts1.getSeconds(\"America/Los_Angeles\")"; - runTest(Activation.of("ts1", ts1)); + runTest(ImmutableMap.of("ts1", ts1)); source = "ts1.getSeconds()"; - runTest(Activation.of("ts1", ts1)); - runTest(Activation.of("ts1", ts2)); + runTest(ImmutableMap.of("ts1", ts1)); + runTest(ImmutableMap.of("ts1", ts2)); source = "ts1.getSeconds(\"Indian/Cocos\")"; - runTest(Activation.of("ts1", ts1)); + runTest(ImmutableMap.of("ts1", ts1)); source = "ts1.getSeconds(\"-8:00\")"; - runTest(Activation.of("ts1", ts1)); + runTest(ImmutableMap.of("ts1", ts1)); source = "ts1.getMilliseconds(\"America/Los_Angeles\")"; - runTest(Activation.of("ts1", ts1)); + runTest(ImmutableMap.of("ts1", ts1)); source = "ts1.getMilliseconds()"; - runTest(Activation.of("ts1", ts1)); + runTest(ImmutableMap.of("ts1", ts1)); source = "ts1.getMilliseconds(\"Indian/Cocos\")"; - runTest(Activation.of("ts1", ts1)); + runTest(ImmutableMap.of("ts1", ts1)); source = "ts1.getMilliseconds(\"-8:00\")"; - runTest(Activation.of("ts1", ts1)); + runTest(ImmutableMap.of("ts1", ts1)); - declareVariable("val", CelTypes.INT64); + declareVariable("val", SimpleType.INT); source = "ts1.getFullYear() < val"; - runTest(Activation.of("ts1", ts1).extend(Activation.of("val", 2013L))); + runTest(extend(ImmutableMap.of("ts1", ts1), ImmutableMap.of("val", 2013L))); source = "ts1.getMonth() < val"; - runTest(Activation.of("ts1", ts1).extend(Activation.of("val", 12L))); + runTest(extend(ImmutableMap.of("ts1", ts1), ImmutableMap.of("val", 12L))); source = "ts1.getDayOfYear() < val"; - runTest(Activation.of("ts1", ts1).extend(Activation.of("val", 13L))); + runTest(extend(ImmutableMap.of("ts1", ts1), ImmutableMap.of("val", 13L))); source = "ts1.getDayOfMonth() < val"; - runTest(Activation.of("ts1", ts1).extend(Activation.of("val", 10L))); + runTest(extend(ImmutableMap.of("ts1", ts1), ImmutableMap.of("val", 10L))); source = "ts1.getDate() < val"; - runTest(Activation.of("ts1", ts1).extend(Activation.of("val", 15L))); + runTest(extend(ImmutableMap.of("ts1", ts1), ImmutableMap.of("val", 15L))); source = "ts1.getDayOfWeek() < val"; - runTest(Activation.of("ts1", ts1).extend(Activation.of("val", 15L))); + runTest(extend(ImmutableMap.of("ts1", ts1), ImmutableMap.of("val", 15L))); source = "ts1.getHours() < val"; - runTest(Activation.of("ts1", ts1).extend(Activation.of("val", 15L))); + runTest(extend(ImmutableMap.of("ts1", ts1), ImmutableMap.of("val", 15L))); source = "ts1.getMinutes() < val"; - runTest(Activation.of("ts1", ts1).extend(Activation.of("val", 15L))); + runTest(extend(ImmutableMap.of("ts1", ts1), ImmutableMap.of("val", 15L))); source = "ts1.getSeconds() < val"; - runTest(Activation.of("ts1", ts1).extend(Activation.of("val", 15L))); + runTest(extend(ImmutableMap.of("ts1", ts1), ImmutableMap.of("val", 15L))); source = "ts1.getMilliseconds() < val"; - runTest(Activation.of("ts1", ts1).extend(Activation.of("val", 15L))); + runTest(extend(ImmutableMap.of("ts1", ts1), ImmutableMap.of("val", 15L))); } @Test - public void unknownField() throws Exception { - container = TestAllTypes.getDescriptor().getFile().getPackage(); - declareVariable("x", CelTypes.createMessage(TestAllTypes.getDescriptor().getFullName())); - TestAllTypes val = - TestAllTypes.newBuilder() - .setSingleTimestamp(Timestamps.fromSeconds(15)) - .setSingleDuration(Durations.fromSeconds(15)) - .setSingleNestedMessage(NestedMessage.getDefaultInstance()) - .addRepeatedNestedMessage(0, NestedMessage.newBuilder().setBb(14).build()) - .build(); - - PartialMessage wm = - new PartialMessage( - val, - FieldMask.newBuilder() - .addPaths("map_int32_int64") - .addPaths("single_int32") - .addPaths("single_nested_message.bb") - .addPaths("repeated_nested_message") - .addPaths("single_duration.seconds") - .build()); + public void unknownField() { + setContainer(CelContainer.ofName(TestAllTypes.getDescriptor().getFile().getPackage())); + declareVariable("x", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())); // Unknown field is accessed. source = "x.single_int32"; - runTest(Activation.of("x", wm)); + runTest(); source = "x.map_int32_int64[22]"; - runTest(Activation.of("x", wm)); + runTest(); source = "x.repeated_nested_message[1]"; - runTest(Activation.of("x", wm)); + runTest(); - // Function call for a known field. + // Function call for an unknown field. source = "x.single_timestamp.getSeconds()"; - runTest(Activation.of("x", wm)); - - // PartialMessage does not support function call - source = "x.single_duration.getMilliseconds()"; - runTest(Activation.of("x", wm)); - - // PartialMessage does not support operators. - source = "x.single_duration + x.single_duration"; - runTest(Activation.of("x", wm)); + runTest(); // Unknown field in a nested message source = "x.single_nested_message.bb"; - runTest(Activation.of("x", wm)); + runTest(); - // PartialMessage cannot be a final expr result. - source = "x.single_nested_message"; - runTest(Activation.of("x", wm)); - - // PartialMessage cannot be a field of another message. - source = "TestAllTypes{single_nested_message: x.single_nested_message}"; - runTest(Activation.of("x", wm)); - - // Unknown field cannot be a value of a map now. + // Unknown field access in a map. source = "{1: x.single_int32}"; - runTest(Activation.of("x", wm)); - - // Access a known field as a val of a map. - source = "{1: x.single_int64}"; - runTest(Activation.of("x", wm)); - - // PartialMessage cannot be a value of a map. - source = "{1: x.single_nested_message}"; - runTest(Activation.of("x", wm)); + runTest(); - // Unknown field cannot be a value of a list now. + // Unknown field access in a list. source = "[1, x.single_int32]"; - runTest(Activation.of("x", wm)); - - // PartialMessage cannot be an elem in a list. - source = "[x.single_nested_message]"; - runTest(Activation.of("x", wm)); - - // Access a field in a nested message masked as unknown. - wm = new PartialMessage(val, FieldMask.newBuilder().addPaths("single_nested_message").build()); - - // Access unknown field. - source = "x.single_nested_message.bb"; - runTest(Activation.of("x", wm)); - - // Error or true should be true. - source = "(x.single_nested_message.bb == 42) || true"; - runTest(Activation.of("x", wm)); - - // Error or false should be error. - source = "(x.single_nested_message.bb == 42) || false"; - runTest(Activation.of("x", wm)); - - // Error and true should be error. - source = "(x.single_nested_message.bb == 42) && true"; - runTest(Activation.of("x", wm)); - - // Error and false should be false. - source = "(x.single_nested_message.bb == 42) && false"; - runTest(Activation.of("x", wm)); + runTest(); } @Test - public void unknownResultSet() throws Exception { - container = TestAllTypes.getDescriptor().getFile().getPackage(); - declareVariable("x", CelTypes.createMessage(TestAllTypes.getDescriptor().getFullName())); - TestAllTypes val = + public void unknownResultSet() { + 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(); - PartialMessage message = - new PartialMessage( - val, - FieldMask.newBuilder() - .addPaths("single_int32") - .addPaths("single_int64") - .addPaths("map_int32_int64") - .build()); - // unknown && true ==> unknown - source = "x.single_int32 == 1 && x.single_string == \"test\""; - runTest(Activation.of("x", message)); + source = "x.single_int32 == 1 && true"; + runTest(); // unknown && false ==> false - source = "x.single_int32 == 1 && x.single_string != \"test\""; - runTest(Activation.of("x", message)); + source = "x.single_int32 == 1 && false"; + runTest(); // unknown && Unknown ==> UnknownSet source = "x.single_int32 == 1 && x.single_int64 == 1"; - runTest(Activation.of("x", message)); + runTest(); // unknown && error ==> unknown source = "x.single_int32 == 1 && x.single_timestamp <= timestamp(\"bad timestamp string\")"; - runTest(Activation.of("x", message)); + runTest(); // true && unknown ==> unknown - source = "x.single_string == \"test\" && x.single_int32 == 1"; - runTest(Activation.of("x", message)); + source = "true && x.single_int32 == 1"; + runTest(); // false && unknown ==> false - source = "x.single_string != \"test\" && x.single_int32 == 1"; - runTest(Activation.of("x", message)); + source = "false && x.single_int32 == 1"; + runTest(); // error && unknown ==> unknown source = "x.single_timestamp <= timestamp(\"bad timestamp string\") && x.single_int32 == 1"; - runTest(Activation.of("x", message)); + runTest(); // error && error ==> error source = "x.single_timestamp <= timestamp(\"bad timestamp string\") " + "&& x.single_timestamp > timestamp(\"another bad timestamp string\")"; - runTest(Activation.of("x", message)); + runTest(); // unknown || true ==> true source = "x.single_int32 == 1 || x.single_string == \"test\""; - runTest(Activation.of("x", message)); + runTest(); // unknown || false ==> unknown source = "x.single_int32 == 1 || x.single_string != \"test\""; - runTest(Activation.of("x", message)); + runTest(); // unknown || unknown ==> UnknownSet source = "x.single_int32 == 1 || x.single_int64 == 1"; - runTest(Activation.of("x", message)); + runTest(); // unknown || error ==> unknown source = "x.single_int32 == 1 || x.single_timestamp <= timestamp(\"bad timestamp string\")"; - runTest(Activation.of("x", message)); + runTest(); // true || unknown ==> true - source = "x.single_string == \"test\" || x.single_int32 == 1"; - runTest(Activation.of("x", message)); + source = "true || x.single_int32 == 1"; + runTest(); // false || unknown ==> unknown - source = "x.single_string != \"test\" || x.single_int32 == 1"; - runTest(Activation.of("x", message)); + source = "false || x.single_int32 == 1"; + runTest(); // error || unknown ==> unknown source = "x.single_timestamp <= timestamp(\"bad timestamp string\") || x.single_int32 == 1"; - runTest(Activation.of("x", message)); + runTest(); // error || error ==> error source = "x.single_timestamp <= timestamp(\"bad timestamp string\") " + "|| x.single_timestamp > timestamp(\"another bad timestamp string\")"; - runTest(Activation.of("x", message)); + runTest(); // dispatch test declareFunction( - "f", memberOverload("f", Arrays.asList(CelTypes.INT64, CelTypes.INT64), CelTypes.BOOL)); - eval.registrar().add("f", Integer.class, Integer.class, Objects::equals); + "f", memberOverload("f", Arrays.asList(SimpleType.INT, SimpleType.INT), SimpleType.BOOL)); + addFunctionBinding(CelFunctionBinding.from("f", Integer.class, Integer.class, Objects::equals)); // dispatch: unknown.f(1) ==> unknown source = "x.single_int32.f(1)"; - runTest(Activation.of("x", message)); + runTest(); // dispatch: 1.f(unknown) ==> unknown source = "1.f(x.single_int32)"; - runTest(Activation.of("x", message)); + runTest(); // dispatch: unknown.f(unknown) ==> unknownSet source = "x.single_int64.f(x.single_int32)"; - runTest(Activation.of("x", message)); + runTest(); // ident is null(x is unbound) ==> unknown source = "x"; - runTest(Activation.of("y", message)); + runTest(ImmutableMap.of("y", message)); // ident is unknown ==> unknown source = "x"; - ExprValue unknownMessage = - ExprValue.newBuilder().setUnknown(UnknownSet.getDefaultInstance()).build(); - runTest(Activation.of("x", unknownMessage)); + CelUnknownSet unknownMessage = CelUnknownSet.create(1L); + runTest(ImmutableMap.of("x", unknownMessage)); // comprehension test // iteRange is unknown => unknown source = "x.map_int32_int64.map(x, x > 0, x + 1)"; - runTest(Activation.of("x", message)); + runTest(); // exists, loop condition encounters unknown => skip unknown and check other element source = "[0, 2, 4].exists(z, z == 2 || z == x.single_int32)"; - runTest(Activation.of("x", message)); + runTest(); // exists, loop condition encounters unknown => skip unknown and check other element, no dupe id // in result source = "[0, 2, 4].exists(z, z == x.single_int32)"; - runTest(Activation.of("x", message)); + runTest(); // exists_one, loop condition encounters unknown => collect all unknowns source = "[0, 2, 4].exists_one(z, z == 0 || (z == 2 && z == x.single_int32) " + "|| (z == 4 && z == x.single_int64))"; - runTest(Activation.of("x", message)); + runTest(); // all, loop condition encounters unknown => skip unknown and check other element source = "[0, 2].all(z, z == 2 || z == x.single_int32)"; - runTest(Activation.of("x", message)); + runTest(); // filter, loop condition encounters unknown => skip unknown and check other element source = "[0, 2, 4].filter(z, z == 0 || (z == 2 && z == x.single_int32) " + "|| (z == 4 && z == x.single_int64))"; - runTest(Activation.of("x", message)); + runTest(); // map, loop condition encounters unknown => skip unknown and check other element source = "[0, 2, 4].map(z, z == 0 || (z == 2 && z == x.single_int32) " + "|| (z == 4 && z == x.single_int64))"; - runTest(Activation.of("x", message)); + runTest(); // conditional test // unknown ? 1 : 2 ==> unknown source = "x.single_int32 == 1 ? 1 : 2"; - runTest(Activation.of("x", message)); + runTest(); // true ? unknown : 2 ==> unknown - source = "x.single_string == \"test\" ? x.single_int32 : 2"; - runTest(Activation.of("x", message)); + source = "true ? x.single_int32 : 2"; + runTest(); // true ? 1 : unknown ==> 1 - source = "x.single_string == \"test\" ? 1 : x.single_int32"; - runTest(Activation.of("x", message)); + source = "true ? 1 : x.single_int32"; + runTest(); // false ? unknown : 2 ==> 2 - source = "x.single_string != \"test\" ? x.single_int32 : 2"; - runTest(Activation.of("x", message)); + source = "false ? x.single_int32 : 2"; + runTest(); // false ? 1 : unknown ==> unknown - source = "x.single_string != \"test\" ? 1 : x.single_int32"; - runTest(Activation.of("x", message)); + source = "false ? 1 : x.single_int32"; + runTest(); // unknown condition ? unknown : unknown ==> unknown condition source = "x.single_int64 == 1 ? x.single_int32 : x.single_int32"; - runTest(Activation.of("x", message)); + runTest(); // map with unknown key => unknown source = "{x.single_int32: 2, 3: 4}"; - runTest(Activation.of("x", message)); + runTest(); // map with unknown value => unknown source = "{1: x.single_int32, 3: 4}"; - runTest(Activation.of("x", message)); + runTest(); // map with unknown key and value => unknownSet source = "{1: x.single_int32, x.single_int64: 4}"; - runTest(Activation.of("x", message)); + runTest(); // list with unknown => unknown source = "[1, x.single_int32, 3, 4]"; - runTest(Activation.of("x", message)); + runTest(); // list with multiple unknowns => unknownSet source = "[1, x.single_int32, x.single_int64, 4]"; - runTest(Activation.of("x", message)); + runTest(); // message with unknown => unknown source = "TestAllTypes{single_int32: x.single_int32}.single_int32 == 2"; - runTest(Activation.of("x", message)); + runTest(); // message with multiple unknowns => unknownSet source = "TestAllTypes{single_int32: x.single_int32, single_int64: x.single_int64}"; - runTest(Activation.of("x", message)); + runTest(); + + // type(unknown) -> unknown + source = "type(x.single_int32)"; + runTest(); + + // type(error) -> error + source = "type(1 / 0 > 2)"; + runTest(); } @Test - public void timeConversions() throws Exception { - container = Type.getDescriptor().getFile().getPackage(); - declareVariable("t1", CelTypes.TIMESTAMP); + public void timeConversions() { + setContainer(CelContainer.ofName(Type.getDescriptor().getFile().getPackage())); + declareVariable("t1", SimpleType.TIMESTAMP); source = "timestamp(\"1972-01-01T10:00:20.021-05:00\")"; - runTest(Activation.EMPTY); + runTest(); source = "timestamp(123)"; - runTest(Activation.EMPTY); + runTest(); source = "duration(\"15.11s\")"; - runTest(Activation.EMPTY); + runTest(); source = "int(t1) == 100"; - runTest(Activation.of("t1", Timestamps.fromSeconds(100))); + runTest(ImmutableMap.of("t1", Instant.ofEpochSecond(100))); source = "duration(\"1h2m3.4s\")"; - runTest(Activation.EMPTY); + runTest(); - // Not supported. + source = "duration(duration('15.0s'))"; // Identity + runTest(); + + source = "timestamp(timestamp(123))"; // Identity + runTest(); + } + + @Test + public void timeConversions_error() { source = "duration('inf')"; - runTest(Activation.EMPTY); + runTest(); } @Test - public void sizeTests() throws Exception { - container = Type.getDescriptor().getFile().getPackage(); - declareVariable("str", CelTypes.STRING); - declareVariable("b", CelTypes.BYTES); + public void sizeTests() { + setContainer(CelContainer.ofName(Type.getDescriptor().getFile().getPackage())); + declareVariable("str", SimpleType.STRING); + declareVariable("b", SimpleType.BYTES); source = "size(b) == 5 && b.size() == 5"; - runTest(Activation.of("b", ByteString.copyFromUtf8("happy"))); + runTest(ImmutableMap.of("b", CelByteString.copyFromUtf8("happy"))); source = "size(str) == 5 && str.size() == 5"; - runTest(Activation.of("str", "happy")); - runTest(Activation.of("str", "happ\uDBFF\uDFFC")); + runTest(ImmutableMap.of("str", "happy")); + runTest(ImmutableMap.of("str", "happ\uDBFF\uDFFC")); source = "size({1:14, 2:15}) == 2 && {1:14, 2:15}.size() == 2"; - runTest(Activation.EMPTY); + runTest(); source = "size([1,2,3]) == 3 && [1,2,3].size() == 3"; - runTest(Activation.EMPTY); + runTest(); } @Test - public void nonstrictQuantifierTests() throws Exception { + public void nonstrictQuantifierTests() { // Plain tests. Everything is constant. source = "[0, 2, 4].exists(x, 4/x == 2 && 4/(4-x) == 2)"; - runTest(Activation.EMPTY); + runTest(); source = "![0, 2, 4].all(x, 4/x != 2 && 4/(4-x) != 2)"; - runTest(Activation.EMPTY); + runTest(); - declareVariable("four", CelTypes.INT64); + declareVariable("four", SimpleType.INT); // Condition is dynamic. source = "[0, 2, 4].exists(x, four/x == 2 && four/(four-x) == 2)"; - runTest(Activation.of("four", 4L)); + runTest(ImmutableMap.of("four", 4L)); source = "![0, 2, 4].all(x, four/x != 2 && four/(four-x) != 2)"; - runTest(Activation.of("four", 4L)); + runTest(ImmutableMap.of("four", 4L)); // Both range and condition are dynamic. source = "[0, 2, four].exists(x, four/x == 2 && four/(four-x) == 2)"; - runTest(Activation.of("four", 4L)); + runTest(ImmutableMap.of("four", 4L)); source = "![0, 2, four].all(x, four/x != 2 && four/(four-x) != 2)"; - runTest(Activation.of("four", 4L)); + runTest(ImmutableMap.of("four", 4L)); + + // Unknown argument + source = "[0, 1].exists(x, x > four || true)"; + runTest(); } @Test - public void regexpMatchingTests() throws Exception { + public void regexpMatchingTests() { // Constant everything. source = "matches(\"alpha\", \"^al.*\") == true"; - runTest(Activation.EMPTY); + runTest(); source = "matches(\"alpha\", \"^.al.*\") == false"; - runTest(Activation.EMPTY); + runTest(); source = "matches(\"alpha\", \".*ha$\") == true"; - runTest(Activation.EMPTY); + runTest(); source = "matches(\"alpha\", \"^.*ha.$\") == false"; - runTest(Activation.EMPTY); + runTest(); source = "matches(\"alpha\", \"\") == true"; - runTest(Activation.EMPTY); + runTest(); source = "matches(\"alpha\", \"ph\") == true"; - runTest(Activation.EMPTY); + runTest(); source = "matches(\"alpha\", \"^ph\") == false"; - runTest(Activation.EMPTY); + runTest(); source = "matches(\"alpha\", \"ph$\") == false"; - runTest(Activation.EMPTY); + runTest(); // Constant everything, receiver-style. source = "\"alpha\".matches(\"^al.*\") == true"; - runTest(Activation.EMPTY); + runTest(); source = "\"alpha\".matches(\"^.al.*\") == false"; - runTest(Activation.EMPTY); + runTest(); source = "\"alpha\".matches(\".*ha$\") == true"; - runTest(Activation.EMPTY); + runTest(); source = "\"alpha\".matches(\".*ha.$\") == false"; - runTest(Activation.EMPTY); + runTest(); source = "\"alpha\".matches(\"\") == true"; - runTest(Activation.EMPTY); + runTest(); source = "\"alpha\".matches(\"ph\") == true"; - runTest(Activation.EMPTY); + runTest(); source = "\"alpha\".matches(\"^ph\") == false"; - runTest(Activation.EMPTY); + runTest(); source = "\"alpha\".matches(\"ph$\") == false"; - runTest(Activation.EMPTY); + runTest(); // Constant string. - declareVariable("regexp", CelTypes.STRING); + declareVariable("regexp", SimpleType.STRING); source = "matches(\"alpha\", regexp) == true"; - runTest(Activation.of("regexp", "^al.*")); + runTest(ImmutableMap.of("regexp", "^al.*")); source = "matches(\"alpha\", regexp) == false"; - runTest(Activation.of("regexp", "^.al.*")); + runTest(ImmutableMap.of("regexp", "^.al.*")); source = "matches(\"alpha\", regexp) == true"; - runTest(Activation.of("regexp", ".*ha$")); + runTest(ImmutableMap.of("regexp", ".*ha$")); source = "matches(\"alpha\", regexp) == false"; - runTest(Activation.of("regexp", ".*ha.$")); + runTest(ImmutableMap.of("regexp", ".*ha.$")); // Constant string, receiver-style. source = "\"alpha\".matches(regexp) == true"; - runTest(Activation.of("regexp", "^al.*")); + runTest(ImmutableMap.of("regexp", "^al.*")); source = "\"alpha\".matches(regexp) == false"; - runTest(Activation.of("regexp", "^.al.*")); + runTest(ImmutableMap.of("regexp", "^.al.*")); source = "\"alpha\".matches(regexp) == true"; - runTest(Activation.of("regexp", ".*ha$")); + runTest(ImmutableMap.of("regexp", ".*ha$")); source = "\"alpha\".matches(regexp) == false"; - runTest(Activation.of("regexp", ".*ha.$")); + runTest(ImmutableMap.of("regexp", ".*ha.$")); // Constant regexp. - declareVariable("s", CelTypes.STRING); + declareVariable("s", SimpleType.STRING); source = "matches(s, \"^al.*\") == true"; - runTest(Activation.of("s", "alpha")); + runTest(ImmutableMap.of("s", "alpha")); source = "matches(s, \"^.al.*\") == false"; - runTest(Activation.of("s", "alpha")); + runTest(ImmutableMap.of("s", "alpha")); source = "matches(s, \".*ha$\") == true"; - runTest(Activation.of("s", "alpha")); + runTest(ImmutableMap.of("s", "alpha")); source = "matches(s, \"^.*ha.$\") == false"; - runTest(Activation.of("s", "alpha")); + runTest(ImmutableMap.of("s", "alpha")); // Constant regexp, receiver-style. source = "s.matches(\"^al.*\") == true"; - runTest(Activation.of("s", "alpha")); + runTest(ImmutableMap.of("s", "alpha")); source = "s.matches(\"^.al.*\") == false"; - runTest(Activation.of("s", "alpha")); + runTest(ImmutableMap.of("s", "alpha")); source = "s.matches(\".*ha$\") == true"; - runTest(Activation.of("s", "alpha")); + runTest(ImmutableMap.of("s", "alpha")); source = "s.matches(\"^.*ha.$\") == false"; - runTest(Activation.of("s", "alpha")); + runTest(ImmutableMap.of("s", "alpha")); // No constants. source = "matches(s, regexp) == true"; - runTest(Activation.copyOf(ImmutableMap.of("s", "alpha", "regexp", "^al.*"))); - runTest(Activation.copyOf(ImmutableMap.of("s", "alpha", "regexp", ".*ha$"))); + runTest(ImmutableMap.of("s", "alpha", "regexp", "^al.*")); + runTest(ImmutableMap.of("s", "alpha", "regexp", ".*ha$")); source = "matches(s, regexp) == false"; - runTest(Activation.copyOf(ImmutableMap.of("s", "alpha", "regexp", "^.al.*"))); - runTest(Activation.copyOf(ImmutableMap.of("s", "alpha", "regexp", ".*ha.$"))); + runTest(ImmutableMap.of("s", "alpha", "regexp", "^.al.*")); + runTest(ImmutableMap.of("s", "alpha", "regexp", ".*ha.$")); // No constants, receiver-style. source = "s.matches(regexp) == true"; - runTest(Activation.copyOf(ImmutableMap.of("s", "alpha", "regexp", "^al.*"))); - runTest(Activation.copyOf(ImmutableMap.of("s", "alpha", "regexp", ".*ha$"))); + runTest(ImmutableMap.of("s", "alpha", "regexp", "^al.*")); + runTest(ImmutableMap.of("s", "alpha", "regexp", ".*ha$")); source = "s.matches(regexp) == false"; - runTest(Activation.copyOf(ImmutableMap.of("s", "alpha", "regexp", "^.al.*"))); - runTest(Activation.copyOf(ImmutableMap.of("s", "alpha", "regexp", ".*ha.$"))); + runTest(ImmutableMap.of("s", "alpha", "regexp", "^.al.*")); + runTest(ImmutableMap.of("s", "alpha", "regexp", ".*ha.$")); } @Test - public void regexpMatches_error() throws Exception { + public void regexpMatches_error() { source = "matches(\"alpha\", \"**\")"; - runTest(Activation.EMPTY); + runTest(); source = "\"alpha\".matches(\"**\")"; - runTest(Activation.EMPTY); + runTest(); } @Test - public void int64Conversions() throws Exception { + public void int64Conversions() { source = "int('-1')"; // string converts to -1 - runTest(Activation.EMPTY); + runTest(); source = "int(2.1)"; // double converts to 2 - runTest(Activation.EMPTY); + runTest(); + source = "int(42u)"; // converts to 42 + runTest(); + } + + @Test + public void int64Conversions_error() { source = "int(18446744073709551615u)"; // 2^64-1 should error - runTest(Activation.EMPTY); + runTest(); source = "int(1e99)"; // out of range should error - runTest(Activation.EMPTY); - - source = "int(42u)"; // converts to 42 - runTest(Activation.EMPTY); + runTest(); } @Test - public void uint64Conversions() throws Exception { + public void uint64Conversions() { // The test case `uint(1e19)` succeeds with unsigned longs and fails with longs in a way that // cannot be easily tested. - if (!eval.celOptions().enableUnsignedLongs()) { + if (!BASE_CEL_OPTIONS.enableUnsignedLongs()) { skipBaselineVerification(); return; } source = "uint('1')"; // string converts to 1u - runTest(Activation.EMPTY); + runTest(); source = "uint(2.1)"; // double converts to 2u - runTest(Activation.EMPTY); - - source = "uint(-1)"; // should error - runTest(Activation.EMPTY); + runTest(); source = "uint(1e19)"; // valid uint but outside of int range - runTest(Activation.EMPTY); - - source = "uint(6.022e23)"; // outside uint range - runTest(Activation.EMPTY); + runTest(); source = "uint(42)"; // int converts to 42u - runTest(Activation.EMPTY); + runTest(); + + source = "uint(1u)"; // identity + runTest(); + + source = "uint(dyn(1u))"; // identity, check dynamic dispatch + runTest(); + } + + @Test + public void uint64Conversions_error() { + source = "uint(-1)"; // should error + runTest(); + + source = "uint(6.022e23)"; // outside uint range + runTest(); source = "uint('f1')"; // should error - runTest(Activation.EMPTY); + runTest(); } @Test - public void doubleConversions() throws Exception { + public void doubleConversions() { source = "double('1.1')"; // string converts to 1.1 - runTest(Activation.EMPTY); + runTest(); source = "double(2u)"; // uint converts to 2.0 - runTest(Activation.EMPTY); + runTest(); source = "double(-1)"; // int converts to -1.0 - runTest(Activation.EMPTY); + runTest(); + source = "double(1.5)"; // Identity + runTest(); + } + + @Test + public void doubleConversions_error() { source = "double('bad')"; - runTest(Activation.EMPTY); + runTest(); } @Test - public void stringConversions() throws Exception { + public void stringConversions() { source = "string(1.1)"; // double converts to '1.1' - runTest(Activation.EMPTY); + runTest(); source = "string(2u)"; // uint converts to '2' - runTest(Activation.EMPTY); + runTest(); source = "string(-1)"; // int converts to '-1' - runTest(Activation.EMPTY); + runTest(); + + source = "string(true)"; // bool converts to 'true' + runTest(); // Byte literals in Google SQL only take the leading byte of an escape character. // This means that to translate a byte literal to a UTF-8 encoded string, all bytes must be // encoded in the literal as they would be laid out in memory for UTF-8, hence the extra octal // escape to achieve parity with the bidi test below. source = "string(b'abc\\303\\203')"; - runTest(Activation.EMPTY); // bytes convert to 'abcÃ' + runTest(); // bytes convert to 'abcÃ' // Bi-di conversion for strings and bytes for 'abcÃ', note the difference between the string // and byte literal values. source = "string(bytes('abc\\303'))"; - runTest(Activation.EMPTY); + runTest(); source = "string(timestamp('2009-02-13T23:31:30Z'))"; - runTest(Activation.EMPTY); + runTest(); source = "string(duration('1000000s'))"; - runTest(Activation.EMPTY); + runTest(); + + source = "string('hello')"; // Identity + runTest(); + } + + @Test + public void stringConversions_error() throws Exception { + source = "string(b'\\xff')"; + runTest(); } @Test @@ -1582,52 +1775,85 @@ public void bytes() throws Exception { source = "b'a' < b'b' && b'a' <= b'b' && b'b' > b'a' && b'a' >= b'a' && b'a' == b'a' && b'a' !=" + " b'b'"; - runTest(Activation.EMPTY); + runTest(); } @Test - public void bytesConversions() throws Exception { + public void boolConversions() { + source = "bool(true)"; + runTest(); // Identity + + source = "bool('true') && bool('TRUE') && bool('True') && bool('t') && bool('1')"; + runTest(); // result is true + + source = "bool('false') || bool('FALSE') || bool('False') || bool('f') || bool('0')"; + runTest(); // result is false + } + + @Test + public void boolConversions_error() { + source = "bool('TrUe')"; + runTest(); + + source = "bool('FaLsE')"; + runTest(); + } + + @Test + public void bytesConversions() { source = "bytes('abc\\303')"; - runTest(Activation.EMPTY); // string converts to abcà in bytes form. + runTest(); // string converts to abcà in bytes form. + + source = "bytes(bytes('abc\\303'))"; // Identity + runTest(); } @Test - public void dynConversions() throws Exception { + public void dynConversions() { source = "dyn(42)"; - runTest(Activation.EMPTY); + runTest(); source = "dyn({'a':1, 'b':2})"; - runTest(Activation.EMPTY); + runTest(); + } + + @Test + public void dyn_error() { + source = "dyn('hello').invalid"; + runTest(); + + source = "has(dyn('hello').invalid)"; + runTest(); + + source = "dyn([]).invalid"; + runTest(); + + source = "has(dyn([]).invalid)"; + runTest(); } - // This lambda implements @Immutable interface 'Function', but 'InterpreterTest' has field 'eval' - // of type 'com.google.api.expr.cel.testing.Eval', the declaration of - // type - // 'com.google.api.expr.cel.testing.Eval' is not annotated with - // @com.google.errorprone.annotations.Immutable - @SuppressWarnings("Immutable") @Test - public void jsonValueTypes() throws Exception { - container = TestAllTypes.getDescriptor().getFile().getPackage(); - declareVariable("x", CelTypes.createMessage(TestAllTypes.getDescriptor().getFullName())); + public void jsonValueTypes() { + setContainer(CelContainer.ofName(TestAllTypes.getDescriptor().getFile().getPackage())); + declareVariable("x", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())); // JSON bool selection. TestAllTypes xBool = TestAllTypes.newBuilder().setSingleValue(Value.newBuilder().setBoolValue(true)).build(); source = "x.single_value"; - runTest(Activation.of("x", xBool)); + runTest(ImmutableMap.of("x", xBool)); // JSON number selection with int comparison. TestAllTypes xInt = TestAllTypes.newBuilder().setSingleValue(Value.newBuilder().setNumberValue(1)).build(); source = "x.single_value == double(1)"; - runTest(Activation.of("x", xInt)); + runTest(ImmutableMap.of("x", xInt)); // JSON number selection with float comparison. TestAllTypes xFloat = TestAllTypes.newBuilder().setSingleValue(Value.newBuilder().setNumberValue(1.1)).build(); source = "x.single_value == 1.1"; - runTest(Activation.of("x", xFloat)); + runTest(ImmutableMap.of("x", xFloat)); // JSON null selection. TestAllTypes xNull = @@ -1635,7 +1861,7 @@ public void jsonValueTypes() throws Exception { .setSingleValue(Value.newBuilder().setNullValue(NullValue.NULL_VALUE)) .build(); source = "x.single_value == null"; - runTest(Activation.of("x", xNull)); + runTest(ImmutableMap.of("x", xNull)); // JSON string selection. TestAllTypes xString = @@ -1643,7 +1869,24 @@ public void jsonValueTypes() throws Exception { .setSingleValue(Value.newBuilder().setStringValue("hello")) .build(); source = "x.single_value == 'hello'"; - runTest(Activation.of("x", xString)); + 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 = @@ -1660,7 +1903,7 @@ public void jsonValueTypes() throws Exception { .addValues(Value.newBuilder().setNumberValue(-1.1)))) .build(); source = "x.single_value[0] == [['hello'], -1.1][0]"; - runTest(Activation.of("x", xList)); + runTest(ImmutableMap.of("x", xList)); // JSON struct equality. TestAllTypes xStruct = @@ -1677,7 +1920,7 @@ public void jsonValueTypes() throws Exception { .putFields("num", Value.newBuilder().setNumberValue(-1.1).build())) .build(); source = "x.single_struct.num == {'str': ['hello'], 'num': -1.1}['num']"; - runTest(Activation.of("x", xStruct)); + runTest(ImmutableMap.of("x", xStruct)); // Build a proto message using a dynamically constructed map and assign the map to a struct // value. @@ -1686,33 +1929,46 @@ public void jsonValueTypes() throws Exception { + "single_struct: " + "TestAllTypes{single_value: {'str': ['hello']}}.single_value" + "}"; - runTest(Activation.EMPTY); + runTest(); // Ensure that types are being wrapped and unwrapped on function dispatch. declareFunction( "pair", - globalOverload("pair", ImmutableList.of(CelTypes.STRING, CelTypes.STRING), CelTypes.DYN)); - eval.registrar() - .add( + globalOverload( + "pair", ImmutableList.of(SimpleType.STRING, SimpleType.STRING), SimpleType.DYN)); + addFunctionBinding( + CelFunctionBinding.from( "pair", ImmutableList.of(String.class, String.class), (Object[] args) -> { String key = (String) args[0]; String val = (String) args[1]; - return eval.adapt( - Value.newBuilder() - .setStructValue( - Struct.newBuilder() - .putFields(key, Value.newBuilder().setStringValue(val).build())) - .build()); - }); + return Value.newBuilder() + .setStructValue( + Struct.newBuilder() + .putFields(key, Value.newBuilder().setStringValue(val).build())) + .build(); + })); source = "pair(x.single_struct.str[0], 'val')"; - runTest(Activation.of("x", xStruct)); + runTest(ImmutableMap.of("x", xStruct)); + } + + @Test + public void jsonConversions() { + declareVariable("ts", SimpleType.TIMESTAMP); + declareVariable("du", SimpleType.DURATION); + source = "google.protobuf.Struct { fields: {'timestamp': ts, 'duration': du } }"; + runTest( + ImmutableMap.of( + "ts", + ProtoTimeUtils.fromSecondsToTimestamp(100), + "du", + ProtoTimeUtils.fromMillisToDuration(200))); } @Test - public void typeComparisons() throws Exception { - container = TestAllTypes.getDescriptor().getFile().getPackage(); + public void typeComparisons() { + setContainer(CelContainer.ofName(TestAllTypes.getDescriptor().getFile().getPackage())); // Test numeric types. source = @@ -1720,49 +1976,58 @@ public void typeComparisons() throws Exception { + "type(1u) != int && type(1) != uint && " + "type(uint(1.1)) == uint && " + "type(1.1) == double"; - runTest(Activation.EMPTY); + runTest(); // Test string and bytes types. source = "type('hello') == string && type(b'\277') == bytes"; - runTest(Activation.EMPTY); + runTest(); // Test list and map types. source = "type([1, 2, 3]) == list && type({'a': 1, 'b': 2}) == map"; - runTest(Activation.EMPTY); + runTest(); // Test bool types. source = "type(true) == bool && type(false) == bool"; - runTest(Activation.EMPTY); + runTest(); // Test well-known proto-based types. source = "type(duration('10s')) == google.protobuf.Duration"; - runTest(Activation.EMPTY); + runTest(); // Test external proto-based types with container resolution. source = "type(TestAllTypes{}) == TestAllTypes && " + "type(TestAllTypes{}) == proto3.TestAllTypes && " - + "type(TestAllTypes{}) == .dev.cel.testing.testdata.proto3.TestAllTypes && " + + "type(TestAllTypes{}) == .cel.expr.conformance.proto3.TestAllTypes && " + "type(proto3.TestAllTypes{}) == TestAllTypes && " + "type(proto3.TestAllTypes{}) == proto3.TestAllTypes && " - + "type(proto3.TestAllTypes{}) == .dev.cel.testing.testdata.proto3.TestAllTypes && " - + "type(.dev.cel.testing.testdata.proto3.TestAllTypes{}) == TestAllTypes && " - + "type(.dev.cel.testing.testdata.proto3.TestAllTypes{}) == proto3.TestAllTypes && " - + "type(.dev.cel.testing.testdata.proto3.TestAllTypes{}) == " - + ".dev.cel.testing.testdata.proto3.TestAllTypes"; - runTest(Activation.EMPTY); + + "type(proto3.TestAllTypes{}) == .cel.expr.conformance.proto3.TestAllTypes && " + + "type(.cel.expr.conformance.proto3.TestAllTypes{}) == TestAllTypes && " + + "type(.cel.expr.conformance.proto3.TestAllTypes{}) == proto3.TestAllTypes && " + + "type(.cel.expr.conformance.proto3.TestAllTypes{}) == " + + ".cel.expr.conformance.proto3.TestAllTypes"; + runTest(); // Test whether a type name is recognized as a type. source = "type(TestAllTypes) == type"; - runTest(Activation.EMPTY); + runTest(); // Test whether the type resolution of a proto object is recognized as the message's type. source = "type(TestAllTypes{}) == TestAllTypes"; - runTest(Activation.EMPTY); + runTest(); // Test whether null resolves to null_type. source = "type(null) == null_type"; - runTest(Activation.EMPTY); + runTest(); + + // Test runtime resolution of types + source = + "type(duration) == google.protobuf.Duration && " + + "type(timestamp) == google.protobuf.Timestamp"; + // Intentionally declare as dyns + declareVariable("duration", SimpleType.DYN); + declareVariable("timestamp", SimpleType.DYN); + runTest(ImmutableMap.of("duration", java.time.Duration.ZERO, "timestamp", Instant.EPOCH)); } @Test @@ -1779,7 +2044,7 @@ public void wrappers() throws Exception { .setSingleUint32Wrapper(UInt32Value.of(12)) .setSingleUint64Wrapper(UInt64Value.of(34)); - declareVariable("x", CelTypes.createMessage(TestAllTypes.getDescriptor().getFullName())); + declareVariable("x", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())); source = "x.single_bool_wrapper == true && " + "x.single_bytes_wrapper == b'hi' && " @@ -1790,7 +2055,7 @@ public void wrappers() throws Exception { + "x.single_string_wrapper == 'hello' && " + "x.single_uint32_wrapper == 12u && " + "x.single_uint64_wrapper == 34u"; - runTest(Activation.of("x", wrapperBindings)); + runTest(ImmutableMap.of("x", wrapperBindings)); source = "x.single_bool_wrapper == google.protobuf.BoolValue{} && " @@ -1803,12 +2068,39 @@ public void wrappers() throws Exception { + "x.single_uint32_wrapper == google.protobuf.UInt32Value{value: 12u} && " + "x.single_uint64_wrapper == google.protobuf.UInt64Value{value: 34u}"; runTest( - Activation.of( + ImmutableMap.of( "x", wrapperBindings .setSingleBoolWrapper(BoolValue.getDefaultInstance()) .setSingleStringWrapper(StringValue.getDefaultInstance()))); + source = + "x.repeated_int32_wrapper == [1,2] && " + + "x.repeated_int64_wrapper == [3] && " + + "x.repeated_float_wrapper == [1.5, 2.5] && " + + "x.repeated_double_wrapper == [3.5, 4.5] && " + + "x.repeated_string_wrapper == ['foo', 'bar'] && " + + "x.repeated_bool_wrapper == [true] && " + + "x.repeated_uint32_wrapper == [1u, 2u] && " + + "x.repeated_uint64_wrapper == []"; + + runTest( + ImmutableMap.of( + "x", + wrapperBindings + .addRepeatedInt32Wrapper(Int32Value.of(1)) + .addRepeatedInt32Wrapper(Int32Value.of(2)) + .addRepeatedInt64Wrapper(Int64Value.of(3)) + .addRepeatedFloatWrapper(FloatValue.of(1.5f)) + .addRepeatedFloatWrapper(FloatValue.of(2.5f)) + .addRepeatedDoubleWrapper(DoubleValue.of(3.5f)) + .addRepeatedDoubleWrapper(DoubleValue.of(4.5f)) + .addRepeatedStringWrapper(StringValue.of("foo")) + .addRepeatedStringWrapper(StringValue.of("bar")) + .addRepeatedBoolWrapper(BoolValue.of(true)) + .addRepeatedUint32Wrapper(UInt32Value.of(1)) + .addRepeatedUint32Wrapper(UInt32Value.of(2)))); + source = "x.single_bool_wrapper == null && " + "x.single_bytes_wrapper == null && " @@ -1819,89 +2111,181 @@ public void wrappers() throws Exception { + "x.single_string_wrapper == null && " + "x.single_uint32_wrapper == null && " + "x.single_uint64_wrapper == null"; - runTest(Activation.of("x", TestAllTypes.getDefaultInstance())); + runTest(ImmutableMap.of("x", TestAllTypes.getDefaultInstance())); + + declareVariable("dyn_var", SimpleType.DYN); + source = "dyn_var"; + runTest(ImmutableMap.of("dyn_var", NullValue.NULL_VALUE)); + + clearAllDeclarations(); + declareVariable("int32_list", ListType.create(SimpleType.INT)); + declareVariable("int64_list", ListType.create(SimpleType.INT)); + declareVariable("uint32_list", ListType.create(SimpleType.UINT)); + declareVariable("uint64_list", ListType.create(SimpleType.UINT)); + declareVariable("float_list", ListType.create(SimpleType.DOUBLE)); + declareVariable("double_list", ListType.create(SimpleType.DOUBLE)); + declareVariable("bool_list", ListType.create(SimpleType.BOOL)); + declareVariable("string_list", ListType.create(SimpleType.STRING)); + declareVariable("bytes_list", ListType.create(SimpleType.BYTES)); + + setContainer(CelContainer.ofName(TestAllTypes.getDescriptor().getFullName())); + source = + "TestAllTypes{repeated_int32: int32_list}.repeated_int32 == [1] && " + + "TestAllTypes{repeated_int64: int64_list}.repeated_int64 == [2] && " + + "TestAllTypes{repeated_uint32: uint32_list}.repeated_uint32 == [3u] && " + + "TestAllTypes{repeated_uint64: uint64_list}.repeated_uint64 == [4u] && " + + "TestAllTypes{repeated_float: float_list}.repeated_float == [5.5] && " + + "TestAllTypes{repeated_double: double_list}.repeated_double == [6.6] && " + + "TestAllTypes{repeated_bool: bool_list}.repeated_bool == [true] && " + + "TestAllTypes{repeated_string: string_list}.repeated_string == ['hello'] && " + + "TestAllTypes{repeated_bytes: bytes_list}.repeated_bytes == [b'world']"; + + runTest( + ImmutableMap.builder() + .put("int32_list", ImmutableList.of(Int32Value.of(1))) + .put("int64_list", ImmutableList.of(Int64Value.of(2))) + .put("uint32_list", ImmutableList.of(UInt32Value.of(3))) + .put("uint64_list", ImmutableList.of(UInt64Value.of(4))) + .put("float_list", ImmutableList.of(FloatValue.of(5.5f))) + .put("double_list", ImmutableList.of(DoubleValue.of(6.6))) + .put("bool_list", ImmutableList.of(BoolValue.of(true))) + .put("string_list", ImmutableList.of(StringValue.of("hello"))) + .put("bytes_list", ImmutableList.of(BytesValue.of(ByteString.copyFromUtf8("world")))) + .buildOrThrow()); + + clearAllDeclarations(); + // Currently allowed, but will be an error + // See https://github.com/google/cel-spec/pull/501 + source = "google.protobuf.Timestamp{ seconds: 253402300800 }"; + 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() throws Exception { + public void longComprehension() { ImmutableList l = LongStream.range(0L, 1000L).boxed().collect(toImmutableList()); - eval.registrar().add("constantLongList", ImmutableList.of(), args -> l); + addFunctionBinding( + CelFunctionBinding.from("constantLongList", ImmutableList.of(), unused -> l)); // Comprehension over compile-time constant long list. declareFunction( "constantLongList", - globalOverload( - "constantLongList", ImmutableList.of(), CelTypes.createList(CelTypes.INT64))); + globalOverload("constantLongList", ImmutableList.of(), ListType.create(SimpleType.INT))); source = "size(constantLongList().map(x, x+1)) == 1000"; - runTest(Activation.EMPTY); + runTest(); // Comprehension over long list that is not compile-time constant. - declareVariable("longlist", CelTypes.createList(CelTypes.INT64)); + declareVariable("longlist", ListType.create(SimpleType.INT)); source = "size(longlist.map(x, x+1)) == 1000"; - runTest(Activation.of("longlist", l)); + runTest(ImmutableMap.of("longlist", l)); // Comprehension over long list where the computation is very slow. // (This is here pro-forma only since in the synchronous interpreter there // is no notion of a computation being slow so that another computation can // build up a stack while waiting.) - eval.registrar().add("f_slow_inc", Long.class, n -> n + 1L); - eval.registrar().add("f_unleash", Object.class, x -> x); + addFunctionBinding( + CelFunctionBinding.from("f_slow_inc", Long.class, n -> n + 1L), + CelFunctionBinding.from("f_unleash", Object.class, x -> x)); declareFunction( "f_slow_inc", - globalOverload("f_slow_inc", ImmutableList.of(CelTypes.INT64), CelTypes.INT64)); + globalOverload("f_slow_inc", ImmutableList.of(SimpleType.INT), SimpleType.INT)); declareFunction( "f_unleash", globalOverload( - "f_unleash", - ImmutableList.of(CelTypes.createTypeParam("A")), - ImmutableList.of("A"), - CelTypes.createTypeParam("A"))); + "f_unleash", ImmutableList.of(TypeParamType.create("A")), TypeParamType.create("A"))); source = "f_unleash(longlist.map(x, f_slow_inc(x)))[0] == 1"; - runTest(Activation.of("longlist", l)); + runTest(ImmutableMap.of("longlist", l)); } @Test - public void maxComprehension() throws Exception { - if (eval.celOptions().comprehensionMaxIterations() < 0) { - skipBaselineVerification(); - return; - } + public void maxComprehension() { // Comprehension over long list that is not compile-time constant. - declareVariable("longlist", CelTypes.createList(CelTypes.INT64)); + declareVariable("longlist", ListType.create(SimpleType.INT)); source = "size(longlist.map(x, x+1)) == 1000"; // Comprehension which exceeds the configured iteration limit. ImmutableList tooLongList = LongStream.range(0L, COMPREHENSION_MAX_ITERATIONS + 1).boxed().collect(toImmutableList()); - runTest(Activation.of("longlist", tooLongList)); + runTest(ImmutableMap.of("longlist", tooLongList)); // Sequential iterations within the collective limit of 1000. source = "longlist.filter(i, i % 2 == 0).map(i, i * 2).map(i, i / 2).size() == 250"; ImmutableList l = LongStream.range(0L, COMPREHENSION_MAX_ITERATIONS / 2).boxed().collect(toImmutableList()); - runTest(Activation.of("longlist", l)); + runTest(ImmutableMap.of("longlist", l)); // Sequential iterations outside the limit of 1000. source = "(longlist + [0]).filter(i, i % 2 == 0).map(i, i * 2).map(i, i / 2).size() == 251"; - runTest(Activation.of("longlist", l)); + runTest(ImmutableMap.of("longlist", l)); // Nested iteration within the iteration limit. // Note, there is some double-counting of the inner-loops which causes the iteration limit to // get tripped sooner than one might expect for the nested case. source = "longlist.map(i, longlist.map(j, longlist.map(k, [i, j, k]))).size() == 9"; l = LongStream.range(0L, 9).boxed().collect(toImmutableList()); - runTest(Activation.of("longlist", l)); + runTest(ImmutableMap.of("longlist", l)); // Nested iteration which exceeds the iteration limit. This result may be surprising, but the // limit is tripped precisely because each complete iteration of an inner-loop counts as inner- // loop + 1 as there's not a clean way to deduct an iteration and only count the inner most // loop. l = LongStream.range(0L, 10).boxed().collect(toImmutableList()); - runTest(Activation.of("longlist", l)); + runTest(ImmutableMap.of("longlist", l)); } @Test - public void dynamicMessage() throws Exception { + public void dynamicMessage_adapted() throws Exception { TestAllTypes wrapperBindings = TestAllTypes.newBuilder() .setSingleAny(Any.pack(NestedMessage.newBuilder().setBb(42).build())) @@ -1914,70 +2298,377 @@ public void dynamicMessage() throws Exception { .setSingleStringWrapper(StringValue.of("hello")) .setSingleUint32Wrapper(UInt32Value.of(12)) .setSingleUint64Wrapper(UInt64Value.of(34)) - .setSingleDuration(Duration.newBuilder().setSeconds(10).setNanos(20)) + .setSingleDuration( + com.google.protobuf.Duration.newBuilder().setSeconds(10).setNanos(20)) .setSingleTimestamp(Timestamp.newBuilder().setSeconds(100).setNanos(200)) .setSingleValue(Value.newBuilder().setStringValue("a")) .setSingleStruct( Struct.newBuilder().putFields("b", Value.newBuilder().setStringValue("c").build())) - .setSingleListValue( + .setListValue( ListValue.newBuilder().addValues(Value.newBuilder().setStringValue("d")).build()) .build(); - Activation activation = - Activation.of( + ImmutableMap input = + ImmutableMap.of( "msg", DynamicMessage.parseFrom( TestAllTypes.getDescriptor(), wrapperBindings.toByteArray(), DefaultDescriptorPool.INSTANCE.getExtensionRegistry())); - declareVariable("msg", CelTypes.createMessage(TestAllTypes.getDescriptor().getFullName())); + declareVariable("msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())); source = "msg.single_any"; - assertThat(runTest(activation)).isInstanceOf(NestedMessage.class); + assertThat(runTest(input)).isInstanceOf(NestedMessage.class); source = "msg.single_bool_wrapper"; - assertThat(runTest(activation)).isInstanceOf(Boolean.class); + assertThat(runTest(input)).isInstanceOf(Boolean.class); source = "msg.single_bytes_wrapper"; - assertThat(runTest(activation)).isInstanceOf(String.class); + assertThat(runTest(input)).isInstanceOf(String.class); source = "msg.single_double_wrapper"; - assertThat(runTest(activation)).isInstanceOf(Double.class); + assertThat(runTest(input)).isInstanceOf(Double.class); source = "msg.single_float_wrapper"; - assertThat(runTest(activation)).isInstanceOf(Double.class); + assertThat(runTest(input)).isInstanceOf(Double.class); source = "msg.single_int32_wrapper"; - assertThat(runTest(activation)).isInstanceOf(Long.class); + assertThat(runTest(input)).isInstanceOf(Long.class); source = "msg.single_int64_wrapper"; - assertThat(runTest(activation)).isInstanceOf(Long.class); + assertThat(runTest(input)).isInstanceOf(Long.class); source = "msg.single_string_wrapper"; - assertThat(runTest(activation)).isInstanceOf(String.class); + assertThat(runTest(input)).isInstanceOf(String.class); source = "msg.single_uint32_wrapper"; - assertThat(runTest(activation)) - .isInstanceOf(eval.celOptions().enableUnsignedLongs() ? UnsignedLong.class : Long.class); + assertThat(runTest(input)) + .isInstanceOf(BASE_CEL_OPTIONS.enableUnsignedLongs() ? UnsignedLong.class : Long.class); source = "msg.single_uint64_wrapper"; - assertThat(runTest(activation)) - .isInstanceOf(eval.celOptions().enableUnsignedLongs() ? UnsignedLong.class : Long.class); + assertThat(runTest(input)) + .isInstanceOf(BASE_CEL_OPTIONS.enableUnsignedLongs() ? UnsignedLong.class : Long.class); source = "msg.single_duration"; - assertThat(runTest(activation)).isInstanceOf(Duration.class); + assertThat(runTest(input)).isInstanceOf(java.time.Duration.class); source = "msg.single_timestamp"; - assertThat(runTest(activation)).isInstanceOf(Timestamp.class); + assertThat(runTest(input)).isInstanceOf(Instant.class); source = "msg.single_value"; - assertThat(runTest(activation)).isInstanceOf(String.class); + assertThat(runTest(input)).isInstanceOf(String.class); source = "msg.single_struct"; - assertThat(runTest(activation)).isInstanceOf(Map.class); + assertThat(runTest(input)).isInstanceOf(Map.class); + + source = "msg.list_value"; + assertThat(runTest(input)).isInstanceOf(List.class); + } + + @Test + public void dynamicMessage_dynamicDescriptor() throws Exception { + setContainer(CelContainer.ofName("dev.cel.testing.testdata.serialized.proto3")); + + source = "TestAllTypes {}"; + assertThat(runTest()).isInstanceOf(DynamicMessage.class); + source = "TestAllTypes { single_int32: 1, single_int64: 2, single_string: 'hello'}"; + assertThat(runTest()).isInstanceOf(DynamicMessage.class); + source = + "TestAllTypes { single_int32: 1, single_int64: 2, single_string: 'hello'}.single_string"; + assertThat(runTest()).isInstanceOf(String.class); + + // Test wrappers + source = "TestAllTypes { single_int32_wrapper: 3 }.single_int32_wrapper"; + assertThat(runTest()).isInstanceOf(Long.class); + source = "TestAllTypes { single_int64_wrapper: 3 }.single_int64_wrapper"; + assertThat(runTest()).isInstanceOf(Long.class); + source = "TestAllTypes { single_bool_wrapper: true }.single_bool_wrapper"; + assertThat(runTest()).isInstanceOf(Boolean.class); + source = "TestAllTypes { single_bytes_wrapper: b'abc' }.single_bytes_wrapper"; + assertThat(runTest()).isInstanceOf(String.class); + source = "TestAllTypes { single_float_wrapper: 1.1 }.single_float_wrapper"; + assertThat(runTest()).isInstanceOf(Double.class); + source = "TestAllTypes { single_double_wrapper: 1.1 }.single_double_wrapper"; + assertThat(runTest()).isInstanceOf(Double.class); + source = "TestAllTypes { single_uint32_wrapper: 2u}.single_uint32_wrapper"; + assertThat(runTest()) + .isInstanceOf(BASE_CEL_OPTIONS.enableUnsignedLongs() ? UnsignedLong.class : Long.class); + source = "TestAllTypes { single_uint64_wrapper: 2u}.single_uint64_wrapper"; + assertThat(runTest()) + .isInstanceOf(BASE_CEL_OPTIONS.enableUnsignedLongs() ? UnsignedLong.class : Long.class); + source = "TestAllTypes { single_list_value: ['a', 1.5, true] }.single_list_value"; + assertThat(runTest()).isInstanceOf(List.class); + + // Test nested messages + source = + "TestAllTypes { standalone_message: TestAllTypes.NestedMessage { } }.standalone_message"; + assertThat(runTest()).isInstanceOf(DynamicMessage.class); + source = + "TestAllTypes { standalone_message: TestAllTypes.NestedMessage { bb: 5}" + + " }.standalone_message.bb"; + assertThat(runTest()).isInstanceOf(Long.class); + source = "TestAllTypes { standalone_enum: TestAllTypes.NestedEnum.BAR }.standalone_enum"; + assertThat(runTest()).isInstanceOf(Long.class); + source = "TestAllTypes { map_string_string: {'key': 'value'}}"; + assertThat(runTest()).isInstanceOf(DynamicMessage.class); + source = "TestAllTypes { map_string_string: {'key': 'value'}}.map_string_string"; + assertThat(runTest()).isInstanceOf(Map.class); + source = "TestAllTypes { map_string_string: {'key': 'value'}}.map_string_string['key']"; + assertThat(runTest()).isInstanceOf(String.class); + + // Test any unpacking + // With well-known type + Any anyDuration = Any.pack(ProtoTimeUtils.fromSecondsToDuration(100)); + declareVariable("dur", SimpleType.TIMESTAMP); + source = "TestAllTypes { single_any: dur }.single_any"; + assertThat(runTest(ImmutableMap.of("dur", anyDuration))).isInstanceOf(Duration.class); + // with custom message + clearAllDeclarations(); + Any anyTestMsg = Any.pack(TestAllTypes.newBuilder().setSingleString("hello").build()); + declareVariable( + "any_packed_test_msg", + StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())); + source = "TestAllTypes { single_any: any_packed_test_msg }.single_any"; + assertThat(runTest(ImmutableMap.of("any_packed_test_msg", anyTestMsg))) + .isInstanceOf(TestAllTypes.class); + + // Test JSON map behavior + declareVariable( + "test_msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())); + declareVariable( + "dynamic_msg", StructTypeReference.create(TEST_ALL_TYPE_DYNAMIC_DESCRIPTOR.getFullName())); + DynamicMessage.Builder dynamicMessageBuilder = + DynamicMessage.newBuilder(TEST_ALL_TYPE_DYNAMIC_DESCRIPTOR); + JsonFormat.parser().merge("{ 'map_string_string' : { 'foo' : 'bar' } }", dynamicMessageBuilder); + ImmutableMap input = + ImmutableMap.of("dynamic_msg", dynamicMessageBuilder.build()); + + source = "dynamic_msg"; + assertThat(runTest(input)).isInstanceOf(DynamicMessage.class); + source = "dynamic_msg.map_string_string"; + assertThat(runTest(input)).isInstanceOf(Map.class); + source = "dynamic_msg.map_string_string['foo']"; + assertThat(runTest(input)).isInstanceOf(String.class); + + // Test function dispatch + declareFunction( + "f_msg", + globalOverload( + "f_msg_generated", + ImmutableList.of( + StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())), + SimpleType.BOOL), + globalOverload( + "f_msg_dynamic", + ImmutableList.of( + StructTypeReference.create(TEST_ALL_TYPE_DYNAMIC_DESCRIPTOR.getFullName())), + SimpleType.BOOL)); + addFunctionBinding( + CelFunctionBinding.fromOverloads( + "f_msg", + CelFunctionBinding.from("f_msg_generated", TestAllTypes.class, unused -> true), + CelFunctionBinding.from("f_msg_dynamic", DynamicMessage.class, unused -> true))); + input = + ImmutableMap.of( + "dynamic_msg", dynamicMessageBuilder.build(), + "test_msg", TestAllTypes.newBuilder().setSingleInt64(10L).build()); + + source = "f_msg(dynamic_msg)"; + assertThat(runTest(input)).isInstanceOf(Boolean.class); + source = "f_msg(test_msg)"; + assertThat(runTest(input)).isInstanceOf(Boolean.class); + } + + @Immutable + private static final class RecordedValues { + @SuppressWarnings("Immutable") + private final Map recordedValues = new HashMap<>(); + + @CanIgnoreReturnValue + private Object record(String key, Object value) { + recordedValues.put(key, value); + return value; + } + + private ImmutableMap getRecordedValues() { + return ImmutableMap.copyOf(recordedValues); + } + } + + @Test + public void lateBoundFunctions() throws Exception { + RecordedValues recordedValues = new RecordedValues(); + CelLateFunctionBindings lateBindings = + CelLateFunctionBindings.from( + CelFunctionBinding.from( + "record_string_dyn", String.class, Object.class, recordedValues::record)); + declareFunction( + "record", + globalOverload( + "record_string_dyn", + ImmutableList.of(SimpleType.STRING, SimpleType.DYN), + SimpleType.DYN)); + source = "record('foo', 'bar')"; + assertThat(runTest(ImmutableMap.of(), lateBindings)).isEqualTo("bar"); + assertThat(recordedValues.getRecordedValues()).containsExactly("foo", "bar"); + } + + @Test + public void jsonFieldNames() throws Exception { + this.celOptions = celOptions.toBuilder().enableJsonFieldNames(true).build(); + this.celRuntime = newBaseRuntimeBuilder(celOptions).build(); + + TestAllTypes message = TestAllTypes.newBuilder().setSingleInt32(42).build(); + declareVariable("x", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())); + + source = "x.singleInt32 == 42"; + assertThat(runTest(ImmutableMap.of("x", message))).isEqualTo(true); + + source = "TestAllTypes{singleInt32: 42}.singleInt32 == 42"; + setContainer(CelContainer.ofName(TestAllTypes.getDescriptor().getFile().getPackage())); + assertThat(runTest()).isEqualTo(true); + + skipBaselineVerification(); + } + + /** + * Checks that the CheckedExpr produced by CelCompiler is equal to the one reproduced by the + * native CelAbstractSyntaxTree + */ + private static void assertAstRoundTrip(CelAbstractSyntaxTree ast) { + if (ast.isChecked()) { + CheckedExpr checkedExpr = CelProtoAbstractSyntaxTree.fromCelAst(ast).toCheckedExpr(); + CelProtoAbstractSyntaxTree protoAst = CelProtoAbstractSyntaxTree.fromCelAst(ast); + assertThat(checkedExpr).isEqualTo(protoAst.toCheckedExpr()); + } else { + ParsedExpr parsedExpr = CelProtoAbstractSyntaxTree.fromCelAst(ast).toParsedExpr(); + CelProtoAbstractSyntaxTree protoAst = CelProtoAbstractSyntaxTree.fromCelAst(ast); + assertThat(parsedExpr).isEqualTo(protoAst.toParsedExpr()); + } + } - source = "msg.single_list_value"; - assertThat(runTest(activation)).isInstanceOf(List.class); + private static String readResourceContent(String path) throws IOException { + return Resources.toString(Resources.getResource(Ascii.toLowerCase(path)), UTF_8); + } + + @SuppressWarnings("unchecked") + private void printBinding(Object input, CelAttributePattern... patterns) { + if (input instanceof Map) { + Map inputMap = (Map) input; + if (inputMap.isEmpty() && patterns.length == 0) { + println("bindings: {}"); + return; + } + + boolean first = true; + StringBuilder sb = new StringBuilder().append("{"); + for (Map.Entry entry : inputMap.entrySet()) { + if (!first) { + sb.append(", "); + } + first = false; + sb.append(entry.getKey()); + sb.append("="); + Object value = entry.getValue(); + if (value instanceof CelByteString) { + sb.append(getHumanReadableString((CelByteString) value)); + } else { + 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 { + if (patterns.length > 0) { + println("bindings: " + input + ", unknown_attributes=" + Arrays.toString(patterns)); + } else { + println("bindings: " + input); + } + } + } + + private static String getHumanReadableString(CelByteString byteString) { + // Very unfortunate we have to do this at all + StringBuilder sb = new StringBuilder(); + sb.append("["); + byte[] bytes = byteString.toByteArray(); + for (int i = 0; i < bytes.length; i++) { + byte b = bytes[i]; + sb.append(b); + if (i < bytes.length - 1) { + sb.append(", "); + } + } + sb.append("]"); + return sb.toString(); + } + + private static final class TestOnlyVariableResolver implements CelVariableResolver { + private final Map map; + + private static TestOnlyVariableResolver newInstance(Map map) { + return new TestOnlyVariableResolver(map); + } + + @Override + public Optional find(String name) { + return Optional.ofNullable(map.get(name)); + } + + @Override + public String toString() { + return UnredactedDebugFormatForTest.unredactedToString(map); + } + + private TestOnlyVariableResolver(Map map) { + this.map = map; + } + } + + private static CelVariableResolver extend(Map primary, Map secondary) { + return hierarchicalVariableResolver( + TestOnlyVariableResolver.newInstance(primary), + TestOnlyVariableResolver.newInstance(secondary)); + } + + private static CelVariableResolver extend(CelVariableResolver primary, Map secondary) { + return hierarchicalVariableResolver(primary, TestOnlyVariableResolver.newInstance(secondary)); + } + + private void addFunctionBinding(CelFunctionBinding... functionBindings) { + addFunctionBinding(Arrays.asList(functionBindings)); + } + + private void addFunctionBinding(Collection functionBindings) { + celRuntime = celRuntime.toRuntimeBuilder().addFunctionBindings(functionBindings).build(); + } + + private static Descriptor getDeserializedTestAllTypeDescriptor() { + try { + String fdsContent = readResourceContent("testdata/proto3/test_all_types.fds"); + FileDescriptorSet fds = TextFormat.parse(fdsContent, FileDescriptorSet.class); + ImmutableSet fileDescriptors = FileDescriptorSetConverter.convert(fds); + + return fileDescriptors.stream() + .flatMap(f -> f.getMessageTypes().stream()) + .filter( + x -> + x.getFullName().equals("dev.cel.testing.testdata.serialized.proto3.TestAllTypes")) + .findAny() + .orElseThrow( + () -> + new IllegalStateException( + "Could not find deserialized TestAllTypes descriptor.")); + } catch (IOException e) { + throw new RuntimeException("Error loading TestAllTypes descriptor", e); + } } } diff --git a/testing/src/main/java/dev/cel/testing/BaselineTestCase.java b/testing/src/main/java/dev/cel/testing/BaselineTestCase.java index b5f83c83a..493106451 100644 --- a/testing/src/main/java/dev/cel/testing/BaselineTestCase.java +++ b/testing/src/main/java/dev/cel/testing/BaselineTestCase.java @@ -16,13 +16,18 @@ import static java.nio.charset.StandardCharsets.UTF_8; +import com.google.common.base.Strings; +import com.google.common.io.Files; import com.google.common.io.Resources; import java.io.BufferedWriter; import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.net.URL; +import java.nio.charset.Charset; import junit.framework.AssertionFailedError; import org.junit.Before; import org.junit.Rule; @@ -37,6 +42,7 @@ public abstract class BaselineTestCase { public static class BaselineComparisonError extends AssertionFailedError { private final String testName; private final String actual; + private final String actualFileLocation; private final LineDiffer.Diff lineDiff; private final String baselineFileName; @@ -48,24 +54,42 @@ public static class BaselineComparisonError extends AssertionFailedError { * @param lineDiff the diff between the expected and {@code actual}. */ public BaselineComparisonError( - String testName, String baselineFileName, String actual, LineDiffer.Diff lineDiff) { + String testName, + String baselineFileName, + String actual, + String actualFileLocation, + LineDiffer.Diff lineDiff) { this.testName = testName; this.actual = actual; + this.actualFileLocation = actualFileLocation; this.lineDiff = lineDiff; this.baselineFileName = baselineFileName; } @Override public String getMessage() { - return String.format( - "Expected for '%s' differs from actual:%n%n\"******New baseline content" - + " is******%n%s%nExpected File: %s%nDiff:\n%s", - testName, actual, baselineFileName, lineDiff); + String resultMessage = + String.format( + "Expected for '%s' differs from actual:%n%n\"******New baseline content" + + " is******%n%s%nExpected File: %s%nActual File: %s%nDiff:\n%s", + testName, actual, baselineFileName, actualFileLocation, lineDiff); + + return resultMessage; } } @Rule public TestName testName = new TestName(); + private static final String DIRECTORY_TO_COPY_NEW_BASELINE; + + static { + if (!Strings.isNullOrEmpty(System.getenv("COPY_BASELINE_TO_DIR"))) { + DIRECTORY_TO_COPY_NEW_BASELINE = System.getenv("COPY_BASELINE_TO_DIR"); + } else { + DIRECTORY_TO_COPY_NEW_BASELINE = "/tmp"; + } + } + private OutputStream output; private PrintWriter writer; private boolean isVerified; @@ -75,6 +99,10 @@ protected PrintWriter testOutput() { return writer; } + protected void println(String text) { + writer.println(text); + } + /** * A test watcher which calls baseline verification if the test succeeded. This is like @After, * but verification is only done if there haven't been other errors. @@ -126,8 +154,9 @@ protected void verify() { String expected = getExpected().trim(); LineDiffer.Diff lineDiff = LineDiffer.diffLines(expected, actual); if (!lineDiff.isEmpty()) { + String actualFileLocation = tryCreateNewBaseline(actual); throw new BaselineComparisonError( - testName.getMethodName(), baselineFileName(), actual, lineDiff); + testName.getMethodName(), baselineFileName(), actual, actualFileLocation, lineDiff); } } catch (Exception e) { throw new RuntimeException(e); @@ -138,4 +167,23 @@ protected void verify() { protected void skipBaselineVerification() { isVerified = true; } + + /** + * Creates a baseline file that will need to be used to make the current test pass. + * + *

If the test is failing for a valid reason (e.g. developer changed some output text), then + * this file provides a convenient way for the developer to overwrite the old baseline and keep + * the test passing. + * + *

The created file is stored under /tmp or location specified by the environment variable + * DIRECTORY_TO_COPY_NEW_BASELINE. Information where the file is stored is returned as a string. + */ + private String tryCreateNewBaseline(String actual) throws IOException { + File file = + new File( + File.separator + DIRECTORY_TO_COPY_NEW_BASELINE + File.separator + baselineFileName()); + Files.createParentDirs(file); + Files.asCharSink(file, Charset.defaultCharset()).write(actual); + return file.toString(); + } } diff --git a/testing/src/main/java/dev/cel/testing/CelBaselineTestCase.java b/testing/src/main/java/dev/cel/testing/CelBaselineTestCase.java index ca48de469..8c79e5931 100644 --- a/testing/src/main/java/dev/cel/testing/CelBaselineTestCase.java +++ b/testing/src/main/java/dev/cel/testing/CelBaselineTestCase.java @@ -14,20 +14,20 @@ package dev.cel.testing; -import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.truth.Truth.assertWithMessage; +import static dev.cel.common.CelFunctionDecl.newFunctionDeclaration; -import dev.cel.expr.Decl; -import dev.cel.expr.Decl.FunctionDecl.Overload; -import dev.cel.expr.Type; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.protobuf.DescriptorProtos.FileDescriptorSet; -import com.google.protobuf.Descriptors.Descriptor; import com.google.protobuf.Descriptors.FileDescriptor; 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.CelValidationException; +import dev.cel.common.CelVarDecl; import dev.cel.common.types.CelType; import dev.cel.common.types.CelTypeProvider; import dev.cel.common.types.CelTypes; @@ -35,6 +35,7 @@ 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 java.util.ArrayList; import java.util.List; @@ -44,48 +45,34 @@ * to ensure consistent behavior on future test runs. */ public abstract class CelBaselineTestCase extends BaselineTestCase { - private final boolean declareWithCelTypes; - private final List decls = new ArrayList<>(); + private final List varDecls = new ArrayList<>(); + private final List functionDecls = new ArrayList<>(); protected String source; - protected String container = ""; + protected CelContainer container = CelContainer.ofName(""); protected CelType expectedType; protected CelCompiler celCompiler; protected static final int COMPREHENSION_MAX_ITERATIONS = 1_000; protected static final CelOptions TEST_OPTIONS = CelOptions.current() - .enableTimestampEpoch(true) - .enableUnsignedLongs(true) .enableHeterogeneousNumericComparisons(true) + .enableHiddenAccumulatorVar(true) .enableOptionalSyntax(true) .comprehensionMaxIterations(1_000) .build(); - /** - * @param declareWithCelTypes If true, variables, functions and their overloads are declared - * internally using java native types {@link CelType}. This will also make the declarations to - * be loaded via their type equivalent APIs to the compiler. (Example: {@link - * CelCompilerBuilder#addFunctionDeclarations} vs. {@link CelCompilerBuilder#addDeclarations} - * ). Setting false will declare these using protobuf types {@link Type} instead. - */ - protected CelBaselineTestCase(boolean declareWithCelTypes) { - this.declareWithCelTypes = declareWithCelTypes; - } + protected CelBaselineTestCase() {} protected CelAbstractSyntaxTree prepareTest(List descriptors) { return prepareTest(new ProtoMessageTypeProvider(ImmutableSet.copyOf(descriptors))); } - protected CelAbstractSyntaxTree prepareTest(Iterable descriptors) { - return prepareTest(new ProtoMessageTypeProvider(descriptors)); - } - protected CelAbstractSyntaxTree prepareTest(FileDescriptorSet descriptorSet) { return prepareTest(new ProtoMessageTypeProvider(descriptorSet)); } - private CelAbstractSyntaxTree prepareTest(CelTypeProvider typeProvider) { + protected CelAbstractSyntaxTree prepareTest(CelTypeProvider typeProvider) { prepareCompiler(typeProvider); CelAbstractSyntaxTree ast; @@ -106,31 +93,6 @@ private CelAbstractSyntaxTree prepareTest(CelTypeProvider typeProvider) { private void validateTestSetup() { assertWithMessage("The source field must be non-null").that(source).isNotNull(); - if (declareWithCelTypes) { - assertWithMessage( - "Test is incorrectly setup. Declarations must be done with CEL native types with" - + " declareWithCelTypes enabled") - .that( - decls.stream() - .filter( - d -> - d instanceof TestProtoFunctionDeclWrapper - || d instanceof TestProtoVariableDeclWrapper) - .count()) - .isEqualTo(0); - } else { - assertWithMessage( - "Test is incorrectly setup. Declarations must be done with proto types with" - + " declareWithCelTypes disabled.") - .that( - decls.stream() - .filter( - d -> - d instanceof TestCelFunctionDeclWrapper - || d instanceof TestCelVariableDeclWrapper) - .count()) - .isEqualTo(0); - } } protected void prepareCompiler(CelTypeProvider typeProvider) { @@ -141,6 +103,7 @@ protected void prepareCompiler(CelTypeProvider typeProvider) { CelCompilerFactory.standardCelCompilerBuilder() .setOptions(TEST_OPTIONS) .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addLibraries(CelExtensions.comprehensions()) .setContainer(container) .setTypeProvider(typeProvider); @@ -148,9 +111,8 @@ protected void prepareCompiler(CelTypeProvider typeProvider) { celCompilerBuilder.setResultType(expectedType); } - // Add the function declarations appropriate to the type we're working with (Either CelType or - // Protobuf Type) - decls.forEach(d -> d.loadDeclsToCompiler(celCompilerBuilder)); + varDecls.forEach(celCompilerBuilder::addVarDeclarations); + functionDecls.forEach(celCompilerBuilder::addFunctionDeclarations); celCompiler = celCompilerBuilder.build(); } @@ -160,17 +122,14 @@ protected void prepareCompiler(CelTypeProvider typeProvider) { * * @param name Variable name */ - protected void declareVariable(String name, Type type) { - TestDecl varDecl = - this.declareWithCelTypes - ? new TestCelVariableDeclWrapper(name, type) - : new TestProtoVariableDeclWrapper(name, type); - decls.add(varDecl); + protected void declareVariable(String name, CelType type) { + varDecls.add(CelVarDecl.newVarDeclaration(name, type)); } /** Clears all function and variable declarations. */ protected void clearAllDeclarations() { - decls.clear(); + functionDecls.clear(); + varDecls.clear(); } /** Returns the test source description. */ @@ -181,105 +140,76 @@ protected String testSourceDescription() { protected void printTestSetup() { // Print the source. testOutput().printf("Source: %s%n", source); - for (TestDecl testDecl : decls) { - testOutput().println(formatDecl(testDecl.getDecl())); + for (CelVarDecl varDecl : varDecls) { + testOutput().println(formatVarDecl(varDecl)); + } + for (CelFunctionDecl functionDecl : functionDecls) { + testOutput().println(formatFunctionDecl(functionDecl)); } + testOutput().println("=====>"); } - protected String formatDecl(Decl decl) { + protected String formatFunctionDecl(CelFunctionDecl decl) { StringBuilder declStr = new StringBuilder(); - declStr.append(String.format("declare %s {%n", decl.getName())); - formatDeclImpl(decl, declStr); + declStr.append(String.format("declare %s {%n", decl.name())); + for (CelOverloadDecl overload : decl.overloads()) { + declStr.append( + String.format( + " function %s %s%n", + overload.overloadId(), + CelTypes.formatFunction( + overload.resultType(), + ImmutableList.copyOf(overload.parameterTypes()), + overload.isInstanceFunction(), + /* typeParamToDyn= */ false))); + } declStr.append("}"); return declStr.toString(); } - protected String formatDecl(String name, List declarations) { + protected String formatVarDecl(CelVarDecl decl) { StringBuilder declStr = new StringBuilder(); - declStr.append(String.format("declare %s {%n", name)); - for (Decl decl : declarations) { - formatDeclImpl(decl, declStr); - } + declStr.append(String.format("declare %s {%n", decl.name())); + declStr.append(String.format(" value %s%n", CelTypes.format(decl.type()))); declStr.append("}"); return declStr.toString(); } - private void formatDeclImpl(Decl decl, StringBuilder declStr) { - switch (decl.getDeclKindCase()) { - case IDENT: - declStr.append(String.format(" value %s%n", CelTypes.format(decl.getIdent().getType()))); - break; - case FUNCTION: - for (Overload overload : decl.getFunction().getOverloadsList()) { - declStr.append( - String.format( - " function %s %s%n", - overload.getOverloadId(), - CelTypes.formatFunction( - CelTypes.typeToCelType(overload.getResultType()), - overload.getParamsList().stream() - .map(CelTypes::typeToCelType) - .collect(toImmutableList()), - overload.getIsInstanceFunction(), - /* typeParamToDyn= */ false))); - } - break; - default: - break; - } - } - /** * Declares a function with one or more overloads * * @param functionName Function name - * @param overloads Function overloads in protobuf representation. If {@link #declareWithCelTypes} - * is set, the protobuf overloads are internally converted into java native versions {@link - * CelOverloadDecl}. + * @param overloads Function overloads in protobuf representation. */ - protected void declareFunction(String functionName, Overload... overloads) { - TestDecl functionDecl = - this.declareWithCelTypes - ? new TestCelFunctionDeclWrapper(functionName, overloads) - : new TestProtoFunctionDeclWrapper(functionName, overloads); - this.decls.add(functionDecl); + protected void declareFunction(String functionName, CelOverloadDecl... overloads) { + this.functionDecls.add(newFunctionDeclaration(functionName, overloads)); } - protected void declareGlobalFunction(String name, List paramTypes, Type resultType) { + protected void declareGlobalFunction(String name, List paramTypes, CelType resultType) { declareFunction(name, globalOverload(name, paramTypes, resultType)); } - protected void declareMemberFunction(String name, List paramTypes, Type resultType) { + protected void declareMemberFunction(String name, List paramTypes, CelType resultType) { declareFunction(name, memberOverload(name, paramTypes, resultType)); } - protected Overload memberOverload(String overloadId, List paramTypes, Type resultType) { - return overload(overloadId, paramTypes, resultType).setIsInstanceFunction(true).build(); - } - - protected Overload memberOverload( - String overloadId, List paramTypes, List typeParams, Type resultType) { - return overload(overloadId, paramTypes, resultType) - .addAllTypeParams(typeParams) - .setIsInstanceFunction(true) - .build(); - } - - protected Overload globalOverload(String overloadId, List paramTypes, Type resultType) { - return overload(overloadId, paramTypes, resultType).build(); + protected CelOverloadDecl memberOverload( + String overloadId, List paramTypes, CelType resultType) { + return overloadBuilder(overloadId, paramTypes, resultType).setIsInstanceFunction(true).build(); } - protected Overload globalOverload( - String overloadId, List paramTypes, List typeParams, Type resultType) { - return overload(overloadId, paramTypes, resultType).addAllTypeParams(typeParams).build(); + protected CelOverloadDecl globalOverload( + String overloadId, List paramTypes, CelType resultType) { + return overloadBuilder(overloadId, paramTypes, resultType).setIsInstanceFunction(false).build(); } - private Overload.Builder overload(String overloadId, List paramTypes, Type resultType) { - return Overload.newBuilder() + private CelOverloadDecl.Builder overloadBuilder( + String overloadId, List paramTypes, CelType resultType) { + return CelOverloadDecl.newBuilder() .setOverloadId(overloadId) .setResultType(resultType) - .addAllParams(paramTypes); + .addParameterTypes(paramTypes); } protected void printTestValidationError(CelValidationException error) { diff --git a/testing/src/main/java/dev/cel/testing/CelDebug.java b/testing/src/main/java/dev/cel/testing/CelDebug.java index 58bb2a6b2..410eecb42 100644 --- a/testing/src/main/java/dev/cel/testing/CelDebug.java +++ b/testing/src/main/java/dev/cel/testing/CelDebug.java @@ -42,12 +42,12 @@ public String adorn(EntryOrBuilder entry) { } }; - /** Returns the unadorned string representation of {@link com.google.api.expr.ExprOrBuilder}. */ + /** Returns the unadorned string representation of {@link dev.cel.expr.ExprOrBuilder}. */ public static String toDebugString(ExprOrBuilder expr) { return toAdornedDebugString(expr, UNADORNER); } - /** Returns the adorned string representation of {@link com.google.api.expr.ExprOrBuilder}. */ + /** Returns the adorned string representation of {@link dev.cel.expr.ExprOrBuilder}. */ public static String toAdornedDebugString(ExprOrBuilder expr, CelAdorner adorner) { CelDebug debug = new CelDebug(checkNotNull(adorner)); debug.appendExpr(checkNotNull(expr)); @@ -214,6 +214,11 @@ private void appendComprehension(Expr.ComprehensionOrBuilder comprehensionExpr) append("// Variable"); appendLine(); append(comprehensionExpr.getIterVar()); + if (!comprehensionExpr.getIterVar2().isEmpty()) { + append(','); + appendLine(); + append(comprehensionExpr.getIterVar2()); + } append(','); appendLine(); append("// Target"); 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/Eval.java b/testing/src/main/java/dev/cel/testing/Eval.java deleted file mode 100644 index 2a2b4f5e8..000000000 --- a/testing/src/main/java/dev/cel/testing/Eval.java +++ /dev/null @@ -1,44 +0,0 @@ -// 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.testing; - -import com.google.common.collect.ImmutableList; -import com.google.errorprone.annotations.CheckReturnValue; -import com.google.protobuf.Descriptors.FileDescriptor; -import dev.cel.common.CelAbstractSyntaxTree; -import dev.cel.common.CelOptions; -import dev.cel.runtime.Activation; -import dev.cel.runtime.InterpreterException; -import dev.cel.runtime.Registrar; - -/** - * The {@code Eval} interface is used to model the core concerns of CEL evaluation during testing. - */ -@CheckReturnValue -public interface Eval { - /** Returns the set of file descriptors configured for evaluation. */ - ImmutableList fileDescriptors(); - - /** Returns the function / type registrar used during evaluation. */ - Registrar registrar(); - - CelOptions celOptions(); - - /** Adapts a Java POJO to a CEL value. */ - Object adapt(Object value) throws InterpreterException; - - /** Evaluates an {@code ast} against a set of inputs represented by the {@code Activation}. */ - Object eval(CelAbstractSyntaxTree ast, Activation activation) throws Exception; -} diff --git a/testing/src/main/java/dev/cel/testing/EvalCelValueSync.java b/testing/src/main/java/dev/cel/testing/EvalCelValueSync.java deleted file mode 100644 index 5b6a69bfa..000000000 --- a/testing/src/main/java/dev/cel/testing/EvalCelValueSync.java +++ /dev/null @@ -1,95 +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.testing; - -import com.google.common.collect.ImmutableList; -import com.google.protobuf.Descriptors.FileDescriptor; -import dev.cel.common.CelAbstractSyntaxTree; -import dev.cel.common.CelDescriptorUtil; -import dev.cel.common.CelDescriptors; -import dev.cel.common.CelOptions; -import dev.cel.common.internal.DefaultDescriptorPool; -import dev.cel.common.internal.DefaultMessageFactory; -import dev.cel.common.internal.DynamicProto; -import dev.cel.common.internal.ProtoMessageFactory; -import dev.cel.common.values.CelValueProvider; -import dev.cel.common.values.ProtoMessageValueProvider; -import dev.cel.runtime.Activation; -import dev.cel.runtime.DefaultDispatcher; -import dev.cel.runtime.DefaultInterpreter; -import dev.cel.runtime.Interpreter; -import dev.cel.runtime.InterpreterException; -import dev.cel.runtime.Registrar; -import dev.cel.runtime.RuntimeTypeProvider; -import dev.cel.runtime.RuntimeTypeProviderLegacyImpl; - -/** - * The {@link EvalSync} class represents common concerns for synchronous evaluation using {@code - * CelValue}. - */ -public final class EvalCelValueSync implements Eval { - - private final ImmutableList fileDescriptors; - private final DefaultDispatcher dispatcher; - private final Interpreter interpreter; - private final RuntimeTypeProvider typeProvider; - private final CelOptions celOptions; - - public EvalCelValueSync(ImmutableList fileDescriptors, CelOptions celOptions) { - this.fileDescriptors = fileDescriptors; - this.dispatcher = DefaultDispatcher.create(celOptions); - this.celOptions = celOptions; - this.typeProvider = newTypeProvider(fileDescriptors); - this.interpreter = new DefaultInterpreter(typeProvider, dispatcher, celOptions); - } - - private RuntimeTypeProvider newTypeProvider(ImmutableList fileDescriptors) { - CelDescriptors celDescriptors = - CelDescriptorUtil.getAllDescriptorsFromFileDescriptor(fileDescriptors); - DefaultDescriptorPool celDescriptorPool = DefaultDescriptorPool.create(celDescriptors); - ProtoMessageFactory messageFactory = DefaultMessageFactory.create(celDescriptorPool); - DynamicProto dynamicProto = DynamicProto.create(messageFactory); - CelValueProvider messageValueProvider = - ProtoMessageValueProvider.newInstance(dynamicProto, celOptions); - - return new RuntimeTypeProviderLegacyImpl( - celOptions, messageValueProvider, celDescriptorPool, dynamicProto); - } - - @Override - public ImmutableList fileDescriptors() { - return fileDescriptors; - } - - @Override - public Registrar registrar() { - return dispatcher; - } - - @Override - public CelOptions celOptions() { - return celOptions; - } - - @Override - public Object adapt(Object value) throws InterpreterException { - return typeProvider.adapt(value); - } - - @Override - public Object eval(CelAbstractSyntaxTree ast, Activation activation) throws Exception { - return interpreter.createInterpretable(ast).eval(activation); - } -} diff --git a/testing/src/main/java/dev/cel/testing/EvalSync.java b/testing/src/main/java/dev/cel/testing/EvalSync.java deleted file mode 100644 index 562043ce6..000000000 --- a/testing/src/main/java/dev/cel/testing/EvalSync.java +++ /dev/null @@ -1,79 +0,0 @@ -// 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.testing; - -import com.google.common.collect.ImmutableList; -import com.google.protobuf.Descriptors.FileDescriptor; -import dev.cel.common.CelAbstractSyntaxTree; -import dev.cel.common.CelDescriptorUtil; -import dev.cel.common.CelDescriptors; -import dev.cel.common.CelOptions; -import dev.cel.common.internal.DefaultDescriptorPool; -import dev.cel.common.internal.DefaultMessageFactory; -import dev.cel.runtime.Activation; -import dev.cel.runtime.DefaultDispatcher; -import dev.cel.runtime.DefaultInterpreter; -import dev.cel.runtime.DescriptorMessageProvider; -import dev.cel.runtime.Interpreter; -import dev.cel.runtime.InterpreterException; -import dev.cel.runtime.Registrar; -import dev.cel.runtime.RuntimeTypeProvider; - -/** The {@code EvalSync} class represents common concerns for synchronous evaluation. */ -public final class EvalSync implements Eval { - - private final ImmutableList fileDescriptors; - private final DefaultDispatcher dispatcher; - private final Interpreter interpreter; - private final RuntimeTypeProvider typeProvider; - private final CelOptions celOptions; - - public EvalSync(ImmutableList fileDescriptors, CelOptions celOptions) { - this.fileDescriptors = fileDescriptors; - this.dispatcher = DefaultDispatcher.create(celOptions); - CelDescriptors celDescriptors = - CelDescriptorUtil.getAllDescriptorsFromFileDescriptor(fileDescriptors); - this.typeProvider = - new DescriptorMessageProvider( - DefaultMessageFactory.create(DefaultDescriptorPool.create(celDescriptors)), celOptions); - this.interpreter = new DefaultInterpreter(typeProvider, dispatcher, celOptions); - this.celOptions = celOptions; - } - - @Override - public ImmutableList fileDescriptors() { - return fileDescriptors; - } - - @Override - public Registrar registrar() { - return dispatcher; - } - - @Override - public CelOptions celOptions() { - return celOptions; - } - - @Override - public Object adapt(Object value) throws InterpreterException { - return typeProvider.adapt(value); - } - - @Override - public Object eval(CelAbstractSyntaxTree ast, Activation activation) throws Exception { - return interpreter.createInterpretable(ast).eval(activation); - } -} diff --git a/testing/src/main/java/dev/cel/testing/TestCelFunctionDeclWrapper.java b/testing/src/main/java/dev/cel/testing/TestCelFunctionDeclWrapper.java deleted file mode 100644 index db6349bfc..000000000 --- a/testing/src/main/java/dev/cel/testing/TestCelFunctionDeclWrapper.java +++ /dev/null @@ -1,45 +0,0 @@ -// 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.testing; - -import dev.cel.expr.Decl; -import dev.cel.expr.Decl.FunctionDecl.Overload; -import com.google.common.collect.Iterables; -import dev.cel.common.CelFunctionDecl; -import dev.cel.common.CelOverloadDecl; -import dev.cel.compiler.CelCompilerBuilder; -import java.util.Arrays; - -/** Wrapper for CEL native type based function declarations {@link CelFunctionDecl} */ -class TestCelFunctionDeclWrapper extends TestDecl { - private final CelFunctionDecl functionDecl; - - TestCelFunctionDeclWrapper(String functionName, Overload... overloads) { - this.functionDecl = - CelFunctionDecl.newFunctionDeclaration( - functionName, - Iterables.transform(Arrays.asList(overloads), CelOverloadDecl::overloadToCelOverload)); - } - - @Override - void loadDeclsToCompiler(CelCompilerBuilder compiler) { - compiler.addFunctionDeclarations(functionDecl); - } - - @Override - Decl getDecl() { - return CelFunctionDecl.celFunctionDeclToDecl(functionDecl); - } -} diff --git a/testing/src/main/java/dev/cel/testing/TestCelVariableDeclWrapper.java b/testing/src/main/java/dev/cel/testing/TestCelVariableDeclWrapper.java deleted file mode 100644 index 20c2c4843..000000000 --- a/testing/src/main/java/dev/cel/testing/TestCelVariableDeclWrapper.java +++ /dev/null @@ -1,44 +0,0 @@ -// 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.testing; - -import dev.cel.expr.Decl; -import dev.cel.expr.Decl.IdentDecl; -import dev.cel.expr.Type; -import dev.cel.common.types.CelType; -import dev.cel.common.types.CelTypes; -import dev.cel.compiler.CelCompilerBuilder; - -/** Wrapper for CEL native type based variable declarations */ -class TestCelVariableDeclWrapper extends TestDecl { - private final String name; - private final Type type; - - TestCelVariableDeclWrapper(String name, Type type) { - this.name = name; - this.type = type; - } - - @Override - void loadDeclsToCompiler(CelCompilerBuilder compiler) { - CelType celType = CelTypes.typeToCelType(type); - compiler.addVar(name, celType); - } - - @Override - Decl getDecl() { - return Decl.newBuilder().setName(name).setIdent(IdentDecl.newBuilder().setType(type)).build(); - } -} diff --git a/testing/src/main/java/dev/cel/testing/TestProtoFunctionDeclWrapper.java b/testing/src/main/java/dev/cel/testing/TestProtoFunctionDeclWrapper.java deleted file mode 100644 index f48258059..000000000 --- a/testing/src/main/java/dev/cel/testing/TestProtoFunctionDeclWrapper.java +++ /dev/null @@ -1,44 +0,0 @@ -// 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.testing; - -import dev.cel.expr.Decl; -import dev.cel.expr.Decl.FunctionDecl; -import dev.cel.expr.Decl.FunctionDecl.Overload; -import dev.cel.compiler.CelCompilerBuilder; -import java.util.Arrays; - -/** Wrapper for proto-based function declarations. */ -class TestProtoFunctionDeclWrapper extends TestDecl { - private final Decl functionDecl; - - TestProtoFunctionDeclWrapper(String functionName, Overload... overloads) { - this.functionDecl = - Decl.newBuilder() - .setName(functionName) - .setFunction(FunctionDecl.newBuilder().addAllOverloads(Arrays.asList(overloads))) - .build(); - } - - @Override - void loadDeclsToCompiler(CelCompilerBuilder compiler) { - compiler.addDeclarations(functionDecl); - } - - @Override - Decl getDecl() { - return functionDecl; - } -} diff --git a/testing/src/main/java/dev/cel/testing/TestProtoVariableDeclWrapper.java b/testing/src/main/java/dev/cel/testing/TestProtoVariableDeclWrapper.java deleted file mode 100644 index 1c18b0f94..000000000 --- a/testing/src/main/java/dev/cel/testing/TestProtoVariableDeclWrapper.java +++ /dev/null @@ -1,40 +0,0 @@ -// 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.testing; - -import dev.cel.expr.Decl; -import dev.cel.expr.Decl.IdentDecl; -import dev.cel.expr.Type; -import dev.cel.compiler.CelCompilerBuilder; - -/** Wrapper for proto-based variable declarations. */ -class TestProtoVariableDeclWrapper extends TestDecl { - private final Decl varDecl; - - TestProtoVariableDeclWrapper(String name, Type type) { - varDecl = - Decl.newBuilder().setName(name).setIdent(IdentDecl.newBuilder().setType(type)).build(); - } - - @Override - void loadDeclsToCompiler(CelCompilerBuilder compiler) { - compiler.addDeclarations(varDecl); - } - - @Override - Decl getDecl() { - return varDecl; - } -} diff --git a/testing/src/main/java/dev/cel/testing/compiled/BUILD.bazel b/testing/src/main/java/dev/cel/testing/compiled/BUILD.bazel new file mode 100644 index 000000000..5ef4d8878 --- /dev/null +++ b/testing/src/main/java/dev/cel/testing/compiled/BUILD.bazel @@ -0,0 +1,232 @@ +load("@rules_java//java:defs.bzl", "java_library") +load("//:cel_android_rules.bzl", "cel_android_library") +load("//compiler/tools:compile_cel.bzl", "compile_cel") + +package( + default_applicable_licenses = ["//:license"], + default_testonly = True, + default_visibility = ["//testing/compiled:__pkg__"], +) + +java_library( + name = "compiled_expr_utils", + srcs = ["CompiledExprUtils.java"], + tags = [ + ], + deps = [ + ":compiled_expr_resources", # unuseddeps: keep + "//common:cel_ast", + "//common:proto_ast", + "@cel_spec//proto/cel/expr:checked_java_proto", + "@maven//:com_google_guava_guava", + "@maven_android//:com_google_protobuf_protobuf_javalite", + ], +) + +cel_android_library( + name = "compiled_expr_utils_android", + srcs = ["CompiledExprUtils.java"], + tags = [ + ], + deps = [ + ":compiled_expr_resources", # unuseddeps: keep + "//common:cel_ast_android", + "//common:proto_ast_android", + "@cel_spec//proto/cel/expr:checked_java_proto_lite", + "@maven_android//:com_google_guava_guava", + "@maven_android//:com_google_protobuf_protobuf_javalite", + ], +) + +java_library( + name = "compiled_expr_resources", + # used_by_android + resources = [ + ":compiled_comprehension", + ":compiled_comprehension_exists", + ":compiled_custom_functions", + ":compiled_extended_env", + ":compiled_extensions", + ":compiled_hello_world", + ":compiled_list_literal", + ":compiled_one_plus_two", + ":compiled_primitive_variables", + ":compiled_proto2_deep_traversal", + ":compiled_proto2_select_map_fields", + ":compiled_proto2_select_primitives", + ":compiled_proto2_select_primitives_all_ored", + ":compiled_proto2_select_repeated_fields", + ":compiled_proto2_select_wrappers", + ":compiled_proto3_deep_traversal", + ":compiled_proto3_select_map_fields", + ":compiled_proto3_select_primitives", + ":compiled_proto3_select_primitives_all_ored", + ":compiled_proto3_select_repeated_fields", + ":compiled_proto3_select_wrappers", + ":compiled_proto_message", + ], +) + +compile_cel( + name = "compiled_hello_world", + expression = "'hello world'", +) + +compile_cel( + name = "compiled_one_plus_two", + expression = "1 + 2", +) + +compile_cel( + name = "compiled_list_literal", + expression = "['a', 1, 2u, 3.5]", +) + +compile_cel( + name = "compiled_comprehension", + expression = "[1,2,3].map(x, x + 1)", +) + +compile_cel( + name = "compiled_comprehension_exists", + expression = "[1,2,3].exists(x, x == 3)", +) + +compile_cel( + name = "compiled_proto_message", + expression = "cel.expr.conformance.proto3.TestAllTypes{single_int32: 1}", + proto_srcs = ["@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_proto"], +) + +compile_cel( + name = "compiled_extensions", + environment = "//testing/environment:all_extensions", + expression = "cel.bind(x, 10, math.greatest([1,x])) < int(' 11 '.trim()) && optional.none().orValue(true) && [].flatten() == []", +) + +compile_cel( + name = "compiled_extended_env", + environment = "//testing/environment:extended_env", + expression = "msg.single_string_wrapper.isEmpty() == false", + proto_srcs = ["@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_proto"], +) + +compile_cel( + name = "compiled_primitive_variables", + environment = "//testing/environment:primitive_variables", + expression = "bool_var && bytes_var == b'abc' && double_var == 1.0 && int_var == 42 && uint_var == 42u && str_var == 'foo'", +) + +compile_cel( + name = "compiled_custom_functions", + environment = "//testing/environment:custom_functions", + expression = "''.isEmpty() && [].isEmpty()", +) + +compile_cel( + name = "compiled_proto2_select_primitives_all_ored", + environment = "//testing/environment:proto2_message_variables", + expression = "proto2.single_int32 == 1 || proto2.single_int64 == 2 || proto2.single_uint32 == 3u || proto2.single_uint64 == 4u ||" + + "proto2.single_sint32 == 5 || proto2.single_sint64 == 6 || proto2.single_fixed32 == 7u || proto2.single_fixed64 == 8u ||" + + "proto2.single_sfixed32 == 9 || proto2.single_sfixed64 == 10 || proto2.single_float == 1.5 || proto2.single_double == 2.5 ||" + + "proto2.single_bool || proto2.single_string == 'hello world' || proto2.single_bytes == b\'abc\'", + proto_srcs = ["@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_proto"], +) + +compile_cel( + name = "compiled_proto2_select_primitives", + environment = "//testing/environment:proto2_message_variables", + expression = "proto2.single_int32 == 1 && proto2.single_int64 == 2 && proto2.single_uint32 == 3u && proto2.single_uint64 == 4u &&" + + "proto2.single_sint32 == 5 && proto2.single_sint64 == 6 && proto2.single_fixed32 == 7u && proto2.single_fixed64 == 8u &&" + + "proto2.single_sfixed32 == 9 && proto2.single_sfixed64 == 10 && proto2.single_float == 1.5 && proto2.single_double == 2.5 &&" + + "proto2.single_bool && proto2.single_string == 'hello world' && proto2.single_bytes == b\'abc\'", + proto_srcs = ["@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_proto"], +) + +compile_cel( + name = "compiled_proto2_select_wrappers", + environment = "//testing/environment:proto2_message_variables", + expression = "proto2.single_int32_wrapper == 1 && proto2.single_int64_wrapper == 2 && proto2.single_float_wrapper == 1.5 &&" + + "proto2.single_double_wrapper == 2.5 && proto2.single_uint32_wrapper == 3u && proto2.single_uint64_wrapper == 4u &&" + + "proto2.single_string_wrapper == 'hello world' && proto2.single_bool_wrapper && proto2.single_bytes_wrapper == b\'abc\'", + proto_srcs = ["@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_proto"], +) + +compile_cel( + name = "compiled_proto3_select_primitives_all_ored", + environment = "//testing/environment:proto3_message_variables", + expression = "proto3.single_int32 == 1 || proto3.single_int64 == 2 || proto3.single_uint32 == 3u || proto3.single_uint64 == 4u ||" + + "proto3.single_sint32 == 5 || proto3.single_sint64 == 6 || proto3.single_fixed32 == 7u || proto3.single_fixed64 == 8u ||" + + "proto3.single_sfixed32 == 9 || proto3.single_sfixed64 == 10 || proto3.single_float == 1.5 || proto3.single_double == 2.5 ||" + + "proto3.single_bool || proto3.single_string == 'hello world' || proto3.single_bytes == b\'abc\'", + proto_srcs = ["@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_proto"], +) + +compile_cel( + name = "compiled_proto3_select_primitives", + environment = "//testing/environment:proto3_message_variables", + expression = "proto3.single_int32 == 1 && proto3.single_int64 == 2 && proto3.single_uint32 == 3u && proto3.single_uint64 == 4u &&" + + "proto3.single_sint32 == 5 && proto3.single_sint64 == 6 && proto3.single_fixed32 == 7u && proto3.single_fixed64 == 8u &&" + + "proto3.single_sfixed32 == 9 && proto3.single_sfixed64 == 10 && proto3.single_float == 1.5 && proto3.single_double == 2.5 &&" + + "proto3.single_bool && proto3.single_string == 'hello world' && proto3.single_bytes == b\'abc\'", + proto_srcs = ["@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_proto"], +) + +compile_cel( + name = "compiled_proto3_select_wrappers", + environment = "//testing/environment:proto3_message_variables", + expression = "proto3.single_int32_wrapper == 1 && proto3.single_int64_wrapper == 2 && proto3.single_float_wrapper == 1.5 &&" + + "proto3.single_double_wrapper == 2.5 && proto3.single_uint32_wrapper == 3u && proto3.single_uint64_wrapper == 4u &&" + + "proto3.single_string_wrapper == 'hello world' && proto3.single_bool_wrapper && proto3.single_bytes_wrapper == b\'abc\'", + proto_srcs = ["@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_proto"], +) + +compile_cel( + name = "compiled_proto2_deep_traversal", + environment = "//testing/environment:proto2_message_variables", + expression = "proto2.oneof_type.payload.repeated_string", + proto_srcs = ["@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_proto"], +) + +compile_cel( + name = "compiled_proto3_deep_traversal", + environment = "//testing/environment:proto3_message_variables", + expression = "proto3.oneof_type.payload.repeated_string", + proto_srcs = ["@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_proto"], +) + +compile_cel( + name = "compiled_proto2_select_repeated_fields", + environment = "//testing/environment:proto2_message_variables", + expression = "[proto2.repeated_int32, proto2.repeated_int64, proto2.repeated_uint32, proto2.repeated_uint64, proto2.repeated_sint32, proto2.repeated_sint64, " + + "proto2.repeated_fixed32, proto2.repeated_fixed64, proto2.repeated_sfixed32, proto2.repeated_sfixed64, proto2.repeated_float, proto2.repeated_double, " + + "proto2.repeated_bool, proto2.repeated_string, proto2.repeated_bytes]", + proto_srcs = ["@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_proto"], +) + +compile_cel( + name = "compiled_proto3_select_repeated_fields", + environment = "//testing/environment:proto3_message_variables", + expression = "[proto3.repeated_int32, proto3.repeated_int64, proto3.repeated_uint32, proto3.repeated_uint64, proto3.repeated_sint32, proto3.repeated_sint64, " + + "proto3.repeated_fixed32, proto3.repeated_fixed64, proto3.repeated_sfixed32, proto3.repeated_sfixed64, proto3.repeated_float, proto3.repeated_double, " + + "proto3.repeated_bool, proto3.repeated_string, proto3.repeated_bytes]", + proto_srcs = ["@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_proto"], +) + +compile_cel( + name = "compiled_proto2_select_map_fields", + environment = "//testing/environment:proto2_message_variables", + expression = "[proto2.map_bool_bool, proto2.map_bool_string, proto2.map_bool_bytes, proto2.map_bool_int32, proto2.map_bool_int64, " + + "proto2.map_bool_uint32, proto2.map_bool_uint64, proto2.map_bool_float, proto2.map_bool_double, proto2.map_bool_enum, " + + "proto2.map_bool_duration, proto2.map_bool_timestamp]", + proto_srcs = ["@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_proto"], +) + +compile_cel( + name = "compiled_proto3_select_map_fields", + environment = "//testing/environment:proto3_message_variables", + expression = "[proto3.map_bool_bool, proto3.map_bool_string, proto3.map_bool_bytes, proto3.map_bool_int32, proto3.map_bool_int64, " + + "proto3.map_bool_uint32, proto3.map_bool_uint64, proto3.map_bool_float, proto3.map_bool_double, proto3.map_bool_enum, " + + "proto3.map_bool_duration, proto3.map_bool_timestamp]", + proto_srcs = ["@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_proto"], +) diff --git a/testing/src/main/java/dev/cel/testing/compiled/CompiledExprUtils.java b/testing/src/main/java/dev/cel/testing/compiled/CompiledExprUtils.java new file mode 100644 index 000000000..692170c13 --- /dev/null +++ b/testing/src/main/java/dev/cel/testing/compiled/CompiledExprUtils.java @@ -0,0 +1,45 @@ +// 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.testing.compiled; + +import dev.cel.expr.CheckedExpr; +import com.google.common.io.Resources; +import com.google.protobuf.ExtensionRegistryLite; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelProtoAbstractSyntaxTree; +import java.io.IOException; +import java.net.URL; + +/** + * CompiledExprUtils handles reading a CheckedExpr stored in a .binarypb format from the JAR's + * resources. + */ +public final class CompiledExprUtils { + + /** + * Reads a CheckedExpr stored in the running JAR's resources, then returns an adapted {@link + * CelAbstractSyntaxTree}. + */ + public static CelAbstractSyntaxTree readCheckedExpr(String compiledCelTarget) throws IOException { + String resourcePath = String.format("%s.binarypb", compiledCelTarget); + URL url = Resources.getResource(CompiledExprUtils.class, resourcePath); + byte[] checkedExprBytes = Resources.toByteArray(url); + CheckedExpr checkedExpr = + CheckedExpr.parseFrom(checkedExprBytes, ExtensionRegistryLite.getEmptyRegistry()); + return CelProtoAbstractSyntaxTree.fromCheckedExpr(checkedExpr).getAst(); + } + + private CompiledExprUtils() {} +} diff --git a/testing/src/main/java/dev/cel/testing/testrunner/Annotations.java b/testing/src/main/java/dev/cel/testing/testrunner/Annotations.java new file mode 100644 index 000000000..056fd424f --- /dev/null +++ b/testing/src/main/java/dev/cel/testing/testrunner/Annotations.java @@ -0,0 +1,37 @@ +// 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.testing.testrunner; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** These annotations are intended only for CEL Test Runner code. */ +public final class Annotations { + + /** + * Annotates a method which is a test runner test suite supplier. + * + *

This annotation is used to identify the method that is responsible for declaring the test + * cases. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.METHOD}) + @Documented + public @interface TestSuiteSupplier {} + + private Annotations() {} +} diff --git a/testing/src/main/java/dev/cel/testing/testrunner/BUILD.bazel b/testing/src/main/java/dev/cel/testing/testrunner/BUILD.bazel new file mode 100644 index 000000000..677884a8a --- /dev/null +++ b/testing/src/main/java/dev/cel/testing/testrunner/BUILD.bazel @@ -0,0 +1,266 @@ +load("@rules_java//java:java_library.bzl", "java_library") + +package( + default_applicable_licenses = ["//:license"], + default_testonly = True, + default_visibility = [ + "//testing/testrunner:__pkg__", + ], +) + +java_library( + name = "test_executor", + srcs = ["TestExecutor.java"], + tags = [ + ], + deps = [ + ":annotations", + ":cel_coverage_index", + ":cel_test_suite", + ":cel_test_suite_exception", + ":cel_test_suite_text_proto_parser", + ":cel_test_suite_yaml_parser", + ":cel_user_test_template", + ":junit_xml_reporter", + "//testing/testrunner:class_loader_utils", + "@maven//:com_google_guava_guava", + "@maven//:io_github_classgraph_classgraph", + "@maven//:junit_junit", + ], +) + +java_library( + name = "junit_xml_reporter", + srcs = ["JUnitXmlReporter.java"], + tags = [ + ], + deps = [ + ":cel_coverage_index", + "@maven//:com_google_guava_guava", + "@maven//:org_jspecify_jspecify", + ], +) + +java_library( + name = "cel_user_test_template", + srcs = ["CelUserTestTemplate.java"], + tags = [ + ], + deps = [ + ":cel_coverage_index", + ":cel_expression_source", + ":cel_test_context", + ":cel_test_suite", + ":test_runner_library", + "@maven//:junit_junit", + "@maven//:org_jspecify_jspecify", + ], +) + +java_library( + name = "cel_coverage_index", + srcs = ["CelCoverageIndex.java"], + tags = [ + ], + deps = [ + "//:auto_value", + "//common:cel_ast", + "//common/ast", + "//common/navigation", + "//common/types:type_providers", + "//parser:unparser_visitor", + "//runtime:evaluation_listener", + "@maven//:com_google_code_findbugs_annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "test_runner_library", + srcs = ["TestRunnerLibrary.java"], + tags = [ + ], + deps = [ + ":cel_coverage_index", + ":cel_expression_source", + ":cel_test_context", + ":cel_test_suite", + ":registry_utils", + ":result_matcher", + "//bundle:cel", + "//bundle:environment", + "//bundle:environment_yaml_parser", + "//common:cel_ast", + "//common:cel_descriptor_util", + "//common:cel_descriptors", + "//common:compiler_common", + "//common:options", + "//common:proto_ast", + "//policy", + "//policy:compiler_factory", + "//policy:parser", + "//policy:parser_factory", + "//policy:validation_exception", + "//runtime", + "//testing:expr_value_utils", + "@cel_spec//proto/cel/expr:expr_java_proto", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + "@maven//:org_jspecify_jspecify", + ], +) + +java_library( + name = "cel_test_suite", + srcs = ["CelTestSuite.java"], + tags = [ + ], + deps = [ + "//:auto_value", + "//common:source", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + ], +) + +java_library( + name = "cel_test_suite_yaml_parser", + srcs = ["CelTestSuiteYamlParser.java"], + tags = [ + ], + deps = [ + ":cel_test_suite", + ":cel_test_suite_exception", + "//common:compiler_common", + "//common/annotations", + "//common/formats:file_source", + "//common/formats:parser_context", + "//common/formats:yaml_helper", + "//common/formats:yaml_parser_context_impl", + "//common/internal", + "@maven//:com_google_guava_guava", + "@maven//:org_yaml_snakeyaml", + ], +) + +java_library( + name = "cel_test_suite_exception", + srcs = ["CelTestSuiteException.java"], + tags = [ + ], + deps = ["//common:cel_exception"], +) + +java_library( + name = "cel_test_context", + srcs = ["CelTestContext.java"], + tags = [ + ], + 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", + ], +) + +java_library( + name = "registry_utils", + srcs = ["RegistryUtils.java"], + tags = [ + ], + deps = [ + "//common:cel_descriptors", + "@maven//:com_google_protobuf_protobuf_java", + ], +) + +java_library( + name = "result_matcher", + srcs = ["ResultMatcher.java"], + deps = [ + ":cel_test_suite", + "//:auto_value", + "//bundle:cel", + "//common/types:type_providers", + "//runtime", + "@cel_spec//proto/cel/expr:expr_java_proto", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "default_result_matcher", + srcs = ["DefaultResultMatcher.java"], + deps = [ + ":cel_test_suite", + ":registry_utils", + ":result_matcher", + "//: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", + ], +) + +java_library( + name = "cel_test_suite_text_proto_parser", + srcs = ["CelTestSuiteTextProtoParser.java"], + tags = [ + ], + deps = [ + ":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", + "@maven//:com_google_protobuf_protobuf_java", + ], +) + +java_library( + name = "annotations", + srcs = ["Annotations.java"], + tags = [ + ], +) + +java_library( + name = "cel_expression_source", + srcs = ["CelExpressionSource.java"], + tags = [ + ], + deps = [ + "//:auto_value", + ], +) + +filegroup( + name = "test_runner_binary", + srcs = [ + "TestRunnerBinary.java", + ], +) diff --git a/testing/src/main/java/dev/cel/testing/testrunner/CelCoverageIndex.java b/testing/src/main/java/dev/cel/testing/testrunner/CelCoverageIndex.java new file mode 100644 index 000000000..d99525934 --- /dev/null +++ b/testing/src/main/java/dev/cel/testing/testrunner/CelCoverageIndex.java @@ -0,0 +1,429 @@ +// 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.testing.testrunner; + +import static com.google.common.collect.ImmutableList.toImmutableList; +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import com.google.common.io.Files; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import javax.annotation.concurrent.ThreadSafe; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.ast.CelExpr; +import dev.cel.common.ast.CelExpr.ExprKind; +import dev.cel.common.navigation.CelNavigableAst; +import dev.cel.common.navigation.CelNavigableExpr; +import dev.cel.common.types.CelKind; +import dev.cel.parser.CelUnparserVisitor; +import dev.cel.runtime.CelEvaluationListener; +import java.io.File; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; + +import java.util.logging.Logger; + +/** + * A class for managing the coverage index for CEL tests. + * + *

This class is used to manage the coverage index for CEL tests. It provides a method for + * getting the coverage index for a given test case. + */ +final class CelCoverageIndex { + + private static final Logger logger = Logger.getLogger(CelCoverageIndex.class.getName()); + + private static final String DIGRAPH_HEADER = "digraph {\n"; + private static final String UNCOVERED_NODE_STYLE = "color=\"indianred2\", style=filled"; + private static final String PARTIALLY_COVERED_NODE_STYLE = "color=\"lightyellow\"," + + "style=filled"; + private static final String COMPLETELY_COVERED_NODE_STYLE = "color=\"lightgreen\"," + + "style=filled"; + + private CelAbstractSyntaxTree ast; + private final ConcurrentHashMap nodeCoverageStatsMap = + new ConcurrentHashMap<>(); + + public void init(CelAbstractSyntaxTree ast) { + // If the AST and node coverage stats map are already initialized, then we don't need to + // re-initialize them. + if (this.ast == null && nodeCoverageStatsMap.isEmpty()) { + this.ast = ast; + CelNavigableExpr.fromExpr(ast.getExpr()) + .allNodes() + .forEach( + celNavigableExpr -> { + NodeCoverageStats nodeCoverageStats = new NodeCoverageStats(); + nodeCoverageStats.isBooleanNode.set(isNodeTypeBoolean(celNavigableExpr.expr())); + nodeCoverageStatsMap.put(celNavigableExpr.id(), nodeCoverageStats); + }); + } + } + + /** + * Returns the evaluation listener for the CEL test suite. + * + *

This listener is used to track the coverage of the CEL test suite. + */ + public CelEvaluationListener newEvaluationListener() { + return new EvaluationListener(nodeCoverageStatsMap); + } + + /** A class for managing the coverage report for a CEL test suite. */ + @AutoValue + public abstract static class CoverageReport { + public abstract String celExpression(); + + public abstract long nodes(); + + public abstract long coveredNodes(); + + public abstract long branches(); + + public abstract long coveredBooleanOutcomes(); + + public abstract ImmutableList unencounteredNodes(); + + public abstract ImmutableList unencounteredBranches(); + + public abstract String dotGraph(); + + // Currently only supported inside google3. + public abstract String graphUrl(); + + public static Builder builder() { + return new AutoValue_CelCoverageIndex_CoverageReport.Builder() + .setNodes(0L) + .setCoveredNodes(0L) + .setBranches(0L) + .setCelExpression("") + .setDotGraph("") + .setGraphUrl("") + .setCoveredBooleanOutcomes(0L); + } + + /** Builder for {@link CoverageReport}. */ + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder setCelExpression(String value); + + public abstract long nodes(); + + public abstract Builder setNodes(long value); + + public abstract long coveredNodes(); + + public abstract Builder setCoveredNodes(long value); + + public abstract long branches(); + + public abstract Builder setBranches(long value); + + public abstract long coveredBooleanOutcomes(); + + public abstract Builder setCoveredBooleanOutcomes(long value); + + public abstract Builder setDotGraph(String value); + + public abstract Builder setGraphUrl(String value); + + public abstract ImmutableList.Builder unencounteredNodesBuilder(); + + public abstract ImmutableList.Builder unencounteredBranchesBuilder(); + + @CanIgnoreReturnValue + public final Builder addUnencounteredNodes(String value) { + unencounteredNodesBuilder().add(value); + return this; + } + + @CanIgnoreReturnValue + public final Builder addUnencounteredBranches(String value) { + unencounteredBranchesBuilder().add(value); + return this; + } + + public abstract CoverageReport build(); + } + } + + /** + * Generates a coverage report for the CEL test suite. + * + *

Note: If the generated graph URL results in a `Request Entity Too Large` error, download the + * `coverage_graph.txt` file from the test artifacts and upload its contents to Graphviz to render the coverage graph. + */ + public CoverageReport generateCoverageReport() { + CoverageReport.Builder reportBuilder = + CoverageReport.builder().setCelExpression(new CelUnparserVisitor(ast).unparse()); + StringBuilder dotGraphBuilder = new StringBuilder(DIGRAPH_HEADER); + traverseAndCalculateCoverage( + CelNavigableAst.fromAst(ast).getRoot(), + nodeCoverageStatsMap, + true, + "", + reportBuilder, + dotGraphBuilder); + dotGraphBuilder.append("}"); + String dotGraph = dotGraphBuilder.toString(); + + CoverageReport report = reportBuilder.setDotGraph(dotGraph).build(); + logger.info("CEL Expression: " + report.celExpression()); + logger.info("Nodes: " + report.nodes()); + logger.info("Covered Nodes: " + report.coveredNodes()); + logger.info("Branches: " + report.branches()); + logger.info("Covered Boolean Outcomes: " + report.coveredBooleanOutcomes()); + logger.info("Unencountered Nodes: \n" + String.join("\n", report.unencounteredNodes())); + logger.info("Unencountered Branches: \n" + String.join("\n", + report.unencounteredBranches())); + logger.info("Dot Graph: " + report.dotGraph()); + + writeDotGraphToArtifact(dotGraph); + return report; + } + + /** A class for managing the coverage stats for a CEL node. */ + @ThreadSafe + private static final class NodeCoverageStats { + final AtomicBoolean isBooleanNode = new AtomicBoolean(false); + final AtomicBoolean covered = new AtomicBoolean(false); + final AtomicBoolean hasTrueBranch = new AtomicBoolean(false); + final AtomicBoolean hasFalseBranch = new AtomicBoolean(false); + } + + private Boolean isNodeTypeBoolean(CelExpr celExpr) { + return ast.getTypeMap().containsKey(celExpr.id()) + && ast.getTypeMap().get(celExpr.id()).kind().equals(CelKind.BOOL); + } + + private void traverseAndCalculateCoverage( + CelNavigableExpr node, + Map statsMap, + boolean logUnencountered, + String precedingTabs, + CoverageReport.Builder reportBuilder, + StringBuilder dotGraphBuilder) { + long nodeId = node.id(); + NodeCoverageStats stats = statsMap.getOrDefault(nodeId, new NodeCoverageStats()); + reportBuilder.setNodes(reportBuilder.nodes() + 1); + + boolean isInterestingBooleanNode = isInterestingBooleanNode(node, stats); + + String exprText = new CelUnparserVisitor(ast).unparse(node.expr()); + String nodeCoverageStyle = UNCOVERED_NODE_STYLE; + if (stats.covered.get()) { + if (isInterestingBooleanNode) { + if (stats.hasTrueBranch.get() && stats.hasFalseBranch.get()) { + nodeCoverageStyle = COMPLETELY_COVERED_NODE_STYLE; + } else { + nodeCoverageStyle = PARTIALLY_COVERED_NODE_STYLE; + } + } else { + nodeCoverageStyle = COMPLETELY_COVERED_NODE_STYLE; + } + } + String escapedExprText = escapeSpecialCharacters(exprText); + dotGraphBuilder.append( + String.format( + "%d [shape=record, %s, label=\"{<1> exprID: %d | <2> %s} | <3> %s\"];\n", + nodeId, nodeCoverageStyle, nodeId, kindToString(node), escapedExprText)); + + // Update coverage for the current node and determine if we should continue logging + // unencountered. + logUnencountered = + updateNodeCoverage( + nodeId, stats, isInterestingBooleanNode, exprText, logUnencountered, reportBuilder); + + if (isInterestingBooleanNode) { + precedingTabs = + updateBooleanBranchCoverage( + nodeId, stats, exprText, precedingTabs, logUnencountered, reportBuilder); + } + + for (CelNavigableExpr child : node.children().collect(toImmutableList())) { + dotGraphBuilder.append(String.format("%d -> %d;\n", nodeId, child.id())); + traverseAndCalculateCoverage( + child, statsMap, logUnencountered, precedingTabs, reportBuilder, dotGraphBuilder); + } + } + + private boolean isInterestingBooleanNode(CelNavigableExpr node, NodeCoverageStats stats) { + return stats.isBooleanNode.get() + && !node.expr().getKind().equals(ExprKind.Kind.CONSTANT) + && !(node.expr().getKind().equals(ExprKind.Kind.CALL) + && node.expr().call().function().equals("cel.@block")); + } + + /** + * Updates the coverage report based on whether the current node was covered. Returns true if + * logging of unencountered nodes should continue for children, false otherwise. + */ + private boolean updateNodeCoverage( + long nodeId, + NodeCoverageStats stats, + boolean isInterestingBooleanNode, + String exprText, + boolean logUnencountered, + CoverageReport.Builder reportBuilder) { + if (stats.covered.get()) { + reportBuilder.setCoveredNodes(reportBuilder.coveredNodes() + 1); + } else { + if (logUnencountered) { + if (isInterestingBooleanNode) { + reportBuilder.addUnencounteredNodes( + String.format("Expression ID %d ('%s')", nodeId, exprText)); + } + // Once an unencountered node is found, we don't log further unencountered nodes in its + // subtree to avoid noise. + return false; + } + } + return logUnencountered; + } + + /** + * Updates the coverage report for boolean nodes, including branch coverage. Returns the + * potentially modified `precedingTabs` string. + */ + private String updateBooleanBranchCoverage( + long nodeId, + NodeCoverageStats stats, + String exprText, + String precedingTabs, + boolean logUnencountered, + CoverageReport.Builder reportBuilder) { + reportBuilder.setBranches(reportBuilder.branches() + 2); + if (stats.hasTrueBranch.get()) { + reportBuilder.setCoveredBooleanOutcomes(reportBuilder.coveredBooleanOutcomes() + 1); + } else if (logUnencountered) { + reportBuilder.addUnencounteredBranches( + String.format( + "%sExpression ID %d ('%s'): lacks 'true' coverage", precedingTabs, nodeId, exprText)); + precedingTabs += "\t\t"; + } + if (stats.hasFalseBranch.get()) { + reportBuilder.setCoveredBooleanOutcomes(reportBuilder.coveredBooleanOutcomes() + 1); + } else if (logUnencountered) { + reportBuilder.addUnencounteredBranches( + String.format( + "%sExpression ID %d ('%s'): lacks 'false' coverage", + precedingTabs, nodeId, exprText)); + precedingTabs += "\t\t"; + } + return precedingTabs; + } + + @ThreadSafe + private static final class EvaluationListener implements CelEvaluationListener { + + private final ConcurrentHashMap nodeCoverageStatsMap; + + EvaluationListener(ConcurrentHashMap nodeCoverageStatsMap) { + this.nodeCoverageStatsMap = nodeCoverageStatsMap; + } + + @Override + public void callback(CelExpr celExpr, Object evaluationResult) { + NodeCoverageStats nodeCoverageStats = nodeCoverageStatsMap.get(celExpr.id()); + nodeCoverageStats.covered.set(true); + if (nodeCoverageStats.isBooleanNode.get()) { + if (evaluationResult instanceof Boolean) { + if ((Boolean) evaluationResult) { + nodeCoverageStats.hasTrueBranch.set(true); + } else { + nodeCoverageStats.hasFalseBranch.set(true); + } + } + } + } + } + + private String kindToString(CelNavigableExpr node) { + if (node.parent().isPresent() + && node.parent().get().expr().getKind().equals(ExprKind.Kind.COMPREHENSION)) { + CelExpr.CelComprehension comp = node.parent().get().expr().comprehension(); + if (node.id() == comp.iterRange().id()) { + return "IterRange"; + } + if (node.id() == comp.accuInit().id()) { + return "AccuInit"; + } + if (node.id() == comp.loopCondition().id()) { + return "LoopCondition"; + } + if (node.id() == comp.loopStep().id()) { + return "LoopStep"; + } + if (node.id() == comp.result().id()) { + return "Result"; + } + } + + switch (node.getKind()) { + case CALL: + return "Call Node"; + case COMPREHENSION: + return "Comprehension Node"; + case IDENT: + return "Ident Node"; + case LIST: + return "List Node"; + case CONSTANT: + return "Literal Node"; + case MAP: + return "Map Node"; + case SELECT: + return "Select Node"; + case STRUCT: + return "Struct Node"; + default: + return "Unspecified Node"; + } + } + + private static String escapeSpecialCharacters(String exprText) { + return exprText + .replace("\\\"", "\"") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("||", " \\| \\| ") + .replace("<", "\\<") + .replace(">", "\\>") + .replace("{", "\\{") + .replace("}", "\\}"); + } + + private void writeDotGraphToArtifact(String dotGraph) { + String testOutputsDir = System.getenv("TEST_UNDECLARED_OUTPUTS_DIR"); + if (testOutputsDir == null) { + // Only for non-bazel/blaze users, we write to a subdirectory under the cwd. + testOutputsDir = "cel_artifacts"; + } + File outputDir = new File(testOutputsDir, "cel_test_coverage"); + if (!outputDir.exists()) { + outputDir.mkdirs(); + } + try { + Files.asCharSink(new File(outputDir, "coverage_graph.txt"), UTF_8).write(dotGraph); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/testing/src/main/java/dev/cel/testing/testrunner/CelExpressionSource.java b/testing/src/main/java/dev/cel/testing/testrunner/CelExpressionSource.java new file mode 100644 index 000000000..23075ac41 --- /dev/null +++ b/testing/src/main/java/dev/cel/testing/testrunner/CelExpressionSource.java @@ -0,0 +1,75 @@ +// 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.testing.testrunner; + +import com.google.auto.value.AutoValue; + +/** + * CelExpressionSource is an encapsulation around cel_expr file format argument accepted in + * cel_java_test/cel_test bzl macro. It either holds a {@link CheckedExpr} in binarypb/textproto + * format, a serialized {@link CelPolicy} file in yaml/celpolicy format or a raw cel expression in + * cel file format or string format. + */ +@AutoValue +public abstract class CelExpressionSource { + + /** Returns the value of the expression source. This can be a file path or a raw expression. */ + public abstract String value(); + + /** Returns the type of the expression source. */ + public abstract ExpressionSourceType type(); + + /** + * Creates a {@link CelExpressionSource} from a file path. The type of the expression source is + * inferred from the file extension. + */ + public static CelExpressionSource fromSource(String value) { + return new AutoValue_CelExpressionSource(value, ExpressionSourceType.fromSource(value)); + } + + /** Creates a {@link CelExpressionSource} from a raw CEL expression string. */ + public static CelExpressionSource fromRawExpr(String value) { + return new AutoValue_CelExpressionSource(value, ExpressionSourceType.RAW_EXPR); + } + + /** + * ExpressionSourceType is an enumeration of the supported expression file types. + * + *

This enumeration is used to determine the type of the expression file based on the file + * extension. + */ + public enum ExpressionSourceType { + BINARYPB, + TEXTPROTO, + POLICY, + CEL, + RAW_EXPR; + + private static ExpressionSourceType fromSource(String filePath) { + if (filePath.endsWith(".binarypb")) { + return BINARYPB; + } + if (filePath.endsWith(".textproto")) { + return TEXTPROTO; + } + if (filePath.endsWith(".yaml") || filePath.endsWith(".celpolicy")) { + return POLICY; + } + if (filePath.endsWith(".cel")) { + return CEL; + } + throw new IllegalArgumentException("Unsupported expression file type: " + filePath); + } + } +} diff --git a/testing/src/main/java/dev/cel/testing/testrunner/CelTestContext.java b/testing/src/main/java/dev/cel/testing/testrunner/CelTestContext.java new file mode 100644 index 000000000..6ef988a44 --- /dev/null +++ b/testing/src/main/java/dev/cel/testing/testrunner/CelTestContext.java @@ -0,0 +1,228 @@ +// 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.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; + +/** + * The context class for a CEL test, holding configurations needed to create environments and + * evaluate CEL expressions and policies. + */ +@AutoValue +public abstract class CelTestContext { + + private static final Cel DEFAULT_CEL = CelFactory.standardCelBuilder().build(); + + /** + * The CEL environment for the CEL test. + * + *

The CEL environment is created by extending the provided base CEL environment with the + * config file if provided. + */ + public abstract Cel cel(); + + /** + * The CEL policy parser for the CEL test. + * + *

A custom parser to be used for parsing CEL policies in scenarios where custom policy tags + * are used. If not provided, the default CEL policy parser will be used. + */ + public abstract Optional celPolicyParser(); + + /** + * The CEL options for the CEL test. + * + *

The CEL options are used to configure the {@link Cel} environment. + */ + public abstract CelOptions celOptions(); + + /** + * The late function bindings for the CEL test. + * + *

These bindings are used to provide functions which are to be consumed during the eval phase + * directly. + */ + 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. + * + *

These bindings are used to provide values for variables for which it is difficult to provide + * a value in the test suite file for example, using proto extensions or fetching the value from + * some other source. + */ + public abstract ImmutableMap variableBindings(); + + /** + * The result matcher for the CEL test. + * + *

This matcher is used to perform assertions on the result of a CEL test case. + */ + public abstract ResultMatcher resultMatcher(); + + /** + * The CEL expression to be tested. Could be a expression string or a policy/cel file path. This + * should only be used when invoking the runner library directly. + */ + public abstract Optional celExpression(); + + /** + * The config file for the CEL test. + * + *

The config file is used to provide a custom environment for the CEL test. + */ + public abstract Optional configFile(); + + /** + * The file descriptor set path for the CEL test. + * + *

The file descriptor set path is used to provide proto descriptors for the CEL test. + */ + 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(); + + /** Returns a new builder for {@link CelTestContext}. */ + public static CelTestContext.Builder newBuilder() { + return new AutoValue_CelTestContext.Builder() + .setCel(DEFAULT_CEL) + .setCelOptions(CelOptions.DEFAULT) + .setVariableBindings(ImmutableMap.of()) + .setResultMatcher(new DefaultResultMatcher()); + } + + /** Builder for {@link CelTestContext}. */ + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder setCel(Cel cel); + + public abstract Builder setCelPolicyParser(CelPolicyParser celPolicyParser); + + public abstract Builder setCelOptions(CelOptions celOptions); + + public abstract Builder setCelLateFunctionBindings( + CelLateFunctionBindings celLateFunctionBindings); + + public abstract Builder setBindingTransformer(BindingTransformer bindingTransformer); + + public abstract Builder setVariableBindings(Map variableBindings); + + public abstract Builder setResultMatcher(ResultMatcher resultMatcher); + + public abstract Builder setCelExpression(CelExpressionSource celExpression); + + public abstract Builder setConfigFile(String configFile); + + 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 new file mode 100644 index 000000000..a8869a8fb --- /dev/null +++ b/testing/src/main/java/dev/cel/testing/testrunner/CelTestSuite.java @@ -0,0 +1,245 @@ +// 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.testing.testrunner; + +import com.google.auto.value.AutoOneOf; +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.CheckReturnValue; +import com.google.protobuf.Any; +import dev.cel.common.Source; +import java.util.Optional; +import java.util.Set; + +/** Class representing a CEL test suite which is generated post parsing the test suite file. */ +@AutoValue +public abstract class CelTestSuite { + + public abstract String name(); + + /** Test suite source in textual format (ex: textproto, YAML). */ + public abstract Optional source(); + + public abstract String description(); + + public abstract ImmutableSet sections(); + + /** Builder for {@link CelTestSuite}. */ + @AutoValue.Builder + public abstract static class Builder { + + public abstract Builder setName(String name); + + public abstract Builder setDescription(String description); + + public abstract Builder setSections(Set section); + + public abstract Builder setSections(CelTestSection... sections); + + public abstract Builder setSource(Source source); + + @CheckReturnValue + public abstract CelTestSuite build(); + } + + public abstract Builder toBuilder(); + + public static Builder newBuilder() { + return new AutoValue_CelTestSuite.Builder(); + } + + /** + * Class representing a CEL test section within a test suite following the schema in {@link + * dev.cel.expr.conformance.test.TestSuite}. + */ + @AutoValue + public abstract static class CelTestSection { + + public abstract String name(); + + public abstract String description(); + + public abstract ImmutableSet tests(); + + /** Builder for {@link CelTestSection}. */ + @AutoValue.Builder + public abstract static class Builder { + + public abstract Builder setName(String name); + + public abstract Builder setTests(Set tests); + + public abstract Builder setTests(CelTestCase... tests); + + public abstract Builder setDescription(String description); + + @CheckReturnValue + public abstract CelTestSection build(); + } + + public abstract Builder toBuilder(); + + public static Builder newBuilder() { + return new AutoValue_CelTestSuite_CelTestSection.Builder().setDescription(""); + } + + /** Class representing a CEL test case within a test section. */ + @AutoValue + public abstract static class CelTestCase { + + public abstract String name(); + + public abstract String description(); + + public abstract Input input(); + + public abstract Output output(); + + /** This class represents the input of a CEL test case. */ + @AutoOneOf(Input.Kind.class) + public abstract static class Input { + /** Kind of input for a CEL test case. */ + public enum Kind { + BINDINGS, + CONTEXT_EXPR, + CONTEXT_MESSAGE, + NO_INPUT + } + + public abstract Input.Kind kind(); + + public abstract ImmutableMap bindings(); + + public abstract String contextExpr(); + + public abstract Any contextMessage(); + + public abstract void noInput(); + + public static Input ofBindings(ImmutableMap bindings) { + return AutoOneOf_CelTestSuite_CelTestSection_CelTestCase_Input.bindings(bindings); + } + + public static Input ofContextExpr(String contextExpr) { + return AutoOneOf_CelTestSuite_CelTestSection_CelTestCase_Input.contextExpr(contextExpr); + } + + public static Input ofContextMessage(Any contextMessage) { + return AutoOneOf_CelTestSuite_CelTestSection_CelTestCase_Input.contextMessage( + contextMessage); + } + + public static Input ofNoInput() { + return AutoOneOf_CelTestSuite_CelTestSection_CelTestCase_Input.noInput(); + } + + /** This class represents a binding for a CEL test case. */ + @AutoOneOf(Binding.Kind.class) + public abstract static class Binding { + + /** Kind of binding for a CEL test case. */ + public enum Kind { + VALUE, + EXPR + } + + public abstract Binding.Kind kind(); + + public abstract Object value(); + + public abstract String expr(); + + public static Binding ofValue(Object value) { + return AutoOneOf_CelTestSuite_CelTestSection_CelTestCase_Input_Binding.value(value); + } + + public static Binding ofExpr(String expr) { + return AutoOneOf_CelTestSuite_CelTestSection_CelTestCase_Input_Binding.expr(expr); + } + } + } + + /** This class represents the result of a CEL test case. */ + @AutoOneOf(Output.Kind.class) + public abstract static class Output { + /** Kind of result for a CEL test case. */ + public enum Kind { + RESULT_VALUE, + RESULT_EXPR, + EVAL_ERROR, + UNKNOWN_SET, + NO_OUTPUT + } + + public abstract Output.Kind kind(); + + public abstract Object resultValue(); + + public abstract String resultExpr(); + + public abstract void noOutput(); + + public abstract ImmutableList evalError(); + + public abstract ImmutableList unknownSet(); + + public static Output ofResultValue(Object resultValue) { + return AutoOneOf_CelTestSuite_CelTestSection_CelTestCase_Output.resultValue(resultValue); + } + + public static Output ofResultExpr(String resultExpr) { + return AutoOneOf_CelTestSuite_CelTestSection_CelTestCase_Output.resultExpr(resultExpr); + } + + public static Output ofEvalError(ImmutableList errors) { + return AutoOneOf_CelTestSuite_CelTestSection_CelTestCase_Output.evalError(errors); + } + + public static Output ofUnknownSet(ImmutableList unknownSet) { + return AutoOneOf_CelTestSuite_CelTestSection_CelTestCase_Output.unknownSet(unknownSet); + } + + public static Output ofNoOutput() { + return AutoOneOf_CelTestSuite_CelTestSection_CelTestCase_Output.noOutput(); + } + } + + /** Builder for {@link CelTestCase}. */ + @AutoValue.Builder + public abstract static class Builder { + + public abstract Builder setName(String name); + + public abstract Builder setDescription(String description); + + public abstract Builder setInput(Input input); + + public abstract Builder setOutput(Output output); + + @CheckReturnValue + public abstract CelTestCase build(); + } + + public abstract Builder toBuilder(); + + public static Builder newBuilder() { + return new AutoValue_CelTestSuite_CelTestSection_CelTestCase.Builder() + .setInput(Input.ofNoInput()) // Default input to no input. + .setDescription(""); + } + } + } +} diff --git a/testing/src/main/java/dev/cel/testing/testrunner/CelTestSuiteException.java b/testing/src/main/java/dev/cel/testing/testrunner/CelTestSuiteException.java new file mode 100644 index 000000000..3b3449df4 --- /dev/null +++ b/testing/src/main/java/dev/cel/testing/testrunner/CelTestSuiteException.java @@ -0,0 +1,29 @@ +// 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.testing.testrunner; + +import dev.cel.common.CelException; + +/** Checked exception thrown when a CEL test suite is misconfigured. */ +public final class CelTestSuiteException extends CelException { + + CelTestSuiteException(String message) { + super(message); + } + + CelTestSuiteException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/testing/src/main/java/dev/cel/testing/testrunner/CelTestSuiteTextProtoParser.java b/testing/src/main/java/dev/cel/testing/testrunner/CelTestSuiteTextProtoParser.java new file mode 100644 index 000000000..9c0ab4720 --- /dev/null +++ b/testing/src/main/java/dev/cel/testing/testrunner/CelTestSuiteTextProtoParser.java @@ -0,0 +1,188 @@ +// 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.testing.testrunner; + +import dev.cel.expr.Status; +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.protobuf.ExtensionRegistry; +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; +import dev.cel.expr.conformance.test.TestSuite; +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. + */ +@Internal +public final class CelTestSuiteTextProtoParser { + + /** Creates a new instance of {@link CelTestSuiteTextProtoParser}. */ + public static CelTestSuiteTextProtoParser newInstance() { + return new CelTestSuiteTextProtoParser(); + } + + 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, TypeRegistry customTypeRegistry, ExtensionRegistry customExtensionRegistry) + throws IOException { + String fileDescriptorSetPath = System.getProperty("file_descriptor_set_path"); + TypeRegistry typeRegistry = customTypeRegistry; + ExtensionRegistry extensionRegistry = customExtensionRegistry; + if (fileDescriptorSetPath != null) { + 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(); + try { + parser.merge(textProto, extensionRegistry, builder); + } catch (ParseException e) { + throw new IllegalArgumentException("Failed to parse test suite", e); + } + return builder.build(); + } + + @VisibleForTesting + static CelTestSuite parseCelTestSuite(TestSuite testSuite) throws CelTestSuiteException { + CelTestSuite.Builder builder = + CelTestSuite.newBuilder() + .setName(testSuite.getName()) + .setDescription(testSuite.getDescription()); + ImmutableSet.Builder sectionSetBuilder = ImmutableSet.builder(); + + for (TestSection section : testSuite.getSectionsList()) { + CelTestSection.Builder sectionBuilder = + CelTestSection.newBuilder() + .setName(section.getName()) + .setDescription(section.getDescription()); + ImmutableSet.Builder testCaseSetBuilder = ImmutableSet.builder(); + + for (TestCase testCase : section.getTestsList()) { + CelTestCase.Builder testCaseBuilder = + CelTestCase.newBuilder() + .setName(testCase.getName()) + .setDescription(testCase.getDescription()); + addInputs(testCaseBuilder, testCase); + addOutputs(testCaseBuilder, testCase); + testCaseSetBuilder.add(testCaseBuilder.build()); + } + + sectionBuilder.setTests(testCaseSetBuilder.build()); + sectionSetBuilder.add(sectionBuilder.build()); + } + return builder.setSections(sectionSetBuilder.build()).build(); + } + + private static void addInputs(CelTestCase.Builder testCaseBuilder, TestCase testCase) + throws CelTestSuiteException { + if (testCase.getInputCount() > 0 && testCase.hasInputContext()) { + throw new CelTestSuiteException( + String.format( + "Test case: %s cannot have both input map and input context.", testCase.getName())); + } else if (testCase.getInputCount() > 0) { + testCaseBuilder.setInput(parseInputMap(testCase)); + } else if (testCase.hasInputContext()) { + testCaseBuilder.setInput(parseInputContext(testCase)); + } else { + testCaseBuilder.setInput(CelTestCase.Input.ofNoInput()); + } + } + + private static CelTestCase.Input parseInputMap(TestCase testCase) { + ImmutableMap.Builder inputMapBuilder = ImmutableMap.builder(); + for (Map.Entry entry : testCase.getInputMap().entrySet()) { + InputValue inputValue = entry.getValue(); + if (inputValue.hasValue()) { + inputMapBuilder.put(entry.getKey(), Binding.ofValue(inputValue.getValue())); + } else if (inputValue.hasExpr()) { + inputMapBuilder.put(entry.getKey(), Binding.ofExpr(inputValue.getExpr())); + } + } + return CelTestCase.Input.ofBindings(inputMapBuilder.buildOrThrow()); + } + + private static CelTestCase.Input parseInputContext(TestCase testCase) { + if (testCase.getInputContext().hasContextMessage()) { + return CelTestCase.Input.ofContextMessage(testCase.getInputContext().getContextMessage()); + } else if (testCase.getInputContext().hasContextExpr()) { + return CelTestCase.Input.ofContextExpr(testCase.getInputContext().getContextExpr()); + } + return CelTestCase.Input.ofNoInput(); + } + + private static void addOutputs(CelTestCase.Builder testCaseBuilder, TestCase testCase) { + if (testCase.hasOutput()) { + switch (testCase.getOutput().getResultKindCase()) { + case RESULT_VALUE: + testCaseBuilder.setOutput( + CelTestCase.Output.ofResultValue(testCase.getOutput().getResultValue())); + break; + case RESULT_EXPR: + testCaseBuilder.setOutput( + CelTestCase.Output.ofResultExpr(testCase.getOutput().getResultExpr())); + break; + case EVAL_ERROR: + testCaseBuilder.setOutput(CelTestCase.Output.ofEvalError(parseEvalError(testCase))); + break; + default: + break; + } + } + } + + private static ImmutableList parseEvalError(TestCase testCase) { + ImmutableList.Builder evalErrorSetBuilder = ImmutableList.builder(); + for (Status error : testCase.getOutput().getEvalError().getErrorsList()) { + evalErrorSetBuilder.add(error.getMessage()); + } + return evalErrorSetBuilder.build(); + } + + private CelTestSuiteTextProtoParser() {} +} diff --git a/testing/src/main/java/dev/cel/testing/testrunner/CelTestSuiteYamlParser.java b/testing/src/main/java/dev/cel/testing/testrunner/CelTestSuiteYamlParser.java new file mode 100644 index 000000000..2340bf229 --- /dev/null +++ b/testing/src/main/java/dev/cel/testing/testrunner/CelTestSuiteYamlParser.java @@ -0,0 +1,383 @@ +// 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.testing.testrunner; + +import static dev.cel.common.formats.YamlHelper.YamlNodeType.nodeType; +import static dev.cel.common.formats.YamlHelper.assertYamlType; +import static dev.cel.common.formats.YamlHelper.newBoolean; +import static dev.cel.common.formats.YamlHelper.newDouble; +import static dev.cel.common.formats.YamlHelper.newInteger; +import static dev.cel.common.formats.YamlHelper.newString; +import static dev.cel.common.formats.YamlHelper.parseYamlSource; + +import com.google.common.collect.ImmutableList; +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; +import dev.cel.common.formats.YamlParserContextImpl; +import dev.cel.common.internal.CelCodePointArray; +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 java.util.Optional; +import org.yaml.snakeyaml.nodes.MappingNode; +import org.yaml.snakeyaml.nodes.Node; +import org.yaml.snakeyaml.nodes.NodeTuple; +import org.yaml.snakeyaml.nodes.ScalarNode; +import org.yaml.snakeyaml.nodes.SequenceNode; + +/** + * 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. + */ +@Internal +public final class CelTestSuiteYamlParser { + + /** Creates a new instance of {@link CelTestSuiteYamlParser}. */ + public static CelTestSuiteYamlParser newInstance() { + return new CelTestSuiteYamlParser(); + } + + public CelTestSuite parse(String celTestSuiteYamlContent) throws CelTestSuiteException { + return parseYaml(celTestSuiteYamlContent, ""); + } + + private CelTestSuite parseYaml(String celTestSuiteYamlContent, String description) + throws CelTestSuiteException { + Node node; + try { + node = + parseYamlSource(celTestSuiteYamlContent) + .orElseThrow( + () -> + new CelTestSuiteException( + String.format( + "YAML document empty or malformed: %s", celTestSuiteYamlContent))); + } catch (RuntimeException e) { + throw new CelTestSuiteException("YAML document is malformed: " + e.getMessage(), e); + } + + CelFileSource testSuiteSource = + CelFileSource.newBuilder(CelCodePointArray.fromString(celTestSuiteYamlContent)) + .setDescription(description) + .build(); + ParserContext ctx = YamlParserContextImpl.newInstance(testSuiteSource); + CelTestSuite.Builder builder = parseTestSuite(ctx, node); + testSuiteSource = testSuiteSource.toBuilder().setPositionsMap(ctx.getIdToOffsetMap()).build(); + + if (!ctx.getIssues().isEmpty()) { + throw new CelTestSuiteException(CelIssue.toDisplayString(ctx.getIssues(), testSuiteSource)); + } + + return builder.setSource(testSuiteSource).build(); + } + + private CelTestSuite.Builder parseTestSuite(ParserContext ctx, Node node) { + 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()); + return builder; + } + + MappingNode rootNode = (MappingNode) node; + for (NodeTuple nodeTuple : rootNode.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 fieldName = ((ScalarNode) keyNode).getValue(); + switch (fieldName) { + case "name": + builder.setName(newString(ctx, valueNode)); + break; + case "description": + builder.setDescription(newString(ctx, valueNode)); + break; + case "section": + case "sections": + builder.setSections(parseSections(ctx, valueNode)); + break; + default: + ctx.reportError(keyId, "Unknown test suite tag: " + fieldName); + break; + } + } + return builder; + } + + private ImmutableSet parseSections(ParserContext ctx, Node node) { + long valueId = ctx.collectMetadata(node); + ImmutableSet.Builder celTestSectionSetBuilder = ImmutableSet.builder(); + if (!assertYamlType(ctx, valueId, node, YamlNodeType.LIST)) { + ctx.reportError(valueId, "Sections is not a list: " + node.getTag()); + return celTestSectionSetBuilder.build(); + } + + SequenceNode sectionListNode = (SequenceNode) node; + for (Node elementNode : sectionListNode.getValue()) { + celTestSectionSetBuilder.add(parseSection(ctx, elementNode)); + } + return celTestSectionSetBuilder.build(); + } + + private CelTestSection parseSection(ParserContext ctx, Node node) { + long valueId = ctx.collectMetadata(node); + if (!assertYamlType(ctx, valueId, node, YamlNodeType.MAP)) { + ctx.reportError(valueId, "Unknown section type: " + node.getTag()); + return CelTestSection.newBuilder().build(); + } + + CelTestSection.Builder celTestSectionBuilder = CelTestSection.newBuilder(); + MappingNode sectionNode = (MappingNode) node; + for (NodeTuple nodeTuple : sectionNode.getValue()) { + Node keyNode = nodeTuple.getKeyNode(); + long keyId = ctx.collectMetadata(keyNode); + Node valueNode = nodeTuple.getValueNode(); + String fieldName = ((ScalarNode) keyNode).getValue(); + switch (fieldName) { + case "name": + celTestSectionBuilder.setName(newString(ctx, valueNode)); + break; + case "description": + celTestSectionBuilder.setDescription(newString(ctx, valueNode)); + break; + case "tests": + celTestSectionBuilder.setTests(parseTests(ctx, valueNode)); + break; + default: + ctx.reportError(keyId, "Unknown test section tag: " + fieldName); + break; + } + } + return celTestSectionBuilder.build(); + } + + private ImmutableSet parseTests(ParserContext ctx, Node node) { + long valueId = ctx.collectMetadata(node); + ImmutableSet.Builder celTestCaseSetBuilder = ImmutableSet.builder(); + if (!assertYamlType(ctx, valueId, node, YamlNodeType.LIST)) { + ctx.reportError(valueId, "Tests is not a list: " + node.getTag()); + return celTestCaseSetBuilder.build(); + } + + SequenceNode testCasesListNode = (SequenceNode) node; + for (Node elementNode : testCasesListNode.getValue()) { + celTestCaseSetBuilder.add(parseTestCase(ctx, elementNode)); + } + return celTestCaseSetBuilder.build(); + } + + private CelTestCase parseTestCase(ParserContext ctx, Node node) { + long valueId = ctx.collectMetadata(node); + CelTestCase.Builder celTestCaseBuilder = CelTestCase.newBuilder(); + if (!assertYamlType(ctx, valueId, node, YamlNodeType.MAP)) { + ctx.reportError(valueId, "Testcase is not a map: " + node.getTag()); + return celTestCaseBuilder.build(); + } + MappingNode testCaseNode = (MappingNode) node; + for (NodeTuple nodeTuple : testCaseNode.getValue()) { + Node keyNode = nodeTuple.getKeyNode(); + long keyId = ctx.collectMetadata(keyNode); + Node valueNode = nodeTuple.getValueNode(); + String fieldName = ((ScalarNode) keyNode).getValue(); + switch (fieldName) { + case "name": + celTestCaseBuilder.setName(newString(ctx, valueNode)); + break; + case "description": + celTestCaseBuilder.setDescription(newString(ctx, valueNode)); + break; + case "input": + celTestCaseBuilder.setInput(parseInput(ctx, valueNode)); + break; + case "context_expr": + celTestCaseBuilder.setInput(parseContextExpr(ctx, valueNode)); + break; + case "output": + celTestCaseBuilder.setOutput(parseOutput(ctx, valueNode)); + break; + default: + ctx.reportError(keyId, "Unknown test case tag: " + fieldName); + break; + } + } + return celTestCaseBuilder.build(); + } + + private CelTestCase.Input parseInput(ParserContext ctx, Node node) { + long valueId = ctx.collectMetadata(node); + if (!assertYamlType(ctx, valueId, node, YamlNodeType.MAP)) { + ctx.reportError(valueId, "Input is not a map: " + node.getTag()); + return CelTestCase.Input.ofNoInput(); + } + MappingNode inputNode = (MappingNode) node; + ImmutableMap.Builder bindingsBuilder = ImmutableMap.builder(); + for (NodeTuple nodeTuple : inputNode.getValue()) { + Node valueNode = nodeTuple.getValueNode(); + Optional binding = parseBindingValueNode(ctx, valueNode); + binding.ifPresent( + b -> bindingsBuilder.put(((ScalarNode) nodeTuple.getKeyNode()).getValue(), b)); + } + return CelTestCase.Input.ofBindings(bindingsBuilder.buildOrThrow()); + } + + private Optional parseBindingValueNode(ParserContext ctx, Node node) { + long valueId = ctx.collectMetadata(node); + if (!assertYamlType(ctx, valueId, node, YamlNodeType.MAP)) { + ctx.reportError(valueId, "Input binding node is not a map: " + node.getTag()); + return Optional.empty(); + } + MappingNode bindingValueNode = (MappingNode) node; + + if (bindingValueNode.getValue().size() != 1) { + ctx.reportError(valueId, "Input binding node must have exactly one value: " + node.getTag()); + return Optional.empty(); + } + + for (NodeTuple nodeTuple : bindingValueNode.getValue()) { + Node keyNode = nodeTuple.getKeyNode(); + long keyId = ctx.collectMetadata(keyNode); + Node valueNode = nodeTuple.getValueNode(); + String fieldName = ((ScalarNode) keyNode).getValue(); + switch (fieldName) { + case "value": + return Optional.of(Binding.ofValue(parseNodeValue(ctx, valueNode))); + case "expr": + return Optional.of(Binding.ofExpr(newString(ctx, valueNode))); + default: + ctx.reportError(keyId, "Unknown input binding value tag: " + fieldName); + break; + } + } + return Optional.empty(); + } + + // TODO: Create a CelTestSuiteNodeValue class to represent the value of a test suite + // node. + private Object parseNodeValue(ParserContext ctx, Node node) { + Object value = null; + Optional yamlNodeType = nodeType(node.getTag().getValue()); + if (yamlNodeType.isPresent()) { + switch (yamlNodeType.get()) { + case STRING: + case TEXT: + value = newString(ctx, node); + break; + case BOOLEAN: + value = newBoolean(ctx, node); + break; + case INTEGER: + value = newInteger(ctx, node); + break; + case DOUBLE: + value = newDouble(ctx, node); + break; + case MAP: + value = parseMap(ctx, node); + break; + case LIST: + value = parseList(ctx, node); + break; + } + } + return value; + } + + private ImmutableMap parseMap(ParserContext ctx, Node node) { + ImmutableMap.Builder mapBuilder = ImmutableMap.builder(); + MappingNode mapNode = (MappingNode) node; + mapNode + .getValue() + .forEach( + nodeTuple -> { + Node keyNode = nodeTuple.getKeyNode(); + Node valueNode = nodeTuple.getValueNode(); + mapBuilder.put(parseNodeValue(ctx, keyNode), parseNodeValue(ctx, valueNode)); + }); + return mapBuilder.buildOrThrow(); + } + + private ImmutableList parseList(ParserContext ctx, Node node) { + ImmutableList.Builder listBuilder = ImmutableList.builder(); + SequenceNode listNode = (SequenceNode) node; + listNode.getValue().forEach(childNode -> listBuilder.add(parseNodeValue(ctx, childNode))); + return listBuilder.build(); + } + + private ImmutableList parseUnknown(ParserContext ctx, Node node) { + ImmutableList unknown = parseList(ctx, node); + ImmutableList.Builder unknownBuilder = ImmutableList.builder(); + for (Object object : unknown) { + if (object instanceof Integer) { + unknownBuilder.add(Long.valueOf((Integer) object)); + } else { + ctx.reportError( + ctx.collectMetadata(node), + "Only integer ids are supported in unknown list. Found: " + + object.getClass().getName()); + } + } + return unknownBuilder.build(); + } + + private CelTestCase.Input parseContextExpr(ParserContext ctx, Node node) { + long valueId = ctx.collectMetadata(node); + if (!assertYamlType(ctx, valueId, node, YamlNodeType.STRING)) { + ctx.reportError(valueId, "Input context is not a string: " + node.getTag()); + return CelTestCase.Input.ofNoInput(); + } + return CelTestCase.Input.ofContextExpr(newString(ctx, node)); + } + + private CelTestCase.Output parseOutput(ParserContext ctx, Node node) { + long valueId = ctx.collectMetadata(node); + if (!assertYamlType(ctx, valueId, node, YamlNodeType.MAP)) { + ctx.reportError(valueId, "Output is not a map: " + node.getTag()); + return CelTestCase.Output.ofNoOutput(); + } + MappingNode outputNode = (MappingNode) node; + for (NodeTuple nodeTuple : outputNode.getValue()) { + Node keyNode = nodeTuple.getKeyNode(); + long keyId = ctx.collectMetadata(keyNode); + Node valueNode = nodeTuple.getValueNode(); + String fieldName = ((ScalarNode) keyNode).getValue(); + switch (fieldName) { + case "value": + return CelTestCase.Output.ofResultValue(parseNodeValue(ctx, valueNode)); + case "expr": + return CelTestCase.Output.ofResultExpr(newString(ctx, valueNode)); + case "error_set": + return CelTestCase.Output.ofEvalError(parseList(ctx, valueNode)); + case "unknown": + return CelTestCase.Output.ofUnknownSet(parseUnknown(ctx, valueNode)); + default: + ctx.reportError(keyId, "Unknown output tag: " + fieldName); + break; + } + } + return CelTestCase.Output.ofNoOutput(); + } + + private CelTestSuiteYamlParser() {} +} diff --git a/testing/src/main/java/dev/cel/testing/testrunner/CelUserTestTemplate.java b/testing/src/main/java/dev/cel/testing/testrunner/CelUserTestTemplate.java new file mode 100644 index 000000000..02180ffb7 --- /dev/null +++ b/testing/src/main/java/dev/cel/testing/testrunner/CelUserTestTemplate.java @@ -0,0 +1,83 @@ +// 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.testing.testrunner; + +import dev.cel.testing.testrunner.CelTestSuite.CelTestSection.CelTestCase; +import org.jspecify.annotations.Nullable; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +/** + * Template to be extended by user's test class in order to be parameterized based on individual + * test cases. + */ +@RunWith(Parameterized.class) +public abstract class CelUserTestTemplate { + + @Parameter(0) + public CelTestCase testCase; + + @Parameter(1) + public @Nullable CelCoverageIndex celCoverageIndex; + + private final CelTestContext celTestContext; + + public CelUserTestTemplate(CelTestContext celTestContext) { + this.celTestContext = celTestContext; + } + + @Test + public void test() throws Exception { + if (celCoverageIndex != null) { + TestRunnerLibrary.runTest(testCase, updateCelTestContext(celTestContext), celCoverageIndex); + } else { + TestRunnerLibrary.runTest(testCase, updateCelTestContext(celTestContext)); + } + } + + /** + * Updates the CEL test context based on the system properties. + * + *

This method is used to update the CEL test context based on the system properties. It checks + * if the runner library is triggered via blaze macro or via JUnit and assigns values accordingly. + * + * @param celTestContext The CEL test context to update. + * @return The updated CEL test context. + */ + private CelTestContext updateCelTestContext(CelTestContext celTestContext) { + String celExpr = System.getProperty("cel_expr"); + String configPath = System.getProperty("config_path"); + String fileDescriptorSetPath = System.getProperty("file_descriptor_set_path"); + String isRawExpr = System.getProperty("is_raw_expr"); + + CelTestContext.Builder celTestContextBuilder = celTestContext.toBuilder(); + if (celExpr != null) { + if (isRawExpr.equals("True")) { + celTestContextBuilder.setCelExpression(CelExpressionSource.fromRawExpr(celExpr)); + } else { + celTestContextBuilder.setCelExpression(CelExpressionSource.fromSource(celExpr)); + } + } + if (configPath != null) { + celTestContextBuilder.setConfigFile(configPath); + } + if (fileDescriptorSetPath != null) { + celTestContextBuilder.setFileDescriptorSetPath(fileDescriptorSetPath); + } + return celTestContextBuilder.build(); + } +} diff --git a/testing/src/main/java/dev/cel/testing/testrunner/DefaultResultMatcher.java b/testing/src/main/java/dev/cel/testing/testrunner/DefaultResultMatcher.java new file mode 100644 index 000000000..279d591a2 --- /dev/null +++ b/testing/src/main/java/dev/cel/testing/testrunner/DefaultResultMatcher.java @@ -0,0 +1,114 @@ +// 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.testing.testrunner; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; +import static com.google.protobuf.LegacyUnredactedTextFormat.legacyUnredactedStringValueOf; +import static dev.cel.testing.utils.ExprValueUtils.toExprValue; + +import dev.cel.expr.ExprValue; +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 { + + @Override + public void match(ResultMatcherParams params, Cel cel) throws Exception { + Output result = params.expectedOutput().get(); + switch (result.kind()) { + case RESULT_EXPR: + if (params.computedOutput().kind().equals(ComputedOutput.Kind.ERROR)) { + throw new AssertionError( + "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; + try { + evaluationResult = exprProgram.eval(); + } catch (CelEvaluationException e) { + throw new IllegalArgumentException( + "Failed to evaluate result_expr: " + e.getMessage(), e); + } + ExprValue expectedExprValue = toExprValue(evaluationResult, exprAst.getResultType()); + assertThat(params.computedOutput().exprValue()).isEqualTo(expectedExprValue); + break; + case RESULT_VALUE: + if (params.computedOutput().kind().equals(ComputedOutput.Kind.ERROR)) { + throw new AssertionError( + "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())); + break; + case EVAL_ERROR: + if (params.computedOutput().kind().equals(ComputedOutput.Kind.EXPR_VALUE)) { + throw new AssertionError( + "Evaluation was successful but no value was provided. Computed output: " + + legacyUnredactedStringValueOf(params.computedOutput().exprValue())); + } + assertThat(params.computedOutput().error().toString()) + .contains(result.evalError().get(0).toString()); + break; + case UNKNOWN_SET: + assertThat(params.computedOutput().unknownSet()) + .containsExactlyElementsIn(result.unknownSet()); + break; + default: + throw new IllegalArgumentException("Unexpected output type: " + result.kind()); + } + } + + private static void assertExprValue(ExprValue exprValue, ExprValue expectedExprValue) + 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(descriptors), + RegistryUtils.getExtensionRegistry(descriptors)) + .isEqualTo(expectedExprValue); + } else { + assertThat(exprValue) + .ignoringRepeatedFieldOrderOfFieldDescriptors( + MapValue.getDescriptor().findFieldByName("entries")) + .isEqualTo(expectedExprValue); + } + } +} diff --git a/testing/src/main/java/dev/cel/testing/testrunner/JUnitXmlReporter.java b/testing/src/main/java/dev/cel/testing/testrunner/JUnitXmlReporter.java new file mode 100644 index 000000000..7be21e1e3 --- /dev/null +++ b/testing/src/main/java/dev/cel/testing/testrunner/JUnitXmlReporter.java @@ -0,0 +1,291 @@ +// 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.testing.testrunner; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.base.Throwables; +import com.google.common.collect.Lists; +import java.io.BufferedWriter; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.List; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import org.jspecify.annotations.Nullable; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Text; + +/** Reporter class to generate an xml report in junit format. */ +final class JUnitXmlReporter { + private String outputFileName = null; + private File outputFile = null; + private TestContext testContext = null; + // NOMUTANTS -- To be fixed in b/394771693 and when more failure tests are added. + private int numFailed = 0; + + private final List allTests = Lists.newArrayList(); + + /** Creates an instance that will write to {@code outputFileName}. */ + JUnitXmlReporter(String outputFileName) { + this.outputFileName = outputFileName; + } + + /** Called for each test case */ + void onTestStart(TestResult result) {} + + /** Called on test success */ + void onTestSuccess(TestResult tr) { + allTests.add(tr); + } + + /** Called when the test fails */ + void onTestFailure(TestResult tr) { + allTests.add(tr); + numFailed++; + } + + /** Called in the beginning of test suite. */ + void onStart(TestContext context) { + outputFile = new File(outputFileName); + testContext = context; + } + + void onFinish() { + generateReport(/* coverageReport= */ null); + } + + /** Called after all tests are run */ + void onFinish(CelCoverageIndex.@Nullable CoverageReport coverageReport) { + generateReport(coverageReport); + } + + /** Returns the number of failed tests */ + int getNumFailed() { + return numFailed; + } + + /** + * Generates junit equivalent xml report that sponge/fusion can understand. Called after all tests + * are run + */ + void generateReport(CelCoverageIndex.@Nullable CoverageReport coverageReport) { + try { + DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder(); + Document doc = docBuilder.newDocument(); + + Element rootElement = doc.createElement(XmlConstants.TESTSUITE); + rootElement.setAttribute(XmlConstants.ATTR_NAME, testContext.getSuiteName()); + + rootElement.setAttribute(XmlConstants.ATTR_TESTS, "" + allTests.size()); + rootElement.setAttribute(XmlConstants.ATTR_FAILURES, "" + numFailed); + rootElement.setAttribute(XmlConstants.ATTR_ERRORS, "0"); + + long elapsedTimeMillis = testContext.getEndTime() - testContext.getStartTime(); + + rootElement.setAttribute(XmlConstants.ATTR_TIME, "" + (elapsedTimeMillis / 1000.0)); + + String prevClassName = null; + String currentClassName = null; + Element prevSuite = null; + Element currentSuite = null; + int testsInSuite = 0; + int failedTests = 0; + // NOMUTANTS -- Need not to be fixed. + long startTime = 0; + // NOMUTANTS -- Need not to be fixed. + long endTime = 0; + + // go through each test result + for (TestResult tr : allTests) { + prevClassName = currentClassName; + currentClassName = tr.getTestClassName(); + + // as all results are in single array this will create + // testsuite element as in junit. + if (!currentClassName.equals(prevClassName)) { + prevSuite = currentSuite; + currentSuite = doc.createElement(XmlConstants.TESTSUITE); + rootElement.appendChild(currentSuite); + addCoverageAttributes(currentSuite, coverageReport); + currentSuite.setAttribute(XmlConstants.ATTR_NAME, tr.getTestClassName()); + if (prevSuite != null) { + prevSuite.setAttribute(XmlConstants.ATTR_TESTS, "" + testsInSuite); + prevSuite.setAttribute(XmlConstants.ATTR_FAILURES, "" + failedTests); + prevSuite.setAttribute(XmlConstants.ATTR_ERRORS, "0"); + prevSuite.setAttribute(XmlConstants.ATTR_TIME, "" + (endTime - startTime) / 1000.0); + testsInSuite = 0; + failedTests = 0; + } + startTime = tr.getStartMillis(); + } + endTime = tr.getEndMillis(); + + Element testCaseElement = doc.createElement(XmlConstants.TESTCASE); + elapsedTimeMillis = tr.getEndMillis() - tr.getStartMillis(); + testCaseElement.setAttribute(XmlConstants.ATTR_NAME, tr.getName()); + testCaseElement.setAttribute(XmlConstants.ATTR_CLASSNAME, tr.getTestClassName()); + testCaseElement.setAttribute( + XmlConstants.ATTR_TIME, "" + ((double) elapsedTimeMillis) / 1000); + + // for failure add fail message + if (tr.getStatus() == TestResult.FAILURE) { + failedTests++; + Element nested = doc.createElement(XmlConstants.FAILURE); + testCaseElement.appendChild(nested); + Throwable t = tr.getThrowable(); + if (t != null) { + nested.setAttribute(XmlConstants.ATTR_TYPE, t.getClass().getName()); + String message = t.getMessage(); + if ((message != null) && (message.length() > 0)) { + nested.setAttribute(XmlConstants.ATTR_MESSAGE, message); + } + Text trace = doc.createTextNode(Throwables.getStackTraceAsString(t)); + nested.appendChild(trace); + } + } + currentSuite.appendChild(testCaseElement); + testsInSuite++; + } + + currentSuite.setAttribute(XmlConstants.ATTR_TESTS, "" + testsInSuite); + currentSuite.setAttribute(XmlConstants.ATTR_FAILURES, "" + failedTests); + currentSuite.setAttribute(XmlConstants.ATTR_ERRORS, "0"); + currentSuite.setAttribute(XmlConstants.ATTR_TIME, "" + (endTime - startTime) / 1000.0); + + // Writes to a file + try (BufferedWriter fw = Files.newBufferedWriter(outputFile.toPath(), UTF_8)) { + Transformer transformer = TransformerFactory.newInstance().newTransformer(); + transformer.transform(new DOMSource(rootElement), new StreamResult(fw)); + } catch (TransformerException te) { + te.printStackTrace(); + System.err.println("Error while writing out JUnitXML because of " + te); + } catch (IOException ioe) { + ioe.printStackTrace(); + System.err.println("failed to create JUnitXML because of " + ioe); + } + + } catch (ParserConfigurationException pce) { + pce.printStackTrace(); + System.err.println("failed to create JUnitXML because of " + pce); + } + } + + private void addCoverageAttributes( + Element currentSuite, CelCoverageIndex.@Nullable CoverageReport coverageReport) { + if (coverageReport == null) { + return; + } + if (coverageReport.nodes() == 0) { + currentSuite.setAttribute(XmlConstants.ATTR_CEL_COVERAGE, "No coverage stats found"); + } else { + // CEL expression + currentSuite.setAttribute(XmlConstants.ATTR_CEL_EXPR, coverageReport.celExpression()); + // Node coverage + double nodeCoverage = + (double) coverageReport.coveredNodes() / (double) coverageReport.nodes() * 100.0; + String nodeCoverageString = + String.format( + "%.2f%% (%d out of %d nodes covered)", + nodeCoverage, coverageReport.coveredNodes(), coverageReport.nodes()); + currentSuite.setAttribute(XmlConstants.ATTR_AST_NODE_COVERAGE, nodeCoverageString); + if (!coverageReport.unencounteredNodes().isEmpty()) { + currentSuite.setAttribute( + XmlConstants.ATTR_INTERESTING_UNENCOUNTERED_NODES, + String.join("\n", coverageReport.unencounteredNodes())); + } + // Branch coverage + double branchCoverage = 0.0; + if (coverageReport.branches() > 0) { + branchCoverage = + (double) coverageReport.coveredBooleanOutcomes() + / (double) coverageReport.branches() + * 100.0; + } + String branchCoverageString = + String.format( + "%.2f%% (%d out of %d branch outcomes covered)", + branchCoverage, coverageReport.coveredBooleanOutcomes(), coverageReport.branches()); + currentSuite.setAttribute(XmlConstants.ATTR_AST_BRANCH_COVERAGE, branchCoverageString); + if (!coverageReport.unencounteredBranches().isEmpty()) { + currentSuite.setAttribute( + XmlConstants.ATTR_INTERESTING_UNENCOUNTERED_BRANCH_PATHS, + String.join("\n", coverageReport.unencounteredBranches())); + } + currentSuite.setAttribute( + XmlConstants.ATTR_CEL_TEST_COVERAGE_GRAPH_URL, coverageReport.graphUrl()); + } + } + + /** Description of a test suite execution. */ + static interface TestContext { + String getSuiteName(); + + long getEndTime(); + + long getStartTime(); + } + + /** Description of a single test result. */ + static interface TestResult { + String getTestClassName(); + + String getName(); + + long getStartMillis(); + + long getEndMillis(); + + Throwable getThrowable(); + + int getStatus(); + + public static int FAILURE = 0; + public static int SUCCESS = 1; + } + + /** Elements and attributes for JUnit-style XML doc. */ + private static final class XmlConstants { + static final String TESTSUITE = "testsuite"; + static final String TESTCASE = "testcase"; + static final String FAILURE = "failure"; + static final String ATTR_NAME = "name"; + static final String ATTR_TIME = "time"; + static final String ATTR_ERRORS = "errors"; + static final String ATTR_FAILURES = "failures"; + static final String ATTR_TESTS = "tests"; + static final String ATTR_TYPE = "type"; + static final String ATTR_MESSAGE = "message"; + static final String ATTR_CLASSNAME = "classname"; + // Coverage attributes. + static final String ATTR_CEL_EXPR = "Cel_Expr"; + static final String ATTR_CEL_COVERAGE = "Cel_Coverage"; + static final String ATTR_AST_NODE_COVERAGE = "Ast_Node_Coverage"; + static final String ATTR_INTERESTING_UNENCOUNTERED_NODES = "Interesting_Unencountered_Nodes"; + static final String ATTR_AST_BRANCH_COVERAGE = "Ast_Branch_Coverage"; + static final String ATTR_INTERESTING_UNENCOUNTERED_BRANCH_PATHS = + "Interesting_Unencountered_Branch_Paths"; + static final String ATTR_CEL_TEST_COVERAGE_GRAPH_URL = "Cel_Test_Coverage_Graph_URL"; + } +} diff --git a/testing/src/main/java/dev/cel/testing/testrunner/RegistryUtils.java b/testing/src/main/java/dev/cel/testing/testrunner/RegistryUtils.java new file mode 100644 index 000000000..a10904abb --- /dev/null +++ b/testing/src/main/java/dev/cel/testing/testrunner/RegistryUtils.java @@ -0,0 +1,52 @@ +// 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.testing.testrunner; + + + +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.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(CelDescriptors descriptors) { + return TypeRegistry.newBuilder().add(descriptors.messageTypeDescriptors()).build(); + } + + /** Returns the {@link ExtensionRegistry} for the given file descriptor set. */ + public static ExtensionRegistry getExtensionRegistry(CelDescriptors descriptors) { + ExtensionRegistry extensionRegistry = ExtensionRegistry.newInstance(); + + descriptors + .extensionDescriptors() + .forEach( + (descriptorName, descriptor) -> { + if (descriptor.getType().equals(FieldDescriptor.Type.MESSAGE)) { + Message output = DynamicMessage.getDefaultInstance(descriptor.getMessageType()); + extensionRegistry.add(descriptor, output); + } else { + extensionRegistry.add(descriptor); + } + }); + return extensionRegistry; + } + + private RegistryUtils() {} +} diff --git a/testing/src/main/java/dev/cel/testing/testrunner/ResultMatcher.java b/testing/src/main/java/dev/cel/testing/testrunner/ResultMatcher.java new file mode 100644 index 000000000..927cc3987 --- /dev/null +++ b/testing/src/main/java/dev/cel/testing/testrunner/ResultMatcher.java @@ -0,0 +1,89 @@ +// 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.testing.testrunner; + +import dev.cel.expr.ExprValue; +import com.google.auto.value.AutoOneOf; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import dev.cel.bundle.Cel; +import dev.cel.common.types.CelType; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.testing.testrunner.CelTestSuite.CelTestSection.CelTestCase.Output; +import java.util.Optional; + +/** Custom result matcher for performing assertions on the result of a CEL test case. */ +public interface ResultMatcher { + + /** Parameters for the result matcher. */ + @AutoValue + public abstract class ResultMatcherParams { + public abstract Optional expectedOutput(); + + public abstract ComputedOutput computedOutput(); + + /** Computed output of the CEL test case. */ + @AutoOneOf(ComputedOutput.Kind.class) + public abstract static class ComputedOutput { + /** Kind of the computed output. */ + public enum Kind { + EXPR_VALUE, + ERROR, + UNKNOWN_SET, + } + + public abstract Kind kind(); + + public abstract ExprValue exprValue(); + + public abstract CelEvaluationException error(); + + public abstract ImmutableList unknownSet(); + + public static ComputedOutput ofExprValue(ExprValue exprValue) { + return AutoOneOf_ResultMatcher_ResultMatcherParams_ComputedOutput.exprValue(exprValue); + } + + public static ComputedOutput ofError(CelEvaluationException error) { + return AutoOneOf_ResultMatcher_ResultMatcherParams_ComputedOutput.error(error); + } + + public static ComputedOutput ofUnknownSet(ImmutableList unknownSet) { + return AutoOneOf_ResultMatcher_ResultMatcherParams_ComputedOutput.unknownSet(unknownSet); + } + } + + public abstract CelType resultType(); + + public abstract Builder toBuilder(); + + public static Builder newBuilder() { + return new AutoValue_ResultMatcher_ResultMatcherParams.Builder(); + } + + /** Builder for {@link ResultMatcherParams}. */ + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder setExpectedOutput(Optional result); + + public abstract Builder setResultType(CelType resultType); + + public abstract Builder setComputedOutput(ComputedOutput computedOutput); + + public abstract ResultMatcherParams build(); + } + } + + void match(ResultMatcherParams params, Cel cel) throws Exception; +} diff --git a/testing/src/main/java/dev/cel/testing/testrunner/TestExecutor.java b/testing/src/main/java/dev/cel/testing/testrunner/TestExecutor.java new file mode 100644 index 000000000..181d99c6f --- /dev/null +++ b/testing/src/main/java/dev/cel/testing/testrunner/TestExecutor.java @@ -0,0 +1,302 @@ +// 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.testing.testrunner; + +import static com.google.common.collect.MoreCollectors.onlyElement; +import static dev.cel.testing.utils.ClassLoaderUtils.loadClassesWithMethodAnnotation; +import static dev.cel.testing.utils.ClassLoaderUtils.loadSubclasses; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.time.ZoneId.systemDefault; + +import com.google.common.io.Files; +import dev.cel.testing.testrunner.Annotations.TestSuiteSupplier; +import dev.cel.testing.testrunner.CelTestSuite.CelTestSection; +import dev.cel.testing.testrunner.CelTestSuite.CelTestSection.CelTestCase; +import io.github.classgraph.ClassInfoList; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Method; +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.Arrays; +import org.junit.runner.Description; +import org.junit.runner.JUnitCore; +import org.junit.runner.Request; +import org.junit.runner.Result; +import org.junit.runner.Runner; +import org.junit.runner.manipulation.Filter; +import org.junit.runners.model.TestClass; +import org.junit.runners.parameterized.BlockJUnit4ClassRunnerWithParametersFactory; +import org.junit.runners.parameterized.ParametersRunnerFactory; +import org.junit.runners.parameterized.TestWithParameters; + +/** Test executor for running tests using custom runner. */ +public final class TestExecutor { + + private TestExecutor() {} + + private static final Class CEL_TESTSUITE_ANNOTATION_CLASS = TestSuiteSupplier.class; + + private static CelTestSuite readTestSuite(String testSuitePath) + throws IOException, CelTestSuiteException { + switch (testSuitePath.substring(testSuitePath.lastIndexOf(".") + 1)) { + case "textproto": + return CelTestSuiteTextProtoParser.newInstance() + .parse(Files.asCharSource(new File(testSuitePath), UTF_8).read()); + case "yaml": + return CelTestSuiteYamlParser.newInstance() + .parse(Files.asCharSource(new File(testSuitePath), UTF_8).read()); + default: + throw new IllegalArgumentException( + "Unsupported test suite file type: " + testSuitePath); + } + } + + private static class TestContext implements JUnitXmlReporter.TestContext { + + final LocalDate startDate; + LocalDate endDate; + + TestContext() { + startDate = Instant.now().atZone(systemDefault()).toLocalDate(); + } + + @Override + public long getEndTime() { + return endDate.atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli(); + } + + @Override + public long getStartTime() { + return startDate.atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli(); + } + + @Override + public String getSuiteName() { + return "test_suite"; + } + + void done() { + endDate = Instant.now().atZone(systemDefault()).toLocalDate(); + } + } + + private static class TestResult implements JUnitXmlReporter.TestResult { + + long endMillis; + + long startMillis; + + int status; + + final String testName; + + Throwable throwable; + + final String testClassName; + + TestResult(String testName, String testClassName) { + this.testName = testName; + this.startMillis = Instant.now().toEpochMilli(); + this.testClassName = testClassName; + } + + @Override + public String getTestClassName() { + return testClassName; + } + + @Override + public long getEndMillis() { + return endMillis; + } + + @Override + public String getName() { + return testName; + } + + @Override + public long getStartMillis() { + return startMillis; + } + + @Override + public int getStatus() { + return status; + } + + @Override + public Throwable getThrowable() { + return throwable; + } + + private void setEndMillis(long endMillis) { + this.endMillis = endMillis; + } + + private void setStatus(int status) { + this.status = status; + } + + private void setThrowable(Throwable throwable) { + this.throwable = throwable; + } + + private void setStartMillis(long startMillis) { + this.startMillis = startMillis; + } + } + + /** Runs test cases for a given test class and test suite. */ + public static void runTests() throws Exception { + String testSuitePath = System.getProperty("test_suite_path"); + + CelTestSuite testSuite; + testSuite = readCustomTestSuite(); + + if (testSuitePath != null) { + if (testSuite != null) { + throw new IllegalArgumentException( + "Both test_suite_path and TestSuiteSupplier are set. Only one of them can be set."); + } else { + testSuite = readTestSuite(testSuitePath); + } + } else if (testSuite == null) { + throw new IllegalArgumentException("Neither test_suite_path nor TestSuiteSupplier is set."); + } + + Class testClass = getUserTestClass(); + String envXmlFile = System.getenv("XML_OUTPUT_FILE"); + JUnitXmlReporter testReporter = new JUnitXmlReporter(envXmlFile); + TestContext testContext = new TestContext(); + testReporter.onStart(testContext); + + boolean allTestsPassed = true; + CelCoverageIndex celCoverageIndex = null; + // If coverage is enabled, the celCoverageIndex parameter will be added in the test class. + // This will be used to run the test case multiple times with different inputs and collect + // the coverage data. + String isCoverageEnabled = System.getProperty("is_coverage_enabled"); + if (isCoverageEnabled != null && Boolean.parseBoolean(isCoverageEnabled)) { + celCoverageIndex = new CelCoverageIndex(); + } + + for (CelTestSection testSection : testSuite.sections()) { + for (CelTestCase testCase : testSection.tests()) { + String testName = testSection.name() + "." + testCase.name(); + ArrayList parameter = new ArrayList<>(Arrays.asList(testCase, celCoverageIndex)); + TestWithParameters test = + new TestWithParameters(testName, new TestClass(testClass), parameter); + + TestResult testResult = new TestResult(testName, testClass.getName()); + testReporter.onTestStart(testResult); + testResult.setStartMillis(Instant.now().toEpochMilli()); + + ParametersRunnerFactory factory = new BlockJUnit4ClassRunnerWithParametersFactory(); + Runner runner = factory.createRunnerForTestWithParameters(test); + + JUnitCore junitCore = new JUnitCore(); + Request request = + Request.runner(runner) + .filterWith( + new Filter() { + @Override + public boolean shouldRun(Description description) { + return true; + } + + @Override + public String describe() { + return "Filter to run only test method"; + } + }); + Result result = junitCore.run(request); + testResult.setEndMillis(Instant.now().toEpochMilli()); + + if (result.wasSuccessful()) { + testResult.setStatus(JUnitXmlReporter.TestResult.SUCCESS); + testReporter.onTestSuccess(testResult); + } else { + allTestsPassed = false; + 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()); + } + } + } + + testContext.done(); + if (celCoverageIndex != null) { + CelCoverageIndex.CoverageReport report = celCoverageIndex.generateCoverageReport(); + testReporter.onFinish(report); + } else { + testReporter.onFinish(); + } + if (!allTestsPassed) { + throw new RuntimeException(testReporter.getNumFailed() + " tests failed"); + } + } + + private static CelTestSuite readCustomTestSuite() throws Exception { + ClassInfoList classInfoList = + loadClassesWithMethodAnnotation(CEL_TESTSUITE_ANNOTATION_CLASS.getName()); + if (classInfoList.isEmpty()) { + return null; + } + if (classInfoList.size() > 1) { + throw new IllegalArgumentException( + "Expected 1 class for TestSuiteSupplier, but got " + + classInfoList.size() + + " classes: " + + classInfoList); + } + Class customFunctionClass = classInfoList.loadClasses().get(0); + Method method = getMethodWithAnnotation(customFunctionClass); + return (CelTestSuite) method.invoke(customFunctionClass.getDeclaredConstructor().newInstance()); + } + + private static Method getMethodWithAnnotation(Class clazz) { + Method testSuiteSupplierMethod = + Arrays.asList(clazz.getDeclaredMethods()).stream() + .filter(method -> method.isAnnotationPresent(TestSuiteSupplier.class)) + .collect(onlyElement()); + + if (!testSuiteSupplierMethod.getReturnType().equals(CelTestSuite.class)) { + throw new IllegalArgumentException( + String.format( + "Method: %s annotated with @TestSuiteSupplier must return CelTestSuite, but got %s", + testSuiteSupplierMethod.getName(), testSuiteSupplierMethod.getReturnType())); + } + return testSuiteSupplierMethod; + } + + private static Class getUserTestClass() { + ClassInfoList subClassInfoList = loadSubclasses(CelUserTestTemplate.class); + if (subClassInfoList.size() != 1) { + throw new IllegalArgumentException( + "Expected 1 subclass for CelUserTestTemplate, but got " + + subClassInfoList.size() + + " subclasses: " + + subClassInfoList); + } + + return subClassInfoList.loadClasses().get(0); + } +} diff --git a/testing/src/main/java/dev/cel/testing/testrunner/TestRunnerBinary.java b/testing/src/main/java/dev/cel/testing/testrunner/TestRunnerBinary.java new file mode 100644 index 000000000..220609ae8 --- /dev/null +++ b/testing/src/main/java/dev/cel/testing/testrunner/TestRunnerBinary.java @@ -0,0 +1,26 @@ +// 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.testing.testrunner; + +/** Main class for the CEL test runner. */ +public final class TestRunnerBinary { + + private TestRunnerBinary() {} + + public static void main(String[] args) throws Exception { + // NOMUTANTS -- To be fixed in b/394771693. Since no assertions result in false positive. + TestExecutor.runTests(); + } +} diff --git a/testing/src/main/java/dev/cel/testing/testrunner/TestRunnerLibrary.java b/testing/src/main/java/dev/cel/testing/testrunner/TestRunnerLibrary.java new file mode 100644 index 000000000..1d3e49fbe --- /dev/null +++ b/testing/src/main/java/dev/cel/testing/testrunner/TestRunnerLibrary.java @@ -0,0 +1,447 @@ +// 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.testing.testrunner; + +import static com.google.common.io.Files.asCharSource; +import static dev.cel.testing.utils.ExprValueUtils.fromValue; +import static dev.cel.testing.utils.ExprValueUtils.toExprValue; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.nio.file.Files.readAllBytes; + +import dev.cel.expr.CheckedExpr; +import dev.cel.expr.ExprValue; +import dev.cel.expr.Value; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; +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.policy.CelPolicy; +import dev.cel.policy.CelPolicyCompilerFactory; +import dev.cel.policy.CelPolicyParser; +import dev.cel.policy.CelPolicyParserFactory; +import dev.cel.policy.CelPolicyValidationException; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.CelRuntime.Program; +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 java.io.File; +import java.io.IOException; +import java.nio.file.Paths; +import java.util.Map; +import java.util.Optional; +import java.util.logging.Logger; +import org.jspecify.annotations.Nullable; + +/** Runner library for creating the environment and running the assertions. */ +public final class TestRunnerLibrary { + TestRunnerLibrary() {} + + private static final Logger logger = Logger.getLogger(TestRunnerLibrary.class.getName()); + + /** + * Run the assertions for a given raw/checked expression test case. + * + * @param testCase The test case to run. + * @param celTestContext The test context containing the {@link Cel} bundle and other + * configurations. + */ + public static void runTest(CelTestCase testCase, CelTestContext celTestContext) throws Exception { + if (celTestContext.celExpression().isPresent()) { + evaluateTestCase(testCase, celTestContext); + } else { + throw new IllegalArgumentException("No cel expression provided."); + } + } + + /** + * Run the assertions for a given raw/checked expression test case with coverage enabled. + * + *

This method is used for generating coverage data. It will be used to run the test case + * multiple times with different inputs and collect the coverage data. + * + * @param testCase The test case to run. + * @param celTestContext The test context containing the {@link Cel} bundle and other + * configurations. + * @param celCoverageIndex The coverage index to use for the test case. + */ + public static void runTest( + CelTestCase testCase, + CelTestContext celTestContext, + @Nullable CelCoverageIndex celCoverageIndex) + throws Exception { + if (celTestContext.celExpression().isPresent()) { + evaluateTestCase(testCase, celTestContext, celCoverageIndex); + } else { + throw new IllegalArgumentException("No cel expression provided."); + } + } + + /** 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 { + evaluateTestCase(testCase, celTestContext, /* celCoverageIndex= */ null); + } + + static void evaluateTestCase( + CelTestCase testCase, + CelTestContext celTestContext, + @Nullable CelCoverageIndex celCoverageIndex) + throws Exception { + celTestContext = extendCelTestContext(celTestContext); + CelAbstractSyntaxTree ast; + CelExpressionSource celExpressionSource = celTestContext.celExpression().get(); + switch (celExpressionSource.type()) { + case POLICY: + ast = + compilePolicy( + celTestContext.cel(), + celTestContext.celPolicyParser().get(), + readFile(celExpressionSource.value())); + break; + case TEXTPROTO: + case BINARYPB: + ast = readAstFromCheckedExpression(celExpressionSource); + break; + case CEL: + ast = celTestContext.cel().compile(readFile(celExpressionSource.value())).getAst(); + break; + case RAW_EXPR: + ast = celTestContext.cel().compile(celExpressionSource.value()).getAst(); + break; + default: + throw new IllegalArgumentException( + "Unsupported expression type: " + celExpressionSource.type()); + } + if (celCoverageIndex != null) { + celCoverageIndex.init(ast); + } + evaluate(ast, testCase, celTestContext, celCoverageIndex); + } + + private static CelAbstractSyntaxTree readAstFromCheckedExpression( + CelExpressionSource celExpressionSource) throws IOException { + switch (celExpressionSource.type()) { + case BINARYPB: + byte[] bytes = readAllBytes(Paths.get(celExpressionSource.value())); + CheckedExpr checkedExpr = + CheckedExpr.parseFrom(bytes, ExtensionRegistry.getEmptyRegistry()); + return CelProtoAbstractSyntaxTree.fromCheckedExpr(checkedExpr).getAst(); + case TEXTPROTO: + String content = readFile(celExpressionSource.value()); + CheckedExpr.Builder builder = CheckedExpr.newBuilder(); + TextFormat.merge(content, builder); + return CelProtoAbstractSyntaxTree.fromCheckedExpr(builder.build()).getAst(); + default: + throw new IllegalArgumentException( + "Unsupported expression type: " + celExpressionSource.type()); + } + } + + private static CelTestContext extendCelTestContext(CelTestContext celTestContext) + throws Exception { + CelOptions celOptions = celTestContext.celOptions(); + CelTestContext.Builder celTestContextBuilder = + celTestContext.toBuilder().setCel(extendCel(celTestContext, celOptions)); + if (celTestContext + .celExpression() + .get() + .type() + .equals(CelExpressionSource.ExpressionSourceType.POLICY)) { + celTestContextBuilder.setCelPolicyParser( + celTestContext + .celPolicyParser() + .orElse(CelPolicyParserFactory.newYamlParserBuilder().build())); + } + + return celTestContextBuilder.build(); + } + + private static Cel extendCel(CelTestContext celTestContext, CelOptions celOptions) + throws Exception { + Cel extendedCel = celTestContext.cel(); + + // Add the file descriptor set to the cel object if provided. + // + // 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. + 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( + CelDescriptorUtil.getAllDescriptorsFromFileDescriptor(celTestContext.fileTypes()) + .messageTypeDescriptors()) + .build(); + } + + CelEnvironment environment = CelEnvironment.newBuilder().build(); + + // Extend the cel object with the config file if provided. + if (celTestContext.configFile().isPresent()) { + String configContent = readFile(celTestContext.configFile().get()); + environment = CelEnvironmentYamlParser.newInstance().parse(configContent); + } + + // Policy compiler requires optional support. Add the optional library by default to the + // environment. + return environment.toBuilder() + .addExtensions(ExtensionConfig.of("optional")) + .build() + .extend(extendedCel, celOptions); + } + + private static String readFile(String path) throws IOException { + return asCharSource(new File(path), UTF_8).read(); + } + + private static CelAbstractSyntaxTree compilePolicy( + Cel cel, CelPolicyParser celPolicyParser, String policyContent) + throws CelPolicyValidationException { + CelPolicy celPolicy = celPolicyParser.parse(policyContent); + return CelPolicyCompilerFactory.newPolicyCompiler(cel).build().compile(celPolicy); + } + + private static void evaluate( + CelAbstractSyntaxTree ast, + CelTestCase testCase, + CelTestContext celTestContext, + @Nullable CelCoverageIndex celCoverageIndex) + throws Exception { + Cel cel = celTestContext.cel(); + Program program = cel.createProgram(ast); + ExprValue exprValue = null; + CelEvaluationException error = null; + Object evaluationResult = null; + try { + evaluationResult = getEvaluationResult(testCase, celTestContext, program, celCoverageIndex); + exprValue = toExprValue(evaluationResult, ast.getResultType()); + } catch (CelEvaluationException e) { + String errorMessage = + String.format( + "Evaluation failed for test case: %s. Error: %s", testCase.name(), e.getMessage()); + error = new CelEvaluationException(errorMessage, e); + logger.severe(e.toString()); + } + + // Perform the assertion on the result of the evaluation. + ResultMatcherParams.Builder paramsBuilder = + ResultMatcherParams.newBuilder() + .setExpectedOutput(Optional.ofNullable(testCase.output())) + .setResultType(ast.getResultType()); + + if (error != null) { + paramsBuilder.setComputedOutput(ResultMatcherParams.ComputedOutput.ofError(error)); + } else { + switch (exprValue.getKindCase()) { + case VALUE: + paramsBuilder.setComputedOutput( + ResultMatcherParams.ComputedOutput.ofExprValue(exprValue)); + break; + case UNKNOWN: + paramsBuilder.setComputedOutput( + ResultMatcherParams.ComputedOutput.ofUnknownSet( + ImmutableList.copyOf(exprValue.getUnknown().getExprsList()))); + break; + default: + throw new IllegalArgumentException( + String.format("Unexpected result type: %s", exprValue.getKindCase())); + } + } + + celTestContext.resultMatcher().match(paramsBuilder.build(), cel); + } + + private static Object getEvaluationResult( + CelTestCase testCase, + CelTestContext celTestContext, + Program program, + @Nullable CelCoverageIndex celCoverageIndex) + throws CelEvaluationException, IOException, CelValidationException { + if (celTestContext.celLateFunctionBindings().isPresent()) { + return program.eval( + getBindings(testCase, celTestContext), celTestContext.celLateFunctionBindings().get()); + } + switch (testCase.input().kind()) { + case CONTEXT_MESSAGE: + return getEvaluationResultWithMessage( + unpackAny(testCase.input().contextMessage(), celTestContext), + program, + celCoverageIndex); + case CONTEXT_EXPR: + return getEvaluationResultWithMessage( + getEvaluatedContextExpr(testCase, celTestContext), program, celCoverageIndex); + case BINDINGS: + 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()) { + if (entry.getValue() instanceof Any) { + newBindings.put(entry.getKey(), unpackAny((Any) entry.getValue(), celTestContext)); + } else { + newBindings.put(entry); + } + } + return getEvaluationResultWithBindings( + newBindings.buildOrThrow(), program, celCoverageIndex); + } + throw new IllegalArgumentException("Unexpected input type: " + testCase.input().kind()); + } + + private static Object getEvaluationResultWithBindings( + Map bindings, Program program, @Nullable CelCoverageIndex celCoverageIndex) + throws CelEvaluationException { + if (celCoverageIndex != null) { + return program.trace(bindings, celCoverageIndex.newEvaluationListener()); + } + return program.eval(bindings); + } + + private static Object getEvaluationResultWithMessage( + Message message, Program program, @Nullable CelCoverageIndex celCoverageIndex) + throws CelEvaluationException { + if (celCoverageIndex != null) { + return program.trace(message, celCoverageIndex.newEvaluationListener()); + } + return program.eval(message); + } + + private static Message unpackAny(Any any, CelTestContext celTestContext) throws IOException { + 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()); + } + + 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( + CelTestCase testCase, CelTestContext celTestContext) + throws CelEvaluationException, CelValidationException { + try { + return (Message) evaluateInput(celTestContext.cel(), testCase.input().contextExpr()); + } catch (ClassCastException e) { + throw new IllegalArgumentException("Context expression must evaluate to a proto message.", e); + } + } + + private static ImmutableMap getBindings( + CelTestCase testCase, CelTestContext celTestContext) + throws IOException, CelEvaluationException, CelValidationException { + Cel cel = celTestContext.cel(); + ImmutableMap.Builder inputBuilder = ImmutableMap.builder(); + for (Map.Entry entry : testCase.input().bindings().entrySet()) { + if (entry.getValue().kind().equals(Binding.Kind.EXPR)) { + inputBuilder.put(entry.getKey(), evaluateInput(cel, entry.getValue().expr())); + } else { + inputBuilder.put( + entry.getKey(), getValueFromBinding(entry.getValue().value(), celTestContext)); + } + } + return inputBuilder.buildOrThrow(); + } + + private static Object evaluateInput(Cel cel, String expr) + throws CelEvaluationException, CelValidationException { + CelAbstractSyntaxTree exprInputAst = cel.compile(expr).getAst(); + return cel.createProgram(exprInputAst).eval(); + } + + private static Object getValueFromBinding(Object value, CelTestContext celTestContext) + throws IOException { + if (value instanceof Value) { + 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, 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 new file mode 100644 index 000000000..eea56752d --- /dev/null +++ b/testing/src/main/java/dev/cel/testing/utils/BUILD.bazel @@ -0,0 +1,54 @@ +load("@rules_java//java:java_library.bzl", "java_library") + +package( + default_applicable_licenses = ["//:license"], + default_testonly = True, + default_visibility = [ + "//testing:__pkg__", + "//testing/testrunner:__pkg__", + ], +) + +java_library( + name = "expr_value_utils", + srcs = ["ExprValueUtils.java"], + tags = [ + ], + deps = [ + "//common:cel_descriptors", + "//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", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + "@maven_android//:com_google_protobuf_protobuf_javalite", + ], +) + +java_library( + name = "class_loader_utils", + srcs = ["ClassLoaderUtils.java"], + tags = [ + ], + deps = [ + "@maven//:com_google_guava_guava", + "@maven//:io_github_classgraph_classgraph", + ], +) + +java_library( + name = "proto_descriptor_utils", + srcs = ["ProtoDescriptorUtils.java"], + deps = [ + "//common:cel_descriptor_util", + "//common:cel_descriptors", + "@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 new file mode 100644 index 000000000..31f45d48f --- /dev/null +++ b/testing/src/main/java/dev/cel/testing/utils/ClassLoaderUtils.java @@ -0,0 +1,52 @@ +// 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.testing.utils; + +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ClassInfo; +import io.github.classgraph.ClassInfoList; +import io.github.classgraph.ScanResult; + +/** Utility class for loading classes using {@link ClassGraph}. */ +public final class ClassLoaderUtils { + + // 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 subclasses of the given class from the JVM. + * + * @param clazz The class to load subclasses for. + * @return A list of {@link ClassInfo} objects representing the subclasses. + */ + public static ClassInfoList loadSubclasses(Class clazz) { + return CLASS_SCAN_RESULT.get().getSubclasses(clazz.getName()); + } + + /** + * Loads all classes with the given method annotation from the JVM. + * + * @param annotationName The name of the annotation to load classes with. + * @return A list of {@link ClassInfo} objects representing the classes with the annotation. + */ + public static ClassInfoList loadClassesWithMethodAnnotation(String annotationName) { + return CLASS_SCAN_RESULT.get().getClassesWithMethodAnnotation(annotationName); + } + + private ClassLoaderUtils() {} +} diff --git a/testing/src/main/java/dev/cel/testing/utils/ExprValueUtils.java b/testing/src/main/java/dev/cel/testing/utils/ExprValueUtils.java new file mode 100644 index 000000000..10ab52786 --- /dev/null +++ b/testing/src/main/java/dev/cel/testing/utils/ExprValueUtils.java @@ -0,0 +1,300 @@ +// 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.testing.utils; + +import dev.cel.expr.ExprValue; +import dev.cel.expr.ListValue; +import dev.cel.expr.MapValue; +import dev.cel.expr.UnknownSet; +import dev.cel.expr.Value; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.primitives.UnsignedLong; +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.CelDescriptors; +import dev.cel.common.internal.ProtoTimeUtils; +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.TypeType; +import dev.cel.common.values.CelByteString; +import dev.cel.runtime.CelUnknownSet; +import dev.cel.testing.testrunner.RegistryUtils; +import java.io.IOException; +import java.time.Duration; +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** Utility class for ExprValue and Value type conversions during test execution. */ +@SuppressWarnings({"UnnecessarilyFullyQualified"}) +public final class ExprValueUtils { + + private ExprValueUtils() {} + + /** + * Converts a {@link Value} to a Java native object using the given file descriptor set to parse + * `Any` messages. + * + * @param value The {@link Value} to convert. + * @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 { + return fromValue(value, ProtoDescriptorUtils.getDescriptorsFromFile(fileDescriptorSetPath)); + } + + /** + * Converts a {@link Value} to a Java native object using custom registries. + * + * @param value The {@link Value} to convert. + * @param typeRegistry The type registry to use for object resolution. + * @param extensionRegistry The extension registry to use for object resolution. + * @return The converted Java object. + * @throws IOException If there's an error during conversion. + */ + public static Object fromValue( + Value value, TypeRegistry typeRegistry, ExtensionRegistry extensionRegistry) + throws IOException { + if (value.getKindCase().equals(Value.KindCase.OBJECT_VALUE)) { + Descriptor descriptor = + 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(), extensionRegistry); + } + return toNativeObject(value, typeRegistry, extensionRegistry); + } + + 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; + case BOOL_VALUE: + return value.getBoolValue(); + case INT64_VALUE: + return value.getInt64Value(); + case UINT64_VALUE: + return UnsignedLong.fromLongBits(value.getUint64Value()); + case DOUBLE_VALUE: + return value.getDoubleValue(); + case STRING_VALUE: + return value.getStringValue(); + case BYTES_VALUE: + ByteString byteString = value.getBytesValue(); + return CelByteString.of(byteString.toByteArray()); + case ENUM_VALUE: + return value.getEnumValue(); + case MAP_VALUE: + { + MapValue map = value.getMapValue(); + ImmutableMap.Builder builder = + ImmutableMap.builderWithExpectedSize(map.getEntriesCount()); + for (MapValue.Entry entry : map.getEntriesList()) { + builder.put( + fromValue(entry.getKey(), typeRegistry, extensionRegistry), + fromValue(entry.getValue(), typeRegistry, extensionRegistry)); + } + return builder.buildOrThrow(); + } + case LIST_VALUE: + { + ListValue list = value.getListValue(); + ImmutableList.Builder builder = + ImmutableList.builderWithExpectedSize(list.getValuesCount()); + for (Value element : list.getValuesList()) { + builder.add(fromValue(element, typeRegistry, extensionRegistry)); + } + return builder.build(); + } + case TYPE_VALUE: + return value.getTypeValue(); + default: + throw new IllegalArgumentException( + String.format("Unexpected binding value kind: %s", value.getKindCase())); + } + } + + /** + * Converts a Java object to an {@link ExprValue}. + * + * @param object The Java object to convert. + * @param type The {@link CelType} of the object. + * @return The converted {@link ExprValue}. + * @throws Exception If there's an error during conversion. + */ + public static ExprValue toExprValue(Object object, CelType type) throws Exception { + if (object instanceof ExprValue) { + return (ExprValue) object; + } + if (object instanceof CelUnknownSet) { + return ExprValue.newBuilder().setUnknown(toUnknownSet((CelUnknownSet) object)).build(); + } + return ExprValue.newBuilder().setValue(toValue(object, type)).build(); + } + + public static UnknownSet toUnknownSet(CelUnknownSet unknownSet) { + return UnknownSet.newBuilder().addAllExprs(unknownSet.unknownExprIds()).build(); + } + + /** + * Converts a Java object to an {@link Value}. + * + * @param object The Java object to convert. + * @param type The {@link CelType} of the object. + * @return The converted {@link Value}. + * @throws Exception If there's an error during conversion. + */ + @SuppressWarnings("unchecked") + public static Value toValue(Object object, CelType type) throws Exception { + if (!(object instanceof Optional) && type instanceof OptionalType) { + return toValue(object, type.parameters().get(0)); + } + if (object == null || object.equals(NullValue.NULL_VALUE)) { + object = dev.cel.common.values.NullValue.NULL_VALUE; + } + if (object instanceof dev.cel.expr.Value) { + object = + Value.parseFrom( + ((dev.cel.expr.Value) object).toByteArray(), + ExtensionRegistry.getEmptyRegistry()); + } + if (object instanceof Value) { + return (Value) object; + } + if (object instanceof dev.cel.common.values.NullValue) { + return Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build(); + } + if (object instanceof Boolean) { + return Value.newBuilder().setBoolValue((Boolean) object).build(); + } + if (object instanceof UnsignedLong) { + switch (type.kind()) { + case UINT: + case DYN: + case ANY: + return Value.newBuilder().setUint64Value(((UnsignedLong) object).longValue()).build(); + default: + throw new IllegalArgumentException(String.format("Unexpected result type: %s", type)); + } + } + if (object instanceof Long) { + switch (type.kind()) { + case INT: + case DYN: + case ANY: + return Value.newBuilder().setInt64Value((Long) object).build(); + case UINT: + return Value.newBuilder().setUint64Value((Long) object).build(); + default: + throw new IllegalArgumentException(String.format("Unexpected result type: %s", type)); + } + } + if (object instanceof Double) { + return Value.newBuilder().setDoubleValue((Double) object).build(); + } + if (object instanceof String) { + switch (type.kind()) { + case TYPE: + return Value.newBuilder().setTypeValue((String) object).build(); + case STRING: + case DYN: + case ANY: + return Value.newBuilder().setStringValue((String) object).build(); + default: + throw new IllegalArgumentException(String.format("Unexpected result type: %s", type)); + } + } + if (object instanceof CelByteString) { + return Value.newBuilder() + .setBytesValue(ByteString.copyFrom(((CelByteString) object).toByteArray())) + .build(); + } + if (object instanceof List) { + CelType elemType = type instanceof ListType ? ((ListType) type).elemType() : SimpleType.DYN; + ListValue.Builder builder = ListValue.newBuilder(); + for (Object element : ((List) object)) { + builder.addValues(toValue(element, elemType)); + } + return Value.newBuilder().setListValue(builder.build()).build(); + } + if (object instanceof Map) { + CelType keyType = type instanceof MapType ? ((MapType) type).keyType() : SimpleType.DYN; + CelType valueType = type instanceof MapType ? ((MapType) type).valueType() : SimpleType.DYN; + MapValue.Builder builder = MapValue.newBuilder(); + for (Map.Entry entry : ((Map) object).entrySet()) { + builder.addEntries( + MapValue.Entry.newBuilder() + .setKey(toValue(entry.getKey(), keyType)) + .setValue(toValue(entry.getValue(), valueType)) + .build()); + } + return Value.newBuilder().setMapValue(builder.build()).build(); + } + + if (object instanceof Instant) { + return Value.newBuilder() + .setObjectValue(Any.pack(ProtoTimeUtils.toProtoTimestamp((Instant) object))) + .build(); + } + + if (object instanceof Duration) { + return Value.newBuilder() + .setObjectValue(Any.pack(ProtoTimeUtils.toProtoDuration((Duration) object))) + .build(); + } + + if (object instanceof Message) { + return Value.newBuilder().setObjectValue(Any.pack((Message) object)).build(); + } + if (object instanceof TypeType) { + return Value.newBuilder().setTypeValue(((TypeType) object).containingTypeName()).build(); + } + + if (object instanceof Optional) { + // TODO: Remove this once the ExprValue Native representation is added. + if (!((Optional) object).isPresent()) { + return Value.getDefaultInstance(); + } + return toValue(((Optional) object).get(), type.parameters().get(0)); + } + + throw new IllegalArgumentException( + String.format("Unexpected result type: %s", object.getClass())); + } +} diff --git a/testing/src/main/java/dev/cel/testing/utils/ProtoDescriptorUtils.java b/testing/src/main/java/dev/cel/testing/utils/ProtoDescriptorUtils.java new file mode 100644 index 000000000..880c03e12 --- /dev/null +++ b/testing/src/main/java/dev/cel/testing/utils/ProtoDescriptorUtils.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.testing.utils; + +import com.google.common.io.Files; +import com.google.protobuf.DescriptorProtos.FileDescriptorSet; +import com.google.protobuf.ExtensionRegistry; +import dev.cel.common.CelDescriptorUtil; +import dev.cel.common.CelDescriptors; +import java.io.File; +import java.io.IOException; + +/** Utility class for working with proto descriptors. */ +public final class ProtoDescriptorUtils { + + /** + * Returns all the descriptors from the file descriptor set file. + * + * @return The {@link CelDescriptors} object containing all the descriptors. + */ + public static CelDescriptors getDescriptorsFromFile(String fileDescriptorSetPath) + throws IOException { + FileDescriptorSet fileDescriptorSet = getFileDescriptorSet(fileDescriptorSetPath); + return CelDescriptorUtil.getAllDescriptorsFromFileDescriptor( + CelDescriptorUtil.getFileDescriptorsFromFileDescriptorSet(fileDescriptorSet)); + } + + private static FileDescriptorSet getFileDescriptorSet(String fileDescriptorSetPath) + throws IOException { + // We can pass an empty extension registry here because extensions are recovered later when + // creating the extension registry in {@link #createExtensionRegistry}. + return FileDescriptorSet.parseFrom( + Files.toByteArray(new File(fileDescriptorSetPath)), ExtensionRegistry.newInstance()); + } + + private ProtoDescriptorUtils() {} +} diff --git a/testing/src/test/java/dev/cel/testing/BUILD.bazel b/testing/src/test/java/dev/cel/testing/BUILD.bazel index 4d4a12ec4..8fe831bb2 100644 --- a/testing/src/test/java/dev/cel/testing/BUILD.bazel +++ b/testing/src/test/java/dev/cel/testing/BUILD.bazel @@ -1,3 +1,4 @@ +load("@rules_java//java:defs.bzl", "java_library") load("//:testing.bzl", "junit4_test_suites") package(default_applicable_licenses = [ @@ -10,20 +11,8 @@ java_library( srcs = glob(["*.java"]), deps = [ "//:java_truth", - "//common", - "//common:options", - "//common/types", - "//common/types:type_providers", - "//compiler", - "//compiler:compiler_builder", - "//parser:macro", - "//runtime:base", - "//runtime:interpreter", "//testing:line_differ", - "//testing:sync", - "@maven//:com_google_api_grpc_proto_google_common_protos", "@maven//:com_google_guava_guava", - "@maven//:com_google_protobuf_protobuf_java", "@maven//:junit_junit", ], ) diff --git a/testing/src/test/java/dev/cel/testing/EvalSyncTest.java b/testing/src/test/java/dev/cel/testing/EvalSyncTest.java deleted file mode 100644 index e516d9780..000000000 --- a/testing/src/test/java/dev/cel/testing/EvalSyncTest.java +++ /dev/null @@ -1,184 +0,0 @@ -// 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.testing; - -import static com.google.common.truth.Truth.assertThat; - -import com.google.common.collect.ImmutableList; -import com.google.protobuf.Any; -import com.google.protobuf.BoolValue; -import com.google.protobuf.ByteString; -import com.google.protobuf.BytesValue; -import com.google.protobuf.Descriptors.FileDescriptor; -import com.google.protobuf.DoubleValue; -import com.google.protobuf.FloatValue; -import com.google.protobuf.Int32Value; -import com.google.protobuf.Int64Value; -import com.google.protobuf.Message; -import com.google.protobuf.StringValue; -import com.google.protobuf.UInt32Value; -import com.google.protobuf.UInt64Value; -import com.google.type.Expr; -import dev.cel.common.CelAbstractSyntaxTree; -import dev.cel.common.CelOptions; -import dev.cel.common.types.CelType; -import dev.cel.common.types.SimpleType; -import dev.cel.compiler.CelCompiler; -import dev.cel.compiler.CelCompilerFactory; -import dev.cel.parser.CelStandardMacro; -import dev.cel.runtime.Activation; -import dev.cel.runtime.InterpreterException; -import java.util.Arrays; -import java.util.List; -import org.junit.Test; -import org.junit.experimental.runners.Enclosed; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; - -@RunWith(Enclosed.class) -public final class EvalSyncTest { - private static final ImmutableList TEST_FILE_DESCRIPTOR = - ImmutableList.of(Expr.getDescriptor().getFile()); - - private static final CelOptions TEST_OPTIONS = CelOptions.current().build(); - - private static final EvalSync EVAL = new EvalSync(TEST_FILE_DESCRIPTOR, TEST_OPTIONS); - - @RunWith(JUnit4.class) - public static class EvalSyncApiTests { - @Test - public void fileDescriptorsTest() { - assertThat(EVAL.fileDescriptors()).isEqualTo(TEST_FILE_DESCRIPTOR); - } - } - - @RunWith(Parameterized.class) - public static class ProtoTypeAdapterTests { - - @Parameters - public static List data() { - return Arrays.asList( - new Object[][] { - {BoolValue.of(true), true}, - {BoolValue.of(false), false}, - {DoubleValue.of(1.5D), 1.5D}, - {FloatValue.of(1.5f), 1.5D}, - {StringValue.of("test"), "test"}, - {Int32Value.of(1), 1L}, - {Int64Value.of(1), 1L}, - {UInt32Value.of(1), 1L}, - {UInt64Value.of(1), 1L}, - {BytesValue.of(ByteString.copyFromUtf8("test")), ByteString.copyFromUtf8("test")}, - }); - } - - private final Message protoMessage; - private final Object nativeValue; - - public ProtoTypeAdapterTests(Message protoMessage, Object nativeValue) { - this.protoMessage = protoMessage; - this.nativeValue = nativeValue; - } - - @Test - public void protoMessageAdapt_convertsToNativeValues() throws InterpreterException { - assertThat(EVAL.adapt(protoMessage)).isEqualTo(nativeValue); - assertThat(EVAL.adapt(Any.pack(protoMessage))).isEqualTo(nativeValue); - } - - @Test - public void nativeValueAdapt_doesNothing() throws InterpreterException { - assertThat(EVAL.adapt(nativeValue)).isEqualTo(nativeValue); - } - } - - /** - * Test cases to show that basic evaluation is working as intended. A comprehensive set of tests - * can be found in {@code BaseInterpreterTest}. - */ - @RunWith(Parameterized.class) - public static class EvalWithoutActivationTests { - private final String expr; - private final Object evaluatedResult; - - private static final CelCompiler COMPILER = - CelCompilerFactory.standardCelCompilerBuilder() - .setOptions(TEST_OPTIONS) - .setStandardMacros(CelStandardMacro.STANDARD_MACROS) - .build(); - - @Parameters - public static List data() { - return Arrays.asList( - new Object[][] { - {"1 < 2", true}, - {"1 + 2 + 3", 6L}, - {"1.9 + 2.0", 3.9}, - {"true == true", true}, - }); - } - - public EvalWithoutActivationTests(String expr, Object evaluatedResult) { - this.expr = expr; - this.evaluatedResult = evaluatedResult; - } - - @Test - public void evaluateExpr_returnsExpectedResult() throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile(expr).getAst(); - assertThat(EVAL.eval(ast, Activation.EMPTY)).isEqualTo(evaluatedResult); - } - } - - @RunWith(Parameterized.class) - public static class EvalWithActivationTests { - private final String expr; - private final Object paramValue; - private final Object evaluatedResult; - private final CelCompiler compiler; - - @Parameters - public static List data() { - return Arrays.asList( - new Object[][] { - {"x < 2", 1, SimpleType.INT, true}, - {"x + 2 + 3", 1, SimpleType.INT, 6L}, - {"x + 2.0", 1.9, SimpleType.DOUBLE, 3.9}, - {"x == true", true, SimpleType.BOOL, true}, - }); - } - - public EvalWithActivationTests( - String expr, Object paramValue, CelType paramType, Object evaluatedResult) { - this.expr = expr; - this.paramValue = paramValue; - this.evaluatedResult = evaluatedResult; - this.compiler = - CelCompilerFactory.standardCelCompilerBuilder() - .setOptions(TEST_OPTIONS) - .setStandardMacros(CelStandardMacro.STANDARD_MACROS) - .addVar("x", paramType) - .build(); - } - - @Test - public void expr_returnsExpectedResult() throws Exception { - CelAbstractSyntaxTree ast = compiler.compile(expr).getAst(); - assertThat(EVAL.eval(ast, Activation.of("x", paramValue))).isEqualTo(evaluatedResult); - } - } -} diff --git a/testing/src/test/java/dev/cel/testing/testrunner/BUILD.bazel b/testing/src/test/java/dev/cel/testing/testrunner/BUILD.bazel new file mode 100644 index 000000000..9141832cb --- /dev/null +++ b/testing/src/test/java/dev/cel/testing/testrunner/BUILD.bazel @@ -0,0 +1,291 @@ +load("@rules_java//java:java_library.bzl", "java_library") +load("@rules_java//java:java_test.bzl", "java_test") +load("@rules_proto//proto:defs.bzl", "proto_descriptor_set") +load("//testing/testrunner:cel_java_test.bzl", "cel_java_test") + +package( + default_applicable_licenses = ["//:license"], + default_testonly = 1, +) + +# Since the user test class is triggered by the cel_test rule, we should not add it to the +# junit4_test_suite. +# This is just a sample test class for the cel_test rule. +java_library( + name = "user_test", + srcs = ["UserTest.java"], + deps = [ + "//bundle:cel", + "//common/types", + "//testing/testrunner:cel_test_context", + "//testing/testrunner:cel_user_test_template", + "@maven//:junit_junit", + ], +) + +# This is just a sample test class for the cel_test rule. +java_library( + name = "env_config_user_test", + srcs = ["EnvConfigUserTest.java"], + deps = [ + "//bundle:cel", + "//testing/testrunner:cel_test_context", + "//testing/testrunner:cel_user_test_template", + "@maven//:junit_junit", + ], +) + +java_library( + name = "late_function_binding_user_test", + srcs = ["LateFunctionBindingUserTest.java"], + deps = [ + "//runtime", + "//runtime:function_binding", + "//testing/testrunner:cel_test_context", + "//testing/testrunner:cel_user_test_template", + "@maven//:junit_junit", + ], +) + +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", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + "@maven//:junit_junit", + ], +) + +java_library( + name = "context_pb_user_test", + srcs = ["ContextPbUserTest.java"], + deps = [ + "//bundle:cel", + "//checker:proto_type_mask", + "//testing/testrunner:cel_test_context", + "//testing/testrunner:cel_user_test_template", + "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", + "@maven//:junit_junit", + ], +) + +java_test( + name = "junit_xml_reporter_test", + srcs = ["JUnitXmlReporterTest.java"], + test_class = "dev.cel.testing.testrunner.JUnitXmlReporterTest", + deps = [ + "//:java_truth", + "//testing/testrunner:cel_coverage_index", + "//testing/testrunner:junit_xml_reporter", + "@maven//:junit_junit", + "@maven//:org_mockito_mockito_core", + ], +) + +java_test( + name = "cel_coverage_index_test", + srcs = ["CelCoverageIndexTest.java"], + test_class = "dev.cel.testing.testrunner.CelCoverageIndexTest", + deps = [ + "//:java_truth", + "//bundle:cel", + "//common:cel_ast", + "//common:options", + "//common/types", + "//compiler:compiler_builder", + "//extensions", + "//parser:macro", + "//runtime", + "//runtime:evaluation_listener", + "//testing/testrunner:cel_coverage_index", + "@maven//:com_google_guava_guava", + "@maven//:junit_junit", + ], +) + +java_test( + name = "default_result_matcher_test", + srcs = ["DefaultResultMatcherTest.java"], + test_class = "dev.cel.testing.testrunner.DefaultResultMatcherTest", + deps = [ + "//:java_truth", + "//bundle:cel", + "//common/types", + "//runtime", + "//testing/testrunner:cel_test_suite", + "//testing/testrunner:default_result_matcher", + "//testing/testrunner:result_matcher", + "@cel_spec//proto/cel/expr:expr_java_proto", + "@maven//:com_google_guava_guava", + "@maven//:com_google_testparameterinjector_test_parameter_injector", + "@maven//:junit_junit", + ], +) + +java_test( + name = "cel_test_suite_yaml_parser_test", + srcs = ["CelTestSuiteYamlParserTest.java"], + test_class = "dev.cel.testing.testrunner.CelTestSuiteYamlParserTest", + deps = [ + "//:java_truth", + "//testing/testrunner:cel_test_suite", + "//testing/testrunner:cel_test_suite_exception", + "//testing/testrunner:cel_test_suite_yaml_parser", + "@maven//:com_google_guava_guava", + "@maven//:com_google_testparameterinjector_test_parameter_injector", + "@maven//:junit_junit", + ], +) + +java_test( + name = "cel_test_suite_textproto_parser_test", + srcs = ["CelTestSuiteTextprotoParserTest.java"], + test_class = "dev.cel.testing.testrunner.CelTestSuiteTextprotoParserTest", + deps = [ + "//:java_truth", + "//testing/testrunner:cel_test_suite_exception", + "//testing/testrunner:cel_test_suite_text_proto_parser", + "@cel_spec//proto/cel/expr/conformance/test:suite_java_proto", + "@maven//:junit_junit", + ], +) + +cel_java_test( + name = "custom_variable_binding_test_runner_sample", + cel_expr = "custom_variable_bindings/policy.yaml", + config = "custom_variable_bindings/config.yaml", + test_data_path = "//testing/src/test/resources/policy", + test_src = ":custom_variable_binding_user_test", + test_suite = "custom_variable_bindings/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 = "test_runner_yaml_sample_with_eval_error", + cel_expr = "nested_rule/eval_error_policy.yaml", + config = "nested_rule/eval_error_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/eval_error_tests.yaml", +) + +cel_java_test( + name = "test_runner_sample_with_expr_value_output", + cel_expr = "expr_value_output/policy.yaml", + test_data_path = "//testing/src/test/resources/policy", + test_src = ":user_test", + test_suite = "expr_value_output/tests.textproto", +) + +cel_java_test( + name = "test_runner_sample_with_eval_error", + cel_expr = "nested_rule/eval_error_policy.yaml", + config = "nested_rule/eval_error_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/eval_error_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", + is_raw_expr = True, + test_data_path = "//testing/src/test/resources/expressions", + test_src = ":user_test", + test_suite = "simple_test_case/tests.textproto", +) + +cel_java_test( + name = "extension_as_input_test", + cel_expr = "2 + 2 == 4", + is_raw_expr = True, + 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 = "protoextension_value_as_input/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", + ], +) + +java_library( + name = "custom_test_suite", + srcs = ["CustomTestSuite.java"], + deps = [ + "//testing/testrunner:annotations", + "//testing/testrunner:cel_test_suite", + "@maven//:com_google_guava_guava", + ], +) + +cel_java_test( + name = "custom_test_suite_test", + cel_expr = "2 + 2 == 4", + is_raw_expr = True, + test_src = ":user_test", + deps = [ + ":custom_test_suite", + ], +) + +cel_java_test( + name = "expression_cel_file_test", + cel_expr = "simple_test_case/simple_expression.cel", + test_data_path = "//testing/src/test/resources/expressions", + test_src = ":user_test", + test_suite = "simple_test_case/tests.textproto", +) + +java_library( + name = "coverage_test", + srcs = ["CoverageTest.java"], + deps = [ + "//bundle:cel", + "//common/types", + "//testing/testrunner:cel_test_context", + "//testing/testrunner:cel_user_test_template", + "@maven//:junit_junit", + ], +) + +cel_java_test( + name = "cel_coverage_test", + cel_expr = "coverage_test_case/simple_expression.cel", + enable_coverage = True, + test_data_path = "//testing/src/test/resources/expressions", + test_src = ":coverage_test", + test_suite = "coverage_test_case/tests.textproto", +) + +cel_java_test( + name = "cel_partial_coverage_test", + cel_expr = "coverage_test_case/simple_expression.cel", + enable_coverage = True, + test_data_path = "//testing/src/test/resources/expressions", + test_src = ":coverage_test", + test_suite = "coverage_test_case/partial_coverage_tests.textproto", +) diff --git a/testing/src/test/java/dev/cel/testing/testrunner/CelCoverageIndexTest.java b/testing/src/test/java/dev/cel/testing/testrunner/CelCoverageIndexTest.java new file mode 100644 index 000000000..4c6a6cf26 --- /dev/null +++ b/testing/src/test/java/dev/cel/testing/testrunner/CelCoverageIndexTest.java @@ -0,0 +1,181 @@ +// 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.testing.testrunner; + +import static com.google.common.truth.Truth.assertThat; +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.collect.ImmutableMap; +import com.google.common.io.Files; +import dev.cel.bundle.Cel; +import dev.cel.bundle.CelFactory; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelOptions; +import dev.cel.common.types.SimpleType; +import dev.cel.compiler.CelCompiler; +import dev.cel.extensions.CelExtensions; +import dev.cel.parser.CelStandardMacro; +import dev.cel.runtime.CelEvaluationListener; +import dev.cel.runtime.CelRuntime; +import dev.cel.testing.testrunner.CelCoverageIndex.CoverageReport; +import java.io.File; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class CelCoverageIndexTest { + + @Test + public void getCoverageReport_fullCoverage() throws Exception { + Cel cel = + CelFactory.standardCelBuilder() + .addVar("x", SimpleType.INT) + .addVar("y", SimpleType.INT) + .build(); + CelAbstractSyntaxTree ast = cel.compile("x > 1 && y > 1").getAst(); + CelRuntime.Program program = cel.createProgram(ast); + CelCoverageIndex coverageIndex = new CelCoverageIndex(); + coverageIndex.init(ast); + CelEvaluationListener listener = coverageIndex.newEvaluationListener(); + + program.trace(ImmutableMap.of("x", 2L, "y", 2L), listener); + + CoverageReport report = coverageIndex.generateCoverageReport(); + assertThat(report.nodes()).isGreaterThan(0); + assertThat(report.coveredNodes()).isEqualTo(report.nodes()); + assertThat(report.branches()).isEqualTo(6); + assertThat(report.coveredBooleanOutcomes()) + .isEqualTo(3); // x>1 -> true, y>1 -> true, && -> true + assertThat(report.unencounteredNodes()).isEmpty(); + assertThat(report.unencounteredBranches()) + .containsExactly( + "Expression ID 4 ('x > 1 && y > 1'): lacks 'false' coverage", + "\t\tExpression ID 2 ('x > 1'): lacks 'false' coverage", + "\t\tExpression ID 6 ('y > 1'): lacks 'false' coverage"); + } + + @Test + public void getCoverageReport_partialCoverage_shortCircuit() throws Exception { + Cel cel = + CelFactory.standardCelBuilder() + .addVar("x", SimpleType.INT) + .addVar("y", SimpleType.INT) + .build(); + CelAbstractSyntaxTree ast = cel.compile("x > 1 && y > 1").getAst(); + CelRuntime.Program program = cel.createProgram(ast); + CelCoverageIndex coverageIndex = new CelCoverageIndex(); + coverageIndex.init(ast); + CelEvaluationListener listener = coverageIndex.newEvaluationListener(); + + program.trace(ImmutableMap.of("x", 0L, "y", 2L), listener); + + CoverageReport report = coverageIndex.generateCoverageReport(); + assertThat(report.celExpression()).isEqualTo("x > 1 && y > 1"); + assertThat(report.nodes()).isGreaterThan(0); + assertThat(report.coveredNodes()).isLessThan(report.nodes()); + assertThat(report.branches()).isEqualTo(6); + assertThat(report.coveredBooleanOutcomes()).isEqualTo(2); // x>1 -> false, && -> false + assertThat(report.unencounteredNodes()).containsExactly("Expression ID 6 ('y > 1')"); + // y > 1 is unencountered, so logUnencountered becomes false, and branch coverage for y > 1 + // isn't logged to unencounteredBranches. + assertThat(report.unencounteredBranches()) + .containsExactly( + "Expression ID 4 ('x > 1 && y > 1'): lacks 'true' coverage", + "\t\tExpression ID 2 ('x > 1'): lacks 'true' coverage"); + } + + @Test + public void getCoverageReport_comprehension_generatesDotGraph() throws Exception { + Cel cel = CelFactory.standardCelBuilder().build(); + CelCompiler compiler = + cel.toCompilerBuilder() + .setOptions(CelOptions.newBuilder().populateMacroCalls(true).build()) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addLibraries(CelExtensions.comprehensions()) + .build(); + CelAbstractSyntaxTree ast = compiler.compile("[1, 2, 3].all(i, i % 2 != 0)").getAst(); + CelRuntime.Program program = cel.createProgram(ast); + CelCoverageIndex coverageIndex = new CelCoverageIndex(); + coverageIndex.init(ast); + CelEvaluationListener listener = coverageIndex.newEvaluationListener(); + + program.trace(ImmutableMap.of(), listener); + + CoverageReport report = coverageIndex.generateCoverageReport(); + assertThat(report.dotGraph()) + .contains("label=\"{<1> exprID: 1 | <2> IterRange} | <3> [1, 2, 3]\""); + assertThat(report.dotGraph()).contains("label=\"{<1> exprID: 12 | <2> AccuInit} | <3> true\""); + assertThat(report.dotGraph()).doesNotContain("red"); // No unencountered nodes. + assertThat(report.dotGraph()) + .contains( + "label=\"{<1> exprID: 14 | <2> LoopCondition} | <3>" + + " @not_strictly_false(@result)\""); + assertThat(report.dotGraph()) + .contains("label=\"{<1> exprID: 16 | <2> LoopStep} | <3> @result && i % 2 != 0\""); + assertThat(report.dotGraph()).contains("label=\"{<1> exprID: 17 | <2> Result} | <3> @result\""); + } + + @Test + public void getCoverageReport_fullCoverage_writesToUndeclaredOutputs() throws Exception { + // Setup for a more complex graph to write. + Cel cel = CelFactory.standardCelBuilder().build(); + CelCompiler compiler = + cel.toCompilerBuilder() + .setOptions(CelOptions.newBuilder().populateMacroCalls(true).build()) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addLibraries(CelExtensions.comprehensions()) + .build(); + CelAbstractSyntaxTree ast = compiler.compile("[1, 2, 3].all(i, i % 2 != 0)").getAst(); + CelRuntime.Program program = cel.createProgram(ast); + CelCoverageIndex coverageIndex = new CelCoverageIndex(); + coverageIndex.init(ast); + CelEvaluationListener listener = coverageIndex.newEvaluationListener(); + program.trace(ImmutableMap.of(), listener); + + CoverageReport report = coverageIndex.generateCoverageReport(); + + String undeclaredOutputsDir = System.getenv("TEST_UNDECLARED_OUTPUTS_DIR"); + assertThat(undeclaredOutputsDir).isNotNull(); + + File outputFile = new File(undeclaredOutputsDir, "cel_test_coverage/coverage_graph.txt"); + + String fileContent = Files.asCharSource(outputFile, UTF_8).read(); + assertThat(fileContent).isEqualTo(report.dotGraph()); + } + + @Test + public void getCoverageReport_fullCoverage_multipleEvaluations() throws Exception { + Cel cel = CelFactory.standardCelBuilder().addVar("x", SimpleType.INT).build(); + CelAbstractSyntaxTree ast = cel.compile("x > 1").getAst(); + CelRuntime.Program program = cel.createProgram(ast); + CelCoverageIndex coverageIndex = new CelCoverageIndex(); + coverageIndex.init(ast); + CelEvaluationListener listener = coverageIndex.newEvaluationListener(); + + program.trace(ImmutableMap.of("x", 2L), listener); + coverageIndex.init(ast); // Re-initialize the coverage index. + program.trace(ImmutableMap.of("x", 0L), listener); + + CoverageReport report = coverageIndex.generateCoverageReport(); + assertThat(report.nodes()).isGreaterThan(0); + assertThat(report.coveredNodes()).isEqualTo(report.nodes()); + assertThat(report.branches()).isEqualTo(2); + // Despite re-initializing the coverage index now, the report should still + // be fully covered. Else, only the second evaluation would've been covered. + assertThat(report.coveredBooleanOutcomes()).isEqualTo(2); + assertThat(report.unencounteredNodes()).isEmpty(); + assertThat(report.unencounteredBranches()).isEmpty(); + } +} diff --git a/testing/src/test/java/dev/cel/testing/testrunner/CelTestSuiteTextprotoParserTest.java b/testing/src/test/java/dev/cel/testing/testrunner/CelTestSuiteTextprotoParserTest.java new file mode 100644 index 000000000..4603cc845 --- /dev/null +++ b/testing/src/test/java/dev/cel/testing/testrunner/CelTestSuiteTextprotoParserTest.java @@ -0,0 +1,60 @@ +// 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.testing.testrunner; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import dev.cel.expr.conformance.test.InputContext; +import dev.cel.expr.conformance.test.InputValue; +import dev.cel.expr.conformance.test.TestCase; +import dev.cel.expr.conformance.test.TestSection; +import dev.cel.expr.conformance.test.TestSuite; +import java.io.IOException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class CelTestSuiteTextprotoParserTest { + + @Test + public void parseTestSuite_illegalInput_failure() throws IOException { + TestSuite testSuite = + TestSuite.newBuilder() + .setName("some_test_suite") + .setDescription("Some test suite") + .addSections( + TestSection.newBuilder() + .setName("section_name") + .addTests( + TestCase.newBuilder() + .setName("test_case_name") + .setDescription("Some test case") + .putInput("test_key", InputValue.getDefaultInstance()) + .setInputContext(InputContext.getDefaultInstance()) + .build()) + .build()) + .build(); + + CelTestSuiteException exception = + assertThrows( + CelTestSuiteException.class, + () -> CelTestSuiteTextProtoParser.parseCelTestSuite(testSuite)); + + assertThat(exception) + .hasMessageThat() + .contains("Test case: test_case_name cannot have both input map and input context."); + } +} diff --git a/testing/src/test/java/dev/cel/testing/testrunner/CelTestSuiteYamlParserTest.java b/testing/src/test/java/dev/cel/testing/testrunner/CelTestSuiteYamlParserTest.java new file mode 100644 index 000000000..46d509e5e --- /dev/null +++ b/testing/src/test/java/dev/cel/testing/testrunner/CelTestSuiteYamlParserTest.java @@ -0,0 +1,489 @@ +// 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.testing.testrunner; + +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.common.collect.ImmutableSet; +import com.google.testing.junit.testparameterinjector.TestParameter; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.testing.testrunner.CelTestSuite.CelTestSection.CelTestCase.Input.Binding; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public final class CelTestSuiteYamlParserTest { + + private static final CelTestSuiteYamlParser CEL_TEST_SUITE_YAML_PARSER = + CelTestSuiteYamlParser.newInstance(); + + @Test + public void parseTestSuite_withBindingsAsInput_success() throws CelTestSuiteException { + String testSuiteYamlContent = + "name: 'test_suite_name'\n" + + "description: 'test_suite_description'\n" + + "sections:\n" + + "- name: 'test_section_name'\n" + + " description: 'test_section_description'\n" + + " tests:\n" + + " - name: 'test_case_name'\n" + + " description: 'test_case_description'\n" + + " input:\n" + + " test_key:\n" + + " value:\n" + + " - nested_key: true\n" + + " output:\n" + + " value: 'test_result_value'\n" + + " - name: 'test_case_name_2'\n" + + " description: 'test_case_description_2'\n" + + " input:\n" + + " test_key:\n" + + " value: 1\n" + + " output:\n" + + " value: 2.20\n"; + + CelTestSuite testSuite = CEL_TEST_SUITE_YAML_PARSER.parse(testSuiteYamlContent); + CelTestSuite expectedTestSuite = + CelTestSuite.newBuilder() + .setSource(testSuite.source().get()) + .setName("test_suite_name") + .setDescription("test_suite_description") + .setSections( + ImmutableSet.of( + CelTestSuite.CelTestSection.newBuilder() + .setName("test_section_name") + .setDescription("test_section_description") + .setTests( + ImmutableSet.of( + CelTestSuite.CelTestSection.CelTestCase.newBuilder() + .setName("test_case_name") + .setDescription("test_case_description") + .setInput( + CelTestSuite.CelTestSection.CelTestCase.Input.ofBindings( + ImmutableMap.of( + "test_key", + Binding.ofValue( + ImmutableList.of( + ImmutableMap.of("nested_key", true)))))) + .setOutput( + CelTestSuite.CelTestSection.CelTestCase.Output + .ofResultValue("test_result_value")) + .build(), + CelTestSuite.CelTestSection.CelTestCase.newBuilder() + .setName("test_case_name_2") + .setDescription("test_case_description_2") + .setInput( + CelTestSuite.CelTestSection.CelTestCase.Input.ofBindings( + ImmutableMap.of("test_key", Binding.ofValue(1)))) + .setOutput( + CelTestSuite.CelTestSection.CelTestCase.Output + .ofResultValue(2.20)) + .build())) + .build())) + .build(); + + assertThat(testSuite).isEqualTo(expectedTestSuite); + assertThat(testSuite.source().get().getPositionsMap()).isNotEmpty(); + } + + @Test + public void parseTestSuite_withExprAsOutput_success() throws CelTestSuiteException { + String testSuiteYamlContent = + "name: 'test_suite_name'\n" + + "description: 'test_suite_description'\n" + + "sections:\n" + + "- name: 'test_section_name'\n" + + " description: 'test_section_description'\n" + + " tests:\n" + + " - name: 'test_case_name'\n" + + " description: 'test_case_description'\n" + + " input:\n" + + " test_key:\n" + + " expr: 'some_value'\n" + + " output:\n" + + " expr: '1 == 1'\n"; + + CelTestSuite testSuite = CEL_TEST_SUITE_YAML_PARSER.parse(testSuiteYamlContent); + CelTestSuite expectedTestSuite = + CelTestSuite.newBuilder() + .setSource(testSuite.source().get()) + .setName("test_suite_name") + .setDescription("test_suite_description") + .setSections( + ImmutableSet.of( + CelTestSuite.CelTestSection.newBuilder() + .setName("test_section_name") + .setDescription("test_section_description") + .setTests( + ImmutableSet.of( + CelTestSuite.CelTestSection.CelTestCase.newBuilder() + .setName("test_case_name") + .setDescription("test_case_description") + .setInput( + CelTestSuite.CelTestSection.CelTestCase.Input.ofBindings( + ImmutableMap.of( + "test_key", Binding.ofExpr("some_value")))) + .setOutput( + CelTestSuite.CelTestSection.CelTestCase.Output.ofResultExpr( + "1 == 1")) + .build())) + .build())) + .build(); + + assertThat(testSuite).isEqualTo(expectedTestSuite); + assertThat(testSuite.source().get().getPositionsMap()).isNotEmpty(); + } + + @Test + public void parseTestSuite_failure_throwsException( + @TestParameter TestSuiteYamlParsingErrorTestCase testCase) throws CelTestSuiteException { + CelTestSuiteException celTestSuiteException = + assertThrows( + CelTestSuiteException.class, + () -> CEL_TEST_SUITE_YAML_PARSER.parse(testCase.testSuiteYamlContent)); + + assertThat(celTestSuiteException).hasMessageThat().contains(testCase.expectedErrorMessage); + } + + private enum TestSuiteYamlParsingErrorTestCase { + TEST_SUITE_WITH_MISALIGNED_NAME_TAG( + "name: 'test_suite_name'\n" + + "description: 'test_suite_description'\n" + + "sections:\n" + + "name: 'test_section_name'\n" + + " description: 'test_section_description'\n" + + " tests:\n" + + "name: 'test_case_name'\n" + + " description: 'test_case_description'\n" + + " input:\n" + + " test_key: 'test_value'\n" + + " value: 'test_result_value'\n", + "YAML document is malformed: while parsing a block mapping\n" + + " in 'reader', line 1, column 1:\n" + + " name: 'test_suite_name'\n" + + " ^"), + TEST_SUITE_WITH_ILLEGAL_TEST_SUITE_TAG( + "name: 'test_suite_name'\n" + + "description: 'test_suite_description'\n" + + "sections:\n" + + "- name: 'test_section_name'\n" + + " description: 'test_section_description'\n" + + " tests:\n" + + " - name: 'test_case_name'\n" + + " description: 'test_case_description'\n" + + " input:\n" + + " test_key:\n" + + " value: 'test_value'\n" + + " output:\n" + + " value: 'test_result_value'\n" + + "unknown_tag: 'test_value'\n", + "ERROR: :14:1: Unknown test suite tag: unknown_tag\n" + + " | unknown_tag: 'test_value'\n" + + " | ^"), + TEST_SUITE_WITH_ILLEGAL_TEST_SECTION_TAG( + "name: 'test_suite_name'\n" + + "description: 'test_suite_description'\n" + + "sections:\n" + + "- name: 'test_section_name'\n" + + " unknown_tag: 'test_value'\n" + + " description: 'test_section_description'\n" + + " tests:\n" + + " - name: 'test_case_name'\n" + + " description: 'test_case_description'\n" + + " input:\n" + + " test_key:\n" + + " value: 'test_value'\n" + + " output:\n" + + " value: 'test_result_value'\n", + "ERROR: :5:3: Unknown test section tag: unknown_tag\n" + + " | unknown_tag: 'test_value'\n" + + " | ..^"), + TEST_SUITE_WITH_ILLEGAL_TEST_CASE_OUTPUT_TAG( + "name: 'test_suite_name'\n" + + "description: 'test_suite_description'\n" + + "sections:\n" + + "- name: 'test_section_name'\n" + + " description: 'test_section_description'\n" + + " tests:\n" + + " - name: 'test_case_name'\n" + + " description: 'test_case_description'\n" + + " input:\n" + + " test_key:\n" + + " value: 'test_value'\n" + + " output:\n" + + " unknown_tag: 'test_result_value'\n", + "ERROR: :13:7: Unknown output tag: unknown_tag\n" + + " | unknown_tag: 'test_result_value'\n" + + " | ......^"), + TEST_SUITE_WITH_ILLEGAL_TEST_CASE_TAG( + "name: 'test_suite_name'\n" + + "description: 'test_suite_description'\n" + + "sections:\n" + + "- name: 'test_section_name'\n" + + " description: 'test_section_description'\n" + + " tests:\n" + + " - name: 'test_case_name'\n" + + " description: 'test_case_description'\n" + + " input:\n" + + " test_key:\n" + + " value: 'test_value'\n" + + " output:\n" + + " value: 'test_result_value'\n" + + " unknown_tag: 'test_value'\n", + "ERROR: :14:5: Unknown test case tag: unknown_tag\n" + + " | unknown_tag: 'test_value'\n" + + " | ....^"), + ILLEGAL_TEST_SUITE_WITH_SECTION_NOT_LIST( + "name: 'test_suite_name'\n" + + "description: 'test_suite_description'\n" + + "sections:\n" + + " name: 'test_section_name'\n" + + " description: 'test_section_description'\n" + + " tests:\n" + + " - name: 'test_case_name'\n" + + " description: 'test_case_description'\n" + + " input:\n" + + " test_key:\n" + + " value: 'test_value'\n" + + " output:\n" + + " value: 'test_result_value'\n", + "ERROR: :4:3: Got yaml node type tag:yaml.org,2002:map, wanted type(s)" + + " [tag:yaml.org,2002:seq]\n" + + " | name: 'test_section_name'\n" + + " | ..^\n" + + "ERROR: :4:3: Sections is not a list: tag:yaml.org,2002:map\n" + + " | name: 'test_section_name'\n" + + " | ..^"), + ILLEGAL_TEST_SUITE_WITH_TEST_SUITE_NOT_MAP( + "- name: 'test_suite_name'\n" + + "- description: 'test_suite_description'\n" + + "- sections:\n" + + " name: 'test_section_name'\n" + + " description: 'test_section_description'\n" + + " tests:\n" + + " - name: 'test_case_name'\n" + + " description: 'test_case_description'\n" + + " input:\n" + + " test_key:\n" + + " value: 'test_value'\n" + + " output:\n" + + " value: 'test_result_value'\n", + "ERROR: :1:1: Got yaml node type tag:yaml.org,2002:seq, wanted type(s)" + + " [tag:yaml.org,2002:map]\n" + + " | - name: 'test_suite_name'\n" + + " | ^\n" + + "ERROR: :1:1: Unknown test suite type: tag:yaml.org,2002:seq\n" + + " | - name: 'test_suite_name'\n" + + " | ^"), + ILLEGAL_TEST_SUITE_WITH_OUTPUT_NOT_MAP( + "name: 'test_suite_name'\n" + + "description: 'test_suite_description'\n" + + "sections:\n" + + "- name: 'test_section_name'\n" + + " description: 'test_section_description'\n" + + " tests:\n" + + " - name: 'test_case_name'\n" + + " description: 'test_case_description'\n" + + " input:\n" + + " test_key:\n" + + " value: 'test_value'\n" + + " output:\n" + + " - value: 'test_result_value'\n", + "ERROR: :13:7: Got yaml node type tag:yaml.org,2002:seq, wanted type(s)" + + " [tag:yaml.org,2002:map]\n" + + " | - value: 'test_result_value'\n" + + " | ......^\n" + + "ERROR: :13:7: Output is not a map: tag:yaml.org,2002:seq\n" + + " | - value: 'test_result_value'\n" + + " | ......^"), + ILLEGAL_TEST_SUITE_WITH_MORE_THAN_ONE_INPUT_VALUES_AGAINST_KEY( + "name: 'test_suite_name'\n" + + "description: 'test_suite_description'\n" + + "sections:\n" + + "- name: 'test_section_name'\n" + + " description: 'test_section_description'\n" + + " tests:\n" + + " - name: 'test_case_name'\n" + + " description: 'test_case_description'\n" + + " input:\n" + + " test_key:\n" + + " value: 'test_value'\n" + + " value: 'test_value_2'\n" + + " expr: 'test_expr'\n" + + " output:\n" + + " value: 'test_result_value'\n", + "ERROR: :11:11: Input binding node must have exactly one value:" + + " tag:yaml.org,2002:map\n" + + " | value: 'test_value'\n" + + " | ..........^"), + ILLEGAL_TEST_SUITE_WITH_UNKNOWN_INPUT_BINDING_VALUE_TAG( + "name: 'test_suite_name'\n" + + "description: 'test_suite_description'\n" + + "sections:\n" + + "- name: 'test_section_name'\n" + + " description: 'test_section_description'\n" + + " tests:\n" + + " - name: 'test_case_name'\n" + + " description: 'test_case_description'\n" + + " input:\n" + + " test_key:\n" + + " something: 'test_value'\n" + + " output:\n" + + " value: 'test_result_value'\n", + "ERROR: :11:11: Unknown input binding value tag: something\n" + + " | something: 'test_value'\n" + + " | ..........^"), + ILLEGAL_TEST_SUITE_WITH_BINDINGS_NOT_MAP( + "name: 'test_suite_name'\n" + + "description: 'test_suite_description'\n" + + "sections:\n" + + "- name: 'test_section_name'\n" + + " description: 'test_section_description'\n" + + " tests:\n" + + " - name: 'test_case_name'\n" + + " description: 'test_case_description'\n" + + " input:\n" + + " - test_key:\n" + + " value: 'test_value'\n" + + " output:\n" + + " value: 'test_result_value'\n", + "ERROR: :10:10: Got yaml node type tag:yaml.org,2002:seq, wanted type(s)" + + " [tag:yaml.org,2002:map]\n" + + " | - test_key:\n" + + " | .........^\n" + + "ERROR: :10:10: Input is not a map: tag:yaml.org,2002:seq\n" + + " | - test_key:\n" + + " | .........^"), + ILLEGAL_TEST_SUITE_WITH_ILLEGAL_BINDINGS_VALUE( + "name: 'test_suite_name'\n" + + "description: 'test_suite_description'\n" + + "sections:\n" + + "- name: 'test_section_name'\n" + + " description: 'test_section_description'\n" + + " tests:\n" + + " - name: 'test_case_name'\n" + + " description: 'test_case_description'\n" + + " input:\n" + + " test_key:\n" + + " - value\n" + + " - 'test_value'\n" + + " output:\n" + + " value: 'test_result_value'\n", + "ERROR: :11:13: Got yaml node type tag:yaml.org,2002:seq, wanted type(s)" + + " [tag:yaml.org,2002:map]\n" + + " | - value\n" + + " | ............^\n" + + "ERROR: :11:13: Input binding node is not a map: tag:yaml.org,2002:seq\n" + + " | - value\n" + + " | ............^"), + ILLEGAL_TEST_SUITE_WITH_ILLEGAL_CONTEXT_EXPR_VALUE( + "name: 'test_suite_name'\n" + + "description: 'test_suite_description'\n" + + "sections:\n" + + "- name: 'test_section_name'\n" + + " description: 'test_section_description'\n" + + " tests:\n" + + " - name: 'test_case_name'\n" + + " description: 'test_case_description'\n" + + " context_expr:\n" + + " test_key:\n" + + " value: 'test_value'\n" + + " output:\n" + + " value: 'test_result_value'\n", + "ERROR: :10:9: Got yaml node type tag:yaml.org,2002:map, wanted type(s)" + + " [tag:yaml.org,2002:str]\n" + + " | test_key:\n" + + " | ........^\n" + + "ERROR: :10:9: Input context is not a string: tag:yaml.org,2002:map\n" + + " | test_key:\n" + + " | ........^"), + ILLEGAL_TEST_SUITE_WITH_TESTS_NOT_LIST( + "name: 'test_suite_name'\n" + + "description: 'test_suite_description'\n" + + "sections:\n" + + "- name: 'test_section_name'\n" + + " description: 'test_section_description'\n" + + " tests:\n" + + " name: 'test_case_name'\n" + + " description: 'test_case_description'\n" + + " input:\n" + + " test_key:\n" + + " value: 'test_value'\n" + + " output:\n" + + " value: 'test_result_value'\n", + "ERROR: :7:5: Got yaml node type tag:yaml.org,2002:map, wanted type(s)" + + " [tag:yaml.org,2002:seq]\n" + + " | name: 'test_case_name'\n" + + " | ....^\n" + + "ERROR: :7:5: Tests is not a list: tag:yaml.org,2002:map\n" + + " | name: 'test_case_name'\n" + + " | ....^"), + TEST_SUITE_WITH_ILLEGAL_TEST_SUITE_FORMAT( + "- name: 'test_suite_name'\n" + + "- name: 'test_section_name'\n" + + "- name: 'test_section_name_2'\n", + "ERROR: :1:1: Unknown test suite type: tag:yaml.org,2002:seq\n" + + " | - name: 'test_suite_name'\n" + + " | ^"), + TEST_SUITE_WITH_ILLEGAL_SECTION_TYPE( + "name: 'test_suite_name'\n" + + "description: 'test_suite_description'\n" + + "1:\n" + + "- name: 'test_section_name'\n" + + " description: 'test_section_description'\n" + + " tests:\n" + + " - name: 'test_case_name'\n" + + " description: 'test_case_description'\n" + + " input:\n" + + " test_key:\n" + + " value: 'test_value'\n" + + " output:\n" + + " value: 'test_result_value'\n", + "ERROR: :3:1: Got yaml node type tag:yaml.org,2002:int, wanted type(s)" + + " [tag:yaml.org,2002:str !txt]\n" + + " | 1:\n" + + " | ^"), + TEST_CASE_WITH_ILLEGAL_UNKNOWN_OUTPUT_TYPE( + "name: 'test_suite_name'\n" + + "description: 'test_suite_description'\n" + + "sections:\n" + + "- name: 'test_section_name'\n" + + " description: 'test_section_description'\n" + + " tests:\n" + + " - name: 'test_case_name'\n" + + " description: 'test_case_description'\n" + + " input:\n" + + " test_key:\n" + + " value: 'test_value'\n" + + " output:\n" + + " unknown:\n" + + " - 'test_result_value'\n", + "ERROR: :14:9: Only integer ids are supported in unknown list. Found:" + + " java.lang.String\n" + + " | - 'test_result_value'\n" + + " | ........^"), + ; + + private final String testSuiteYamlContent; + private final String expectedErrorMessage; + + TestSuiteYamlParsingErrorTestCase(String testSuiteYamlContent, String expectedErrorMessage) { + this.testSuiteYamlContent = testSuiteYamlContent; + this.expectedErrorMessage = expectedErrorMessage; + } + } +} diff --git a/testing/src/test/java/dev/cel/testing/testrunner/ContextPbUserTest.java b/testing/src/test/java/dev/cel/testing/testrunner/ContextPbUserTest.java new file mode 100644 index 000000000..0270cc52d --- /dev/null +++ b/testing/src/test/java/dev/cel/testing/testrunner/ContextPbUserTest.java @@ -0,0 +1,44 @@ +// 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.testing.testrunner; + +import dev.cel.bundle.CelFactory; +import dev.cel.checker.ProtoTypeMask; +import dev.cel.expr.conformance.proto3.TestAllTypes; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +/** + * This test demonstrates the use case where the fields of a proto are used as variables in the CEL + * expression i.e. context_expr. + */ +@RunWith(Parameterized.class) +public class ContextPbUserTest extends CelUserTestTemplate { + + // TODO: Add support for context_expr and remove the need to add the proto type masks + // explicitly. + public ContextPbUserTest() { + super( + CelTestContext.newBuilder() + .setCel( + CelFactory.standardCelBuilder() + .addFileTypes(TestAllTypes.getDescriptor().getFile()) + .addProtoTypeMasks( + ProtoTypeMask.ofAllFields(TestAllTypes.getDescriptor().getFullName()) + .withFieldsAsVariableDeclarations()) + .build()) + .build()); + } +} diff --git a/testing/src/test/java/dev/cel/testing/testrunner/CoverageTest.java b/testing/src/test/java/dev/cel/testing/testrunner/CoverageTest.java new file mode 100644 index 000000000..c7b0c395d --- /dev/null +++ b/testing/src/test/java/dev/cel/testing/testrunner/CoverageTest.java @@ -0,0 +1,39 @@ +// 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.testing.testrunner; + +import dev.cel.bundle.CelFactory; +import dev.cel.common.types.SimpleType; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +/** + * Sample test class used for coverage analysis, demonstrating the use of the CEL test runner + * without a config file. + */ +@RunWith(Parameterized.class) +public class CoverageTest extends CelUserTestTemplate { + + public CoverageTest() { + super( + CelTestContext.newBuilder() + .setCel( + CelFactory.standardCelBuilder() + .addVar("x", SimpleType.INT) + .addVar("y", SimpleType.INT) + .build()) + .build()); + } +} diff --git a/testing/src/test/java/dev/cel/testing/testrunner/CustomTestSuite.java b/testing/src/test/java/dev/cel/testing/testrunner/CustomTestSuite.java new file mode 100644 index 000000000..ba9147678 --- /dev/null +++ b/testing/src/test/java/dev/cel/testing/testrunner/CustomTestSuite.java @@ -0,0 +1,49 @@ +// 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.testing.testrunner; + +import com.google.common.collect.ImmutableSet; +import dev.cel.testing.testrunner.Annotations.TestSuiteSupplier; +import java.util.logging.Logger; + +final class CustomTestSuite { + + private static final Logger logger = Logger.getLogger(CustomTestSuite.class.getName()); + + @TestSuiteSupplier + public CelTestSuite getCelTestSuite() { + logger.info("TestSuite Parser Triggered."); + return CelTestSuite.newBuilder() + .setDescription("CustomFunctionClass Test Suite") + .setName("CustomFunctionClass Test Suite") + .setSections( + ImmutableSet.of( + CelTestSuite.CelTestSection.newBuilder() + .setName("valid") + .setDescription("valid") + .setTests( + ImmutableSet.of( + CelTestSuite.CelTestSection.CelTestCase.newBuilder() + .setName("valid") + .setDescription("valid") + .setInput(CelTestSuite.CelTestSection.CelTestCase.Input.ofNoInput()) + .setOutput( + CelTestSuite.CelTestSection.CelTestCase.Output.ofResultValue( + true)) + .build())) + .build())) + .build(); + } +} diff --git a/testing/src/test/java/dev/cel/testing/testrunner/CustomVariableBindingUserTest.java b/testing/src/test/java/dev/cel/testing/testrunner/CustomVariableBindingUserTest.java new file mode 100644 index 000000000..37382052c --- /dev/null +++ b/testing/src/test/java/dev/cel/testing/testrunner/CustomVariableBindingUserTest.java @@ -0,0 +1,53 @@ +// 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.testing.testrunner; + +import com.google.common.collect.ImmutableMap; +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; +import org.junit.runners.Parameterized; + +/** + * This test demonstrates the use case where the custom variable bindings are being provided + * programmatically for using extensions. + */ +@RunWith(Parameterized.class) +public class CustomVariableBindingUserTest extends CelUserTestTemplate { + + public CustomVariableBindingUserTest() { + 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/DefaultResultMatcherTest.java b/testing/src/test/java/dev/cel/testing/testrunner/DefaultResultMatcherTest.java new file mode 100644 index 000000000..41677c16c --- /dev/null +++ b/testing/src/test/java/dev/cel/testing/testrunner/DefaultResultMatcherTest.java @@ -0,0 +1,131 @@ +// 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.testing.testrunner; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import dev.cel.expr.ExprValue; +import dev.cel.expr.Value; +import com.google.common.collect.ImmutableList; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.bundle.CelFactory; +import dev.cel.common.types.SimpleType; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.testing.testrunner.CelTestSuite.CelTestSection.CelTestCase.Output; +import dev.cel.testing.testrunner.ResultMatcher.ResultMatcherParams; +import java.util.Optional; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public class DefaultResultMatcherTest { + + private static final DefaultResultMatcher MATCHER = new DefaultResultMatcher(); + + @Test + public void match_resultExprEvaluationError_failure() throws Exception { + ResultMatcherParams params = + ResultMatcherParams.newBuilder() + .setExpectedOutput(Optional.of(Output.ofResultExpr("2 / 0"))) + .setComputedOutput( + ResultMatcherParams.ComputedOutput.ofExprValue( + ExprValue.newBuilder() + .setValue(Value.newBuilder().setInt64Value(0).build()) + .build())) + .setResultType(SimpleType.INT) + .build(); + + IllegalArgumentException thrown = + assertThrows( + IllegalArgumentException.class, + () -> MATCHER.match(params, CelFactory.standardCelBuilder().build())); + + assertThat(thrown) + .hasMessageThat() + .contains("Failed to evaluate result_expr: evaluation error at :2: / by zero"); + } + + @Test + public void match_expectedExprValueForResultExprOutputAndComputedEvalError_failure() + throws Exception { + ResultMatcherParams params = + ResultMatcherParams.newBuilder() + .setExpectedOutput(Optional.of(Output.ofResultExpr("x + y"))) + .setComputedOutput( + ResultMatcherParams.ComputedOutput.ofError( + new CelEvaluationException("evaluation error"))) + .setResultType(SimpleType.INT) + .build(); + + AssertionError thrown = + assertThrows( + AssertionError.class, + () -> MATCHER.match(params, CelFactory.standardCelBuilder().build())); + + assertThat(thrown).hasMessageThat().contains("Error: evaluation error"); + } + + @Test + public void match_expectedExprValueAndComputedEvalError_failure() throws Exception { + ResultMatcherParams params = + ResultMatcherParams.newBuilder() + .setExpectedOutput( + Optional.of( + Output.ofResultValue( + ExprValue.newBuilder() + .setValue(Value.newBuilder().setInt64Value(3).build()) + .build()))) + .setComputedOutput( + ResultMatcherParams.ComputedOutput.ofError( + new CelEvaluationException("evaluation error"))) + .setResultType(SimpleType.INT) + .build(); + + AssertionError thrown = + assertThrows( + AssertionError.class, + () -> MATCHER.match(params, CelFactory.standardCelBuilder().build())); + + assertThat(thrown).hasMessageThat().contains("Error: evaluation error"); + } + + @Test + public void match_expectedEvalErrorAndComputedExprValue_failure() throws Exception { + ResultMatcherParams params = + ResultMatcherParams.newBuilder() + .setExpectedOutput( + Optional.of(Output.ofEvalError(ImmutableList.of("evaluation error")))) + .setComputedOutput( + ResultMatcherParams.ComputedOutput.ofExprValue( + ExprValue.newBuilder() + .setValue(Value.newBuilder().setInt64Value(3).build()) + .build())) + .setResultType(SimpleType.INT) + .build(); + + AssertionError thrown = + assertThrows( + AssertionError.class, + () -> MATCHER.match(params, CelFactory.standardCelBuilder().build())); + + assertThat(thrown) + .hasMessageThat() + .contains( + "Evaluation was successful but no value was provided. Computed output: value {\n" + + " int64_value: 3\n" + + "}"); + } +} diff --git a/testing/src/test/java/dev/cel/testing/testrunner/EnvConfigUserTest.java b/testing/src/test/java/dev/cel/testing/testrunner/EnvConfigUserTest.java new file mode 100644 index 000000000..99b2d1f79 --- /dev/null +++ b/testing/src/test/java/dev/cel/testing/testrunner/EnvConfigUserTest.java @@ -0,0 +1,31 @@ +// 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.testing.testrunner; + +import dev.cel.bundle.CelFactory; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +/** + * This test demonstrates the use case where the declarations are provided in the environment config + * file. + */ +@RunWith(Parameterized.class) +public class EnvConfigUserTest extends CelUserTestTemplate { + + public EnvConfigUserTest() { + super(CelTestContext.newBuilder().setCel(CelFactory.standardCelBuilder().build()).build()); + } +} diff --git a/testing/src/test/java/dev/cel/testing/testrunner/JUnitXmlReporterTest.java b/testing/src/test/java/dev/cel/testing/testrunner/JUnitXmlReporterTest.java new file mode 100644 index 000000000..b2a610ccb --- /dev/null +++ b/testing/src/test/java/dev/cel/testing/testrunner/JUnitXmlReporterTest.java @@ -0,0 +1,215 @@ +// 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.testing.testrunner; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.when; + +import dev.cel.testing.testrunner.JUnitXmlReporter.TestContext; +import dev.cel.testing.testrunner.JUnitXmlReporter.TestResult; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +@RunWith(JUnit4.class) +public class JUnitXmlReporterTest { + + private static final String SUITE_NAME = "TestSuiteName"; + private static final String TEST_CLASS_NAME = "TestClass1"; + private static final String TEST_METHOD_NAME = "testMethod1"; + + @Rule public final MockitoRule mockito = MockitoJUnit.rule(); + + @Mock private TestContext context; + @Mock private TestResult result1; + @Mock private TestResult result2; + @Mock private TestResult resultFailure; + + @Rule public TemporaryFolder tempFolder = new TemporaryFolder(); + + @Test + public void testGenerateReport_success() throws IOException { + String outputFileName = "test-report.xml"; + File subFolder = tempFolder.newFolder("subFolder"); + File outputFile = new File(subFolder.getAbsolutePath(), outputFileName); + JUnitXmlReporter reporter = new JUnitXmlReporter(outputFile.getAbsolutePath()); + long startTime = 100L; + long test1EndTime = startTime + 400; + long endTime = startTime + 900; + + when(context.getSuiteName()).thenReturn(SUITE_NAME); + when(context.getStartTime()).thenReturn(startTime); + when(context.getEndTime()).thenReturn(endTime); + reporter.onStart(context); + + when(result1.getTestClassName()).thenReturn(TEST_CLASS_NAME); + when(result1.getName()).thenReturn(TEST_METHOD_NAME); + when(result1.getStartMillis()).thenReturn(startTime); + when(result1.getEndMillis()).thenReturn(test1EndTime); + when(result1.getStatus()).thenReturn(JUnitXmlReporter.TestResult.SUCCESS); + reporter.onTestSuccess(result1); + + when(result2.getTestClassName()).thenReturn("TestClass2"); + when(result2.getName()).thenReturn("testMethod2"); + when(result2.getStartMillis()).thenReturn(test1EndTime); + when(result2.getEndMillis()).thenReturn(endTime); + when(result2.getStatus()).thenReturn(JUnitXmlReporter.TestResult.SUCCESS); + reporter.onTestSuccess(result2); + + reporter.onFinish(); + assertThat(outputFile.exists()).isTrue(); + String concatenatedFileContent = String.join("\n", Files.readAllLines(outputFile.toPath())); + + assertThat(concatenatedFileContent) + .contains( + ""); + + outputFile.delete(); + } + + @Test + public void testGenerateReport_failure() throws IOException { + String outputFileName = "test-report.xml"; + File subFolder = tempFolder.newFolder("subFolder"); + File outputFile = new File(subFolder.getAbsolutePath(), outputFileName); + JUnitXmlReporter reporter = new JUnitXmlReporter(outputFile.getAbsolutePath()); + + when(context.getSuiteName()).thenReturn(SUITE_NAME); + when(context.getStartTime()).thenReturn(0L); + when(context.getEndTime()).thenReturn(1000L); + reporter.onStart(context); + + when(resultFailure.getTestClassName()).thenReturn(TEST_CLASS_NAME); + when(resultFailure.getName()).thenReturn(TEST_METHOD_NAME); + when(resultFailure.getStartMillis()).thenReturn(0L); + when(resultFailure.getEndMillis()).thenReturn(500L); + when(resultFailure.getStatus()).thenReturn(JUnitXmlReporter.TestResult.FAILURE); + Throwable throwable = new RuntimeException("Test Exception"); + when(resultFailure.getThrowable()).thenReturn(throwable); + reporter.onTestFailure(resultFailure); + reporter.onFinish(); + + assertThat(reporter.getNumFailed()).isEqualTo(1); + assertThat(outputFile.exists()).isTrue(); + + String concatenatedFileContent = String.join("\n", Files.readAllLines(outputFile.toPath())); + + assertThat(concatenatedFileContent).contains("failures=\"1\""); + assertThat(concatenatedFileContent).contains("failure message=\"Test Exception\""); + } + + @Test + public void testGenerateReport_coverageReport_noCoverage() throws IOException { + String outputFileName = "test-report-with-coverage.xml"; + File subFolder = tempFolder.newFolder("subFolder"); + File outputFile = new File(subFolder.getAbsolutePath(), outputFileName); + JUnitXmlReporter reporter = new JUnitXmlReporter(outputFile.getAbsolutePath()); + long startTime = 100L; + long test1EndTime = startTime + 400; + long endTime = startTime + 900; + + CelCoverageIndex.CoverageReport coverageReport = + CelCoverageIndex.CoverageReport.builder().build(); + + when(context.getSuiteName()).thenReturn(SUITE_NAME); + when(context.getStartTime()).thenReturn(startTime); + when(context.getEndTime()).thenReturn(endTime); + reporter.onStart(context); + + when(result1.getTestClassName()).thenReturn(TEST_CLASS_NAME); + when(result1.getName()).thenReturn(TEST_METHOD_NAME); + when(result1.getStartMillis()).thenReturn(startTime); + when(result1.getEndMillis()).thenReturn(test1EndTime); + when(result1.getStatus()).thenReturn(JUnitXmlReporter.TestResult.SUCCESS); + reporter.onTestSuccess(result1); + + reporter.onFinish(coverageReport); + assertThat(outputFile.exists()).isTrue(); + String concatenatedFileContent = String.join("\n", Files.readAllLines(outputFile.toPath())); + + assertThat(concatenatedFileContent).contains("No coverage stats found"); + + outputFile.delete(); + } + + @Test + public void testGenerateReport_coverageReport_withCoverage() throws IOException { + String outputFileName = "test-report-with-coverage.xml"; + File subFolder = tempFolder.newFolder("subFolder"); + File outputFile = new File(subFolder.getAbsolutePath(), outputFileName); + JUnitXmlReporter reporter = new JUnitXmlReporter(outputFile.getAbsolutePath()); + long startTime = 100L; + long test1EndTime = startTime + 400; + long endTime = startTime + 900; + + CelCoverageIndex.CoverageReport coverageReport = + CelCoverageIndex.CoverageReport.builder() + .setNodes(10L) + .setCoveredNodes(10L) + .setBranches(10L) + .setCoveredBooleanOutcomes(5L) + .addUnencounteredNodes("Node 1") + .addUnencounteredNodes("Node 2") + .addUnencounteredBranches("Branch 1") + .addUnencounteredBranches("Branch 2") + .setGraphUrl("http://graphviz/url") + .build(); + + when(context.getSuiteName()).thenReturn(SUITE_NAME); + when(context.getStartTime()).thenReturn(startTime); + when(context.getEndTime()).thenReturn(endTime); + reporter.onStart(context); + + when(result1.getTestClassName()).thenReturn(TEST_CLASS_NAME); + when(result1.getName()).thenReturn(TEST_METHOD_NAME); + when(result1.getStartMillis()).thenReturn(startTime); + when(result1.getEndMillis()).thenReturn(test1EndTime); + when(result1.getStatus()).thenReturn(JUnitXmlReporter.TestResult.SUCCESS); + reporter.onTestSuccess(result1); + + reporter.onFinish(coverageReport); + assertThat(outputFile.exists()).isTrue(); + String concatenatedFileContent = String.join("\n", Files.readAllLines(outputFile.toPath())); + + assertThat(concatenatedFileContent) + .contains( + ""); + + outputFile.delete(); + } +} diff --git a/testing/src/test/java/dev/cel/testing/testrunner/LateFunctionBindingUserTest.java b/testing/src/test/java/dev/cel/testing/testrunner/LateFunctionBindingUserTest.java new file mode 100644 index 000000000..72992be3f --- /dev/null +++ b/testing/src/test/java/dev/cel/testing/testrunner/LateFunctionBindingUserTest.java @@ -0,0 +1,35 @@ +// 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.testing.testrunner; + +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.CelLateFunctionBindings; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +/** This test demonstrates the use case where the function bindings are provided at eval stage. */ +@RunWith(Parameterized.class) +public class LateFunctionBindingUserTest extends CelUserTestTemplate { + + public LateFunctionBindingUserTest() { + super( + CelTestContext.newBuilder() + .setCelLateFunctionBindings( + CelLateFunctionBindings.from( + CelFunctionBinding.from("foo_id", String.class, (String a) -> a.equals("foo")), + CelFunctionBinding.from("bar_id", String.class, (String a) -> a.equals("bar")))) + .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 new file mode 100644 index 000000000..112ef1f82 --- /dev/null +++ b/testing/src/test/java/dev/cel/testing/testrunner/TestRunnerLibraryTest.java @@ -0,0 +1,355 @@ +// 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.testing.testrunner; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableMap; +import com.google.protobuf.Any; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import com.google.testing.util.TestUtil; +import dev.cel.bundle.CelFactory; +import dev.cel.checker.ProtoTypeMask; +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; + +@RunWith(TestParameterInjector.class) +public class TestRunnerLibraryTest { + + @Before + public void setUp() { + System.setProperty("is_raw_expr", "False"); + } + + @Test + public void runPolicyTest_simpleBooleanOutput() throws Exception { + CelTestCase simpleOutputTestCase = + CelTestCase.newBuilder() + .setName("simple_output_test_case") + .setDescription("simple_output_test_case_description") + .setOutput(CelTestSuite.CelTestSection.CelTestCase.Output.ofResultValue(false)) + .build(); + + TestRunnerLibrary.evaluateTestCase( + simpleOutputTestCase, + CelTestContext.newBuilder() + .setCelExpression( + CelExpressionSource.fromSource( + TestUtil.getSrcDir() + + "/google3/third_party/java/cel/testing/src/test/java/dev/cel/testing/testrunner/resources/empty_policy.yaml")) + .build()); + } + + @Test + public void triggerRunTest_evaluatePolicy_simpleBooleanOutput() throws Exception { + CelTestCase simpleOutputTestCase = + CelTestCase.newBuilder() + .setName("simple_output_test_case") + .setDescription("simple_output_test_case_description") + .setOutput(CelTestSuite.CelTestSection.CelTestCase.Output.ofResultValue(false)) + .build(); + + TestRunnerLibrary.runTest( + simpleOutputTestCase, + CelTestContext.newBuilder() + .setCelExpression( + CelExpressionSource.fromSource( + TestUtil.getSrcDir() + + "/google3/third_party/java/cel/testing/src/test/java/dev/cel/testing/testrunner/resources/empty_policy.yaml")) + .build()); + } + + @Test + public void triggerRunTest_evaluateRawExpr_simpleBooleanOutput() throws Exception { + CelTestCase simpleOutputTestCase = + CelTestCase.newBuilder() + .setName("simple_output_test_case") + .setDescription("simple_output_test_case_description") + .setOutput(CelTestSuite.CelTestSection.CelTestCase.Output.ofResultValue(true)) + .build(); + + TestRunnerLibrary.runTest( + simpleOutputTestCase, + CelTestContext.newBuilder() + .setCelExpression(CelExpressionSource.fromRawExpr("1 > 0")) + .build()); + } + + @Test + public void runPolicyTest_outputMismatch_failureAssertion() throws Exception { + CelTestCase simpleOutputTestCase = + CelTestCase.newBuilder() + .setName("output_mismatch_test_case") + .setDescription("output_mismatch_test_case_description") + .setOutput(CelTestSuite.CelTestSection.CelTestCase.Output.ofResultValue(true)) + .build(); + + AssertionError thrown = + assertThrows( + AssertionError.class, + () -> + TestRunnerLibrary.evaluateTestCase( + simpleOutputTestCase, + CelTestContext.newBuilder() + .setCelExpression( + CelExpressionSource.fromSource( + TestUtil.getSrcDir() + + "/google3/third_party/java/cel/testing/src/test/java/dev/cel/testing/testrunner/resources/empty_policy.yaml")) + .build())); + + assertThat(thrown).hasMessageThat().contains("modified: value.bool_value: true -> false"); + } + + @Test + public void runPolicyTest_evaluatedContextExprNotProtoMessage_failure() throws Exception { + CelTestCase simpleOutputTestCase = + CelTestCase.newBuilder() + .setName("output_mismatch_test_case") + .setDescription("output_mismatch_test_case_description") + .setInput(CelTestSuite.CelTestSection.CelTestCase.Input.ofContextExpr("1 > 2")) + .setOutput(CelTestSuite.CelTestSection.CelTestCase.Output.ofResultValue(false)) + .build(); + + IllegalArgumentException thrown = + assertThrows( + IllegalArgumentException.class, + () -> + TestRunnerLibrary.evaluateTestCase( + simpleOutputTestCase, + CelTestContext.newBuilder() + .setCelExpression( + CelExpressionSource.fromSource( + TestUtil.getSrcDir() + + "/google3/third_party/java/cel/testing/src/test/java/dev/cel/testing/testrunner/resources/empty_policy.yaml")) + .setCel( + CelFactory.standardCelBuilder() + .addFileTypes(TestAllTypes.getDescriptor().getFile()) + .addProtoTypeMasks( + ProtoTypeMask.ofAllFields( + TestAllTypes.getDescriptor().getFullName()) + .withFieldsAsVariableDeclarations()) + .build()) + .build())); + + assertThat(thrown) + .hasMessageThat() + .contains("Context expression must evaluate to a proto message."); + } + + @Test + public void runPolicyTest_evaluationError_failureAssertion() throws Exception { + CelTestCase simpleOutputTestCase = + CelTestCase.newBuilder() + .setName("evaluation_error_test_case") + .setDescription("evaluation_error_test_case_description") + .setOutput(CelTestSuite.CelTestSection.CelTestCase.Output.ofResultValue(false)) + .build(); + + AssertionError thrown = + assertThrows( + AssertionError.class, + () -> + TestRunnerLibrary.evaluateTestCase( + simpleOutputTestCase, + CelTestContext.newBuilder() + .setVariableBindings(ImmutableMap.of("x", 1L)) + .setCelExpression( + CelExpressionSource.fromSource( + TestUtil.getSrcDir() + + "/google3/third_party/java/cel/testing/src/test/java/dev/cel/testing/testrunner/resources/eval_error_policy.yaml")) + .setCel(CelFactory.standardCelBuilder().addVar("x", SimpleType.INT).build()) + .build())); + + assertThat(thrown) + .hasMessageThat() + .contains( + "Error: Evaluation failed for test case: evaluation_error_test_case." + + " Error: evaluation error: / by zero"); + } + + @Test + public void runExpressionTest_outputMismatch_failureAssertion() throws Exception { + System.setProperty( + "cel_expr", + TestUtil.getSrcDir() + + "/google3/third_party/java/cel/testing/src/test/java/dev/cel/testing/testrunner/output.textproto"); + + CelTestCase simpleOutputTestCase = + CelTestCase.newBuilder() + .setName("output_mismatch_test_case") + .setDescription("output_mismatch_test_case_description") + .setOutput(CelTestSuite.CelTestSection.CelTestCase.Output.ofResultValue(true)) + .build(); + + AssertionError thrown = + assertThrows( + AssertionError.class, + () -> + TestRunnerLibrary.runTest( + simpleOutputTestCase, + CelTestContext.newBuilder() + .setCelExpression( + CelExpressionSource.fromSource( + TestUtil.getSrcDir() + + "/google3/third_party/java/cel/testing/src/test/java/dev/cel/testing/testrunner/output.textproto")) + .setCel(CelFactory.standardCelBuilder().build()) + .build())); + + assertThat(thrown).hasMessageThat().contains("modified: value.bool_value: true -> false"); + } + + @Test + public void runTest_illegalFileType_failure() throws Exception { + CelTestCase simpleOutputTestCase = + CelTestCase.newBuilder() + .setName("illegal_file_type_test_case") + .setDescription("illegal_file_type_test_case_description") + .setOutput(CelTestSuite.CelTestSection.CelTestCase.Output.ofNoOutput()) + .build(); + + IllegalArgumentException thrown = + assertThrows( + IllegalArgumentException.class, + () -> + TestRunnerLibrary.runTest( + simpleOutputTestCase, + CelTestContext.newBuilder() + .setCelExpression(CelExpressionSource.fromSource("output.txt")) + .build())); + + assertThat(thrown).hasMessageThat().contains("Unsupported expression file type: output.txt"); + } + + @Test + public void runTest_missingProtoDescriptors_failure() throws Exception { + CelTestCase simpleOutputTestCase = + CelTestCase.newBuilder() + .setName("missing_file_descriptor_set_path_test_case") + .setDescription("missing_file_descriptor_set_path_test_case_description") + .setInput( + CelTestSuite.CelTestSection.CelTestCase.Input.ofContextMessage( + Any.pack(TestAllTypes.getDefaultInstance()))) + .setOutput(CelTestSuite.CelTestSection.CelTestCase.Output.ofNoOutput()) + .build(); + + IllegalArgumentException thrown = + assertThrows( + IllegalArgumentException.class, + () -> + TestRunnerLibrary.runTest( + simpleOutputTestCase, + CelTestContext.newBuilder() + .setCelExpression(CelExpressionSource.fromRawExpr("true")) + .build())); + + assertThat(thrown) + .hasMessageThat() + .contains("Proto descriptors or type registry are required for unpacking Any messages"); + } + + @Test + public void triggerRunTest_evaluateRawExpr_withCoverage() throws Exception { + CelCoverageIndex celCoverageIndex = new CelCoverageIndex(); + CelTestCase simpleOutputTestCase = + CelTestCase.newBuilder() + .setName("simple_output_test_case") + .setDescription("simple_output_test_case_description") + .setOutput(CelTestSuite.CelTestSection.CelTestCase.Output.ofResultValue(true)) + .build(); + + TestRunnerLibrary.runTest( + simpleOutputTestCase, + CelTestContext.newBuilder() + .setCelExpression(CelExpressionSource.fromRawExpr("1 > 0")) + .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/java/dev/cel/testing/testrunner/UserTest.java b/testing/src/test/java/dev/cel/testing/testrunner/UserTest.java new file mode 100644 index 000000000..6f4c838dd --- /dev/null +++ b/testing/src/test/java/dev/cel/testing/testrunner/UserTest.java @@ -0,0 +1,39 @@ +// 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.testing.testrunner; + +import dev.cel.bundle.CelFactory; +import dev.cel.common.types.MapType; +import dev.cel.common.types.SimpleType; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +/** + * A sample test class for demonstrating the use of the CEL test runner when no config file is + * provided. + */ +@RunWith(Parameterized.class) +public class UserTest extends CelUserTestTemplate { + + public UserTest() { + super( + CelTestContext.newBuilder() + .setCel( + CelFactory.standardCelBuilder() + .addVar("resource", MapType.create(SimpleType.STRING, SimpleType.ANY)) + .build()) + .build()); + } +} diff --git a/testing/src/test/java/dev/cel/testing/testrunner/resources/empty_policy.yaml b/testing/src/test/java/dev/cel/testing/testrunner/resources/empty_policy.yaml new file mode 100644 index 000000000..ea6863dfd --- /dev/null +++ b/testing/src/test/java/dev/cel/testing/testrunner/resources/empty_policy.yaml @@ -0,0 +1,17 @@ +# 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. + +rule: + match: + - output: 'false' diff --git a/testing/src/test/java/dev/cel/testing/testrunner/resources/eval_error_policy.yaml b/testing/src/test/java/dev/cel/testing/testrunner/resources/eval_error_policy.yaml new file mode 100644 index 000000000..e97295c84 --- /dev/null +++ b/testing/src/test/java/dev/cel/testing/testrunner/resources/eval_error_policy.yaml @@ -0,0 +1,20 @@ +# 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: "evaluation_error_policy" +rule: + match: + - condition: "x/0 == 0" + output: "false" + - output: "true" diff --git a/testing/src/test/resources/environment/BUILD.bazel b/testing/src/test/resources/environment/BUILD.bazel new file mode 100644 index 000000000..30bd3c8c1 --- /dev/null +++ b/testing/src/test/resources/environment/BUILD.bazel @@ -0,0 +1,49 @@ +package( + default_applicable_licenses = [ + "//:license", + ], + default_testonly = True, + default_visibility = [ + "//testing/environment:__pkg__", + ], +) + +filegroup( + name = "dump_env", + srcs = ["dump_env.yaml"], +) + +filegroup( + name = "extended_env", + srcs = ["extended_env.yaml"], +) + +filegroup( + name = "all_extensions", + srcs = ["all_extensions.yaml"], +) + +filegroup( + name = "primitive_variables", + srcs = ["primitive_variables.yaml"], +) + +filegroup( + name = "custom_functions", + srcs = ["custom_functions.yaml"], +) + +filegroup( + name = "library_subset_env", + srcs = ["subset_env.yaml"], +) + +filegroup( + name = "proto2_message_variables", + srcs = ["proto2_message_variables.yaml"], +) + +filegroup( + name = "proto3_message_variables", + srcs = ["proto3_message_variables.yaml"], +) diff --git a/testing/src/test/resources/environment/all_extensions.yaml b/testing/src/test/resources/environment/all_extensions.yaml new file mode 100644 index 000000000..623a0fb5b --- /dev/null +++ b/testing/src/test/resources/environment/all_extensions.yaml @@ -0,0 +1,24 @@ +# 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: "all-extensions" +extensions: +- name: "bindings" +- name: "encoders" +- name: "lists" +- name: "math" +- name: "optional" +- name: "protos" +- name: "sets" +- name: "strings" diff --git a/testing/src/test/resources/environment/custom_functions.yaml b/testing/src/test/resources/environment/custom_functions.yaml new file mode 100644 index 000000000..9aab223ae --- /dev/null +++ b/testing/src/test/resources/environment/custom_functions.yaml @@ -0,0 +1,31 @@ +# 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: "custom-functions" +functions: +- name: "isEmpty" + overloads: + - id: "string_isEmpty" + target: + type_name: "string" + return: + type_name: "bool" + - id: "list_isEmpty" + target: + type_name: "list" + params: + - type_name: "T" + is_type_param: true + return: + type_name: "bool" \ No newline at end of file diff --git a/testing/src/test/resources/environment/dump_env.yaml b/testing/src/test/resources/environment/dump_env.yaml new file mode 100644 index 000000000..a5ed23753 --- /dev/null +++ b/testing/src/test/resources/environment/dump_env.yaml @@ -0,0 +1,131 @@ +# 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: dump_env +description: dump_env description +container: + name: test.container + abbreviations: + - abbr1.Abbr1 + - abbr2.Abbr2 + aliases: + - alias: alias1 + qualified_name: qual.name1 + - alias: alias2 + qualified_name: qual.name2 +extensions: +- name: bindings +- name: encoders +- name: lists +- name: math +- name: optional +- name: protos +- name: sets +- name: strings + version: 1 +variables: +- name: request + type_name: google.rpc.context.AttributeContext.Request +- name: map_var + type_name: map + params: + - type_name: string + - type_name: string +functions: +- name: getOrDefault + overloads: + - id: getOrDefault_key_value + target: + type_name: map + params: + - type_name: K + is_type_param: true + - type_name: V + is_type_param: true + args: + - type_name: K + is_type_param: true + - type_name: V + is_type_param: true + 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 + target: + type_name: google.protobuf.Int64Value + args: + - type_name: int + return: + type_name: int +stdlib: + disabled: true + disable_macros: true + include_macros: + - exists + - has + include_functions: + - name: _!=_ + - name: _+_ + 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 new file mode 100644 index 000000000..9fc2d511d --- /dev/null +++ b/testing/src/test/resources/environment/extended_env.yaml @@ -0,0 +1,66 @@ +# 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: "extended-env" +container: "cel.expr" +extensions: +- name: "optional" + version: "2" +- name: "math" + version: "latest" +variables: +- 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" + description: |- + determines whether a list is empty, + or a string has no characters + overloads: + - id: "wrapper_string_isEmpty" + examples: + - "''.isEmpty() // true" + target: + type_name: "google.protobuf.StringValue" + return: + type_name: "bool" + - id: "list_isEmpty" + examples: + - "[].isEmpty() // true" + - "[1].isEmpty() // false" + target: + type_name: "list" + params: + - type_name: "T" + is_type_param: true + return: + type_name: "bool" +features: +- name: cel.feature.macro_call_tracking + enabled: true +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 +#- name: cel.validator.matches +#- name: cel.validator.timestamp +#- name: cel.validator.nesting_comprehension_limit +# config: +# limit: 2 diff --git a/testing/src/test/resources/environment/primitive_variables.yaml b/testing/src/test/resources/environment/primitive_variables.yaml new file mode 100644 index 000000000..f92f704a8 --- /dev/null +++ b/testing/src/test/resources/environment/primitive_variables.yaml @@ -0,0 +1,28 @@ +# 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: "primitive-variables" +variables: +- name: "bool_var" + type_name: "bool" +- name: "bytes_var" + type_name: "bytes" +- name: "double_var" + type_name: "double" +- name: "int_var" + type_name: "int" +- name: "uint_var" + type_name: "uint" +- name: "str_var" + type_name: "string" \ No newline at end of file diff --git a/testing/src/test/resources/environment/proto2_message_variables.yaml b/testing/src/test/resources/environment/proto2_message_variables.yaml new file mode 100644 index 000000000..ac06fb1a2 --- /dev/null +++ b/testing/src/test/resources/environment/proto2_message_variables.yaml @@ -0,0 +1,18 @@ +# 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: "proto2-message-variables" +variables: +- name: "proto2" + type_name: "cel.expr.conformance.proto2.TestAllTypes" diff --git a/testing/src/test/resources/environment/proto3_message_variables.yaml b/testing/src/test/resources/environment/proto3_message_variables.yaml new file mode 100644 index 000000000..12f39c7fa --- /dev/null +++ b/testing/src/test/resources/environment/proto3_message_variables.yaml @@ -0,0 +1,18 @@ +# 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: "proto3-message-variables" +variables: +- name: "proto3" + type_name: "cel.expr.conformance.proto3.TestAllTypes" diff --git a/testing/src/test/resources/environment/subset_env.yaml b/testing/src/test/resources/environment/subset_env.yaml new file mode 100644 index 000000000..f721a1426 --- /dev/null +++ b/testing/src/test/resources/environment/subset_env.yaml @@ -0,0 +1,39 @@ +# 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: "subset-env" +stdlib: + exclude_macros: + - map + - filter + exclude_functions: + - name: "_+_" + overloads: + - id: add_bytes + - id: add_list + - id: add_string + - name: "matches" + - name: "timestamp" + overloads: + - id: "string_to_timestamp" + - name: "duration" + overloads: + - id: "string_to_duration" +variables: +- name: "x" + type_name: "int" +- name: "y" + type_name: "double" +- name: "z" + type_name: "uint" diff --git a/testing/src/test/resources/expressions/BUILD.bazel b/testing/src/test/resources/expressions/BUILD.bazel new file mode 100644 index 000000000..3d3415718 --- /dev/null +++ b/testing/src/test/resources/expressions/BUILD.bazel @@ -0,0 +1,16 @@ +package( + default_applicable_licenses = [ + "//:license", + ], + default_testonly = True, + default_visibility = [ + "//testing:__pkg__", + ], +) + +exports_files( + srcs = glob([ + "**/*.cel", + "**/*.textproto", + ]), +) diff --git a/testing/src/test/resources/expressions/coverage_test_case/partial_coverage_tests.textproto b/testing/src/test/resources/expressions/coverage_test_case/partial_coverage_tests.textproto new file mode 100644 index 000000000..61b57637c --- /dev/null +++ b/testing/src/test/resources/expressions/coverage_test_case/partial_coverage_tests.textproto @@ -0,0 +1,26 @@ +# proto-file: google3/third_party/cel/spec/proto/cel/expr/conformance/test/suite.proto +# proto-message: cel.expr.conformance.test.TestSuite + +name: "partial_coverage_tests" +description: "Tests for partial coverage." +sections: { + name: "simple_map_operations" + description: "Tests for map operations." + tests: { + name: "literal_and_sum" + description: "Test that a map can be created and values can be accessed." + input: { + key: "x" + value { value { int64_value: 2 } } + } + input { + key: "y" + value { value { int64_value: 2 } } + } + output { + result_value { + bool_value: true + } + } + } +} diff --git a/testing/src/test/resources/expressions/coverage_test_case/simple_expression.cel b/testing/src/test/resources/expressions/coverage_test_case/simple_expression.cel new file mode 100644 index 000000000..0e8256613 --- /dev/null +++ b/testing/src/test/resources/expressions/coverage_test_case/simple_expression.cel @@ -0,0 +1 @@ +{'sum': x + y, 'literal': 3}.sum == 3 || x == y \ No newline at end of file diff --git a/testing/src/test/resources/expressions/coverage_test_case/tests.textproto b/testing/src/test/resources/expressions/coverage_test_case/tests.textproto new file mode 100644 index 000000000..fefee8d6c --- /dev/null +++ b/testing/src/test/resources/expressions/coverage_test_case/tests.textproto @@ -0,0 +1,26 @@ +# proto-file: google3/third_party/cel/spec/proto/cel/expr/conformance/test/suite.proto +# proto-message: cel.expr.conformance.test.TestSuite + +name: "coverage_tests" +description: "Tests for coverage." +sections: { + name: "simple_map_operations" + description: "Tests for map operations." + tests: { + name: "literal_and_sum" + description: "Test that a map can be created and values can be accessed." + input: { + key: "x" + value { value { int64_value: 1 } } + } + input { + key: "y" + value { value { int64_value: 2 } } + } + output { + result_value { + bool_value: true + } + } + } +} diff --git a/testing/src/test/resources/expressions/simple_test_case/simple_expression.cel b/testing/src/test/resources/expressions/simple_test_case/simple_expression.cel new file mode 100644 index 000000000..e120ebc26 --- /dev/null +++ b/testing/src/test/resources/expressions/simple_test_case/simple_expression.cel @@ -0,0 +1 @@ +2 + 2 == 4 \ No newline at end of file diff --git a/testing/src/test/resources/expressions/simple_test_case/tests.textproto b/testing/src/test/resources/expressions/simple_test_case/tests.textproto new file mode 100644 index 000000000..2a431d62f --- /dev/null +++ b/testing/src/test/resources/expressions/simple_test_case/tests.textproto @@ -0,0 +1,18 @@ +# proto-file: google3/third_party/cel/spec/proto/cel/expr/conformance/test/suite.proto +# proto-message: cel.expr.conformance.test.TestSuite + +name: "expr_value_output_tests" +description: "Value as expected output" +sections { + name: "basic value" + description: "Basic value" + tests { + name: "basic value test" + description: "Basic value test" + output { + result_value { + bool_value: true + } + } + } +} \ No newline at end of file diff --git a/testing/src/test/resources/policy/BUILD.bazel b/testing/src/test/resources/policy/BUILD.bazel new file mode 100644 index 000000000..fc9971704 --- /dev/null +++ b/testing/src/test/resources/policy/BUILD.bazel @@ -0,0 +1,28 @@ +package( + default_applicable_licenses = [ + "//:license", + ], + default_testonly = True, + default_visibility = [ + "//testing:__subpackages__", + ], +) + +filegroup( + name = "policy_yaml_files", + srcs = glob([ + "**/*.yaml", + "**/*.celpolicy", + "**/*.baseline", + "**/*.textproto", + ]), +) + +exports_files( + srcs = glob([ + "**/*.yaml", + "**/*.baseline", + "**/*.celpolicy", + "**/*.textproto", + ]), +) 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/context_pb/context_msg_tests.textproto b/testing/src/test/resources/policy/context_pb/context_msg_tests.textproto new file mode 100644 index 000000000..aa5d5051c --- /dev/null +++ b/testing/src/test/resources/policy/context_pb/context_msg_tests.textproto @@ -0,0 +1,23 @@ +# proto-file: google3/third_party/cel/spec/proto/cel/expr/conformance/test/suite.proto +# proto-message: cel.expr.conformance.test.TestSuite + +name: "context_msg_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_message { + [type.googleapis.com/cel.expr.conformance.proto3.TestAllTypes] { + single_int32: 10 + } + } + } + output { + result_expr: "optional.none()" + } + } +} diff --git a/testing/src/test/resources/policy/custom_variable_bindings/config.yaml b/testing/src/test/resources/policy/custom_variable_bindings/config.yaml new file mode 100644 index 000000000..83b3685c3 --- /dev/null +++ b/testing/src/test/resources/policy/custom_variable_bindings/config.yaml @@ -0,0 +1,22 @@ +# 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: "custom_variable_bindings" +container: "cel.expr.conformance.proto2" +variables: + - name: "spec" + type: + type_name: "cel.expr.conformance.proto2.TestAllTypes" +extensions: +- name: "protos" \ No newline at end of file diff --git a/testing/src/test/resources/policy/custom_variable_bindings/policy.yaml b/testing/src/test/resources/policy/custom_variable_bindings/policy.yaml new file mode 100644 index 000000000..7a94eebd2 --- /dev/null +++ b/testing/src/test/resources/policy/custom_variable_bindings/policy.yaml @@ -0,0 +1,20 @@ +# 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: "custom_variable_bindings" +rule: + match: + - condition: proto.getExt(spec, cel.expr.conformance.proto2.int32_ext) > 0 + output: "true" + - output: "false" \ No newline at end of file diff --git a/testing/src/test/resources/policy/custom_variable_bindings/tests.yaml b/testing/src/test/resources/policy/custom_variable_bindings/tests.yaml new file mode 100644 index 000000000..dd924141c --- /dev/null +++ b/testing/src/test/resources/policy/custom_variable_bindings/tests.yaml @@ -0,0 +1,26 @@ +# 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: "custom_variable_bindings" +description: "Tests for custom variable bindings." +sections: + - name: "custom_variable_bindings" + description: "Tests for custom variable bindings." + tests: + - name: "true_by_default" + description: "Tests that the custom variable bindings are set to true by default." + # The input for this test is configured programmatically in the test + # class with a value of 1 (see CustomVariableBindingsUserTest.java). + output: + expr: "true" \ 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/expr_value_output/policy.yaml b/testing/src/test/resources/policy/expr_value_output/policy.yaml new file mode 100644 index 000000000..9a49ab808 --- /dev/null +++ b/testing/src/test/resources/policy/expr_value_output/policy.yaml @@ -0,0 +1,19 @@ +# 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: "value_as_output" +rule: + match: + - condition: 2 + 2 == 4 + output: "true" diff --git a/testing/src/test/resources/policy/expr_value_output/tests.textproto b/testing/src/test/resources/policy/expr_value_output/tests.textproto new file mode 100644 index 000000000..2a431d62f --- /dev/null +++ b/testing/src/test/resources/policy/expr_value_output/tests.textproto @@ -0,0 +1,18 @@ +# proto-file: google3/third_party/cel/spec/proto/cel/expr/conformance/test/suite.proto +# proto-message: cel.expr.conformance.test.TestSuite + +name: "expr_value_output_tests" +description: "Value as expected output" +sections { + name: "basic value" + description: "Basic value" + tests { + name: "basic value test" + description: "Basic value test" + output { + result_value { + bool_value: true + } + } + } +} \ No newline at end of file diff --git a/testing/src/test/resources/policy/import/expected_errors.baseline b/testing/src/test/resources/policy/import/expected_errors.baseline new file mode 100644 index 000000000..b88402b8c --- /dev/null +++ b/testing/src/test/resources/policy/import/expected_errors.baseline @@ -0,0 +1,6 @@ +ERROR: import/policy.yaml:19:7: Error configuring import: invalid qualified name: punc.Import!, wanted name of the form 'qualified.name' + | punc.Import! + | ......^ +ERROR: import/policy.yaml:20:12: Error configuring import: invalid qualified name: bad import, wanted name of the form 'qualified.name' + | - name: "bad import" + | ...........^ diff --git a/testing/src/test/resources/policy/incompatible_outputs/expected_errors.baseline b/testing/src/test/resources/policy/incompatible_outputs/expected_errors.baseline new file mode 100644 index 000000000..be370847f --- /dev/null +++ b/testing/src/test/resources/policy/incompatible_outputs/expected_errors.baseline @@ -0,0 +1,6 @@ +ERROR: incompatible_outputs/policy.yaml:19:16: incompatible output types: block has output type optional_type(string), but previous outputs have type bool + | output: "true" + | ...............^ +ERROR: incompatible_outputs/policy.yaml:21:16: incompatible output types: block has output type optional_type(string), but previous outputs have type bool + | output: "'false'" + | ...............^ diff --git a/testing/src/test/resources/policy/late_function_binding/config.yaml b/testing/src/test/resources/policy/late_function_binding/config.yaml new file mode 100644 index 000000000..423db293b --- /dev/null +++ b/testing/src/test/resources/policy/late_function_binding/config.yaml @@ -0,0 +1,34 @@ +# 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: "late_function_binding" +functions: + - name: "foo" + overloads: + - id: 'foo_id' + args: + - type_name: 'string' + return: + type_name: 'bool' + - name: "bar" + overloads: + - id: 'bar_id' + args: + - type_name: 'string' + return: + type_name: 'bool' +variables: + - name: "a" + type: + type_name: 'string' \ No newline at end of file diff --git a/testing/src/test/resources/policy/late_function_binding/policy.celpolicy b/testing/src/test/resources/policy/late_function_binding/policy.celpolicy new file mode 100644 index 000000000..15cc503e9 --- /dev/null +++ b/testing/src/test/resources/policy/late_function_binding/policy.celpolicy @@ -0,0 +1,20 @@ +# 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: "late_function_binding" +rule: + match: + - condition: foo(a) || bar(a) + output: "true" + - output: "false" diff --git a/testing/src/test/resources/policy/late_function_binding/tests.textproto b/testing/src/test/resources/policy/late_function_binding/tests.textproto new file mode 100644 index 000000000..38f53501b --- /dev/null +++ b/testing/src/test/resources/policy/late_function_binding/tests.textproto @@ -0,0 +1,41 @@ +# proto-file: google3/third_party/cel/spec/proto/cel/expr/conformance/test/suite.proto +# proto-message: cel.expr.conformance.test.TestSuite + +name: "late_function_binding_tests" +description: "Tests for late function binding." +sections: { + name: "late_function_binding_tests_section" + description: "Tests for late function binding." + tests: { + name: "true_by_default" + description: "Test that the default value of a late function binding is true." + input: { + key: "a" + value: { + expr: "'foo'" + } + } + output: { + result_value: { + bool_value: true + } + } + } + tests: { + name: "false_by_default" + description: "Test that the default value of a late function binding is false." + input: { + key: "a" + value: { + value: { + string_value: "baz" + } + } + } + output { + result_value { + bool_value: false + } + } + } +} diff --git a/testing/src/test/resources/policy/late_function_binding/tests.yaml b/testing/src/test/resources/policy/late_function_binding/tests.yaml new file mode 100644 index 000000000..cfdaa7ca3 --- /dev/null +++ b/testing/src/test/resources/policy/late_function_binding/tests.yaml @@ -0,0 +1,34 @@ +# 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: "late_function_binding_tests" +description: "Tests for late function binding." +sections: + - name: "late_function_binding_tests_section" + description: "Tests for late function binding." + tests: + - name: "true_by_default" + description: "Test that the default value of a late function binding is true." + input: + a: + expr: "'foo'" + output: + value: true + - name: "false_by_default" + description: "Test that the default value of a late function binding is false." + input: + a: + value: "baz" + output: + value: false \ No newline at end of file diff --git a/testing/src/test/resources/policy/nested_rule/config.textproto b/testing/src/test/resources/policy/nested_rule/config.textproto new file mode 100644 index 000000000..94fff8898 --- /dev/null +++ b/testing/src/test/resources/policy/nested_rule/config.textproto @@ -0,0 +1,14 @@ +# proto-file: google3/google/api/expr/conformance/env_config.proto +# proto-message: google.api.expr.conformance.Environment + +declarations: { + name: "resource" + ident { + type { + map_type { + key_type { primitive: STRING } + value_type { well_known: ANY } + } + } + } +} \ No newline at end of file diff --git a/testing/src/test/resources/policy/nested_rule/eval_error_config.yaml b/testing/src/test/resources/policy/nested_rule/eval_error_config.yaml new file mode 100644 index 000000000..2c4863027 --- /dev/null +++ b/testing/src/test/resources/policy/nested_rule/eval_error_config.yaml @@ -0,0 +1,30 @@ +# 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: "nested_rule" +functions: + - name: "foo" + overloads: + - id: 'foo_id' + args: + - type_name: 'string' + return: + type_name: 'bool' +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_rule/eval_error_policy.yaml b/testing/src/test/resources/policy/nested_rule/eval_error_policy.yaml new file mode 100644 index 000000000..2f1233ea1 --- /dev/null +++ b/testing/src/test/resources/policy/nested_rule/eval_error_policy.yaml @@ -0,0 +1,38 @@ +# 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_eval_error +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: foo(resource.origin) && 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/eval_error_tests.textproto b/testing/src/test/resources/policy/nested_rule/eval_error_tests.textproto new file mode 100644 index 000000000..f841f660b --- /dev/null +++ b/testing/src/test/resources/policy/nested_rule/eval_error_tests.textproto @@ -0,0 +1,37 @@ +# proto-file: google3/third_party/cel/spec/proto/cel/expr/conformance/test/suite.proto +# proto-message: cel.expr.conformance.test.TestSuite +# This testcase is used to test the eval error output in the test runner which +# fails because the function is declared in the compiler but not in the runtime. + +name: "eval_error_tests" +description: "Nested rule conformance tests with eval errors" +sections { + name: "permitted" + description: "Permitted nested rule" + tests { + name: "valid_origin" + description: "Valid origin" + input { + key: "resource" + value { + value { + object_value { + [type.googleapis.com/google.protobuf.Struct] { + fields { + key: "origin" + value { string_value: "uk" } + } + } + } + } + } + } + output { + eval_error { + errors { + message: "evaluation error: No matching overload for function 'foo'. Overload candidates: foo_id" + } + } + } + } +} diff --git a/testing/src/test/resources/policy/nested_rule/eval_error_tests.yaml b/testing/src/test/resources/policy/nested_rule/eval_error_tests.yaml new file mode 100644 index 000000000..7c38c6b9c --- /dev/null +++ b/testing/src/test/resources/policy/nested_rule/eval_error_tests.yaml @@ -0,0 +1,29 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "eval_error" +description: evaluation error tests +sections: + - name: "eval_error" + description: "Tests for evaluation errors" + tests: + - name: "eval_error_no_matching_overload" + description: "No matching overload for function" + input: + resource: + value: + origin: "uk" + output: + error_set: + - "evaluation error: No matching overload for function 'foo'. Overload candidates: foo_id" diff --git a/testing/src/test/resources/policy/nested_rule/testrunner_unknown_output_tests.yaml b/testing/src/test/resources/policy/nested_rule/testrunner_unknown_output_tests.yaml new file mode 100644 index 000000000..ac916c7c6 --- /dev/null +++ b/testing/src/test/resources/policy/nested_rule/testrunner_unknown_output_tests.yaml @@ -0,0 +1,25 @@ +# 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." + output: + unknown: + - 4 \ No newline at end of file diff --git a/testing/src/test/resources/policy/nested_rule/tests.textproto b/testing/src/test/resources/policy/nested_rule/tests.textproto new file mode 100644 index 000000000..701175394 --- /dev/null +++ b/testing/src/test/resources/policy/nested_rule/tests.textproto @@ -0,0 +1,69 @@ +# proto-file: google3/google/api/expr/conformance/test_suite.proto +# proto-message: google.api.expr.conformance.TestSuite + +description: "Nested rule conformance tests" + +sections { + name: "valid" + tests { + name: "restricted_origin" + input { + key: "resource" + value { + expr_value { + object_value { + [type.googleapis.com/google.protobuf.Struct] { + fields { + key: "origin" + value { string_value: "ir" } + } + } + } + } + } + } + expr: "{'banned': true}" + } + tests { + name: "by_default" + input { + key: "resource" + value { + expr_value { + object_value { + [type.googleapis.com/google.protobuf.Struct] { + fields { + key: "origin" + value { string_value: "'de'" } + } + } + } + } + } + } + expr: "{'banned': true}" + } +} + +sections { + name: "permitted" + tests { + name: "valid_origin" + input { + key: "resource" + value { + expr_value { + object_value { + [type.googleapis.com/google.protobuf.Struct] { + fields { + key: "origin" + value { string_value: "uk" } + } + } + } + } + } + } + expr: "{'banned': false}" + } +} \ No newline at end of file diff --git a/testing/src/test/resources/policy/protoextension_value_as_input/tests.textproto b/testing/src/test/resources/policy/protoextension_value_as_input/tests.textproto new file mode 100644 index 000000000..455ac4bdc --- /dev/null +++ b/testing/src/test/resources/policy/protoextension_value_as_input/tests.textproto @@ -0,0 +1,31 @@ +# proto-file: google3/third_party/cel/spec/proto/cel/expr/conformance/test/suite.proto +# proto-message: cel.expr.conformance.test.TestSuite + +# The input binding is not used for evaluation, but rather to ensure +# extension registry generation and support for `Any` typed inputs with +# extensions. + +name: "protoextension_value_as_input" +description: "Valid proto extension value as input" +sections { + name: "valid" + description: "Valid proto extension value as input" + tests { + name: "value_extension_input" + input { + key: "spec" + value { + value { + object_value { + [type.googleapis.com/cel.expr.conformance.proto2.TestAllTypes] { + [cel.expr.conformance.proto2.Proto2ExtensionScopedMessage.message_scoped_nested_ext]: {} + } + } + } + } + } + output { + result_expr: "true" + } + } +} \ No newline at end of file diff --git a/testing/src/test/resources/policy/syntax/expected_errors.baseline b/testing/src/test/resources/policy/syntax/expected_errors.baseline new file mode 100644 index 000000000..dd6af277e --- /dev/null +++ b/testing/src/test/resources/policy/syntax/expected_errors.baseline @@ -0,0 +1,12 @@ +ERROR: syntax/policy.yaml:19:51: mismatched input 'resource' expecting {'==', '!=', 'in', '<', '<=', '>=', '>', '&&', '||', '[', ')', '.', '-', '?', '+', '*', '/', '%%'} + | expression: "variables.want.filter(l, !(lin resource.labels))" + | ..................................................^ +ERROR: syntax/policy.yaml:19:67: extraneous input ')' expecting + | expression: "variables.want.filter(l, !(lin resource.labels))" + | ..................................................................^ +ERROR: syntax/policy.yaml:21:27: mismatched input '2' expecting {'}', ','} + | expression: "{1:305 2:569}" + | ..........................^ +ERROR: syntax/policy.yaml:24:33: extraneous input ']' expecting + | output: "variables.missing]" + | ................................^ diff --git a/testing/src/test/resources/policy/undeclared_reference/expected_errors.baseline b/testing/src/test/resources/policy/undeclared_reference/expected_errors.baseline new file mode 100644 index 000000000..4b887180e --- /dev/null +++ b/testing/src/test/resources/policy/undeclared_reference/expected_errors.baseline @@ -0,0 +1,6 @@ +ERROR: undeclared_reference/policy.yaml:19:19: undeclared reference to 'spec' (in container '') + | expression: spec.labels + | ..................^ +ERROR: undeclared_reference/policy.yaml:23:29: undeclared reference to 'format' (in container '') + | "invalid: %s".format([variables.val]) + | ............................^ diff --git a/testing/src/test/resources/policy/unreachable/expected_errors.baseline b/testing/src/test/resources/policy/unreachable/expected_errors.baseline new file mode 100644 index 000000000..768f0eeb1 --- /dev/null +++ b/testing/src/test/resources/policy/unreachable/expected_errors.baseline @@ -0,0 +1,6 @@ +ERROR: unreachable/policy.yaml:36:9: Match creates unreachable outputs + | - output: | + | ........^ +ERROR: unreachable/policy.yaml:28:7: Rule creates unreachable outputs + | match: + | ......^ \ No newline at end of file diff --git a/testing/src/test/resources/protos/BUILD.bazel b/testing/src/test/resources/protos/BUILD.bazel new file mode 100644 index 000000000..1fac2e1f0 --- /dev/null +++ b/testing/src/test/resources/protos/BUILD.bazel @@ -0,0 +1,120 @@ +load("@com_google_protobuf//bazel:java_proto_library.bzl", "java_proto_library") +load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") +load("//:java_lite_proto_cel_library.bzl", "java_lite_proto_cel_library") +load("//:java_lite_proto_cel_library_impl.bzl", "java_lite_proto_cel_library_impl") + +package( + default_applicable_licenses = [ + "//:license", + ], + default_testonly = True, + default_visibility = [ + "//testing/protos:__pkg__", + ], +) + +proto_library( + name = "single_file_proto", + srcs = ["single_file.proto"], +) + +java_proto_library( + name = "single_file_java_proto", + tags = [ + ], + 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 = [ + "multi_file.proto", + ], + deps = [":single_file_proto"], +) + +proto_library( + name = "message_with_enum_proto", + srcs = ["message_with_enum.proto"], +) + +java_proto_library( + name = "message_with_enum_java_proto", + deps = [":message_with_enum_proto"], +) + +# Test only. java_proto_library supports generating a jar with multiple proto deps, +# so we must test this case as well for lite descriptors. +# buildifier: disable=LANG_proto_library-single-deps +java_proto_library( + name = "multi_file_java_proto", + deps = [ + ":multi_file_proto", + ":single_file_proto", + ], +) + +java_lite_proto_cel_library( + name = "multi_file_cel_java_proto_lite", + deps = [ + ":multi_file_proto", + ":single_file_proto", + ], +) + +java_lite_proto_cel_library( + name = "test_all_types_cel_java_proto2_lite", + deps = ["@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_proto"], +) + +java_lite_proto_cel_library( + name = "test_all_types_cel_java_proto3_lite", + deps = ["@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_proto"], +) + +# The below targets exist to exercise lite descriptor tests against the full protobuf runtime (thus the overridden java_proto_library_dep). +# Use cases outside CEL should follow the example above. + +java_lite_proto_cel_library_impl( + name = "multi_file_cel_java_proto", + java_descriptor_class_suffix = "CelDescriptor", + java_proto_library_dep = ":multi_file_java_proto", + deps = [ + ":multi_file_proto", + ":single_file_proto", + ], +) + +java_lite_proto_cel_library_impl( + name = "test_all_types_cel_java_proto2", + java_descriptor_class_suffix = "CelDescriptor", + java_proto_library_dep = "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto", + deps = ["@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_proto"], +) + +java_lite_proto_cel_library_impl( + name = "test_all_types_cel_java_proto3", + java_descriptor_class_suffix = "CelDescriptor", + java_proto_library_dep = "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", + deps = ["@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_proto"], +) + +java_lite_proto_cel_library_impl( + name = "message_with_enum_cel_java_proto", + java_descriptor_class_suffix = "CelDescriptor", + java_proto_library_dep = ":message_with_enum_java_proto", + deps = [":message_with_enum_proto"], +) diff --git a/common/src/test/resources/single_file.proto b/testing/src/test/resources/protos/message_with_enum.proto similarity index 74% rename from common/src/test/resources/single_file.proto rename to testing/src/test/resources/protos/message_with_enum.proto index 0fcf270a1..c495eea16 100644 --- a/common/src/test/resources/single_file.proto +++ b/testing/src/test/resources/protos/message_with_enum.proto @@ -1,4 +1,4 @@ -// Copyright 2022 Google LLC +// 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. @@ -16,14 +16,16 @@ syntax = "proto3"; package dev.cel.testing.testdata; +option java_multiple_files = true; option java_package = "dev.cel.testing.testdata"; -option java_outer_classname = "SingleFileProto"; +option java_outer_classname = "MessageWithEnumProto"; -message SingleFile { - message Path { - repeated string fragments = 1; - } +message MessageWithEnum { + SimpleEnum simple_enum = 1; +} - string name = 1; - Path path = 2; +enum SimpleEnum { + FOO = 0; + BAR = 1; + BAZ = 2; } diff --git a/common/src/test/resources/multi_file.proto b/testing/src/test/resources/protos/multi_file.proto similarity index 84% rename from common/src/test/resources/multi_file.proto rename to testing/src/test/resources/protos/multi_file.proto index 91499294f..309278e66 100644 --- a/common/src/test/resources/multi_file.proto +++ b/testing/src/test/resources/protos/multi_file.proto @@ -16,6 +16,8 @@ syntax = "proto3"; package dev.cel.testing.testdata; +import "testing/src/test/resources/protos/single_file.proto"; + option java_multiple_files = true; option java_package = "dev.cel.testing.testdata"; option java_outer_classname = "MultiFileProto"; @@ -26,9 +28,11 @@ message MultiFile { repeated string fragments = 1; } - string name = 1; - Path path = 2; + string name = 2; + Path path = 3; } - repeated File files = 1; + repeated File files = 4; + + SingleFile nested_single_file = 5; } diff --git a/testing/src/test/resources/protos/single_file.proto b/testing/src/test/resources/protos/single_file.proto new file mode 100644 index 000000000..8306cc16c --- /dev/null +++ b/testing/src/test/resources/protos/single_file.proto @@ -0,0 +1,38 @@ +// 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. + +edition = "2024"; + +package dev.cel.testing.testdata; + +option java_package = "dev.cel.testing.testdata"; + +message SingleFile { + message Path { + repeated string fragments = 1; + } + + string name = 1; + Path path = 2; + 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 new file mode 100644 index 000000000..043d62c88 --- /dev/null +++ b/testing/testrunner/BUILD.bazel @@ -0,0 +1,118 @@ +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") +load("@rules_java//java:defs.bzl", "java_library") + +package( + default_applicable_licenses = ["//:license"], + default_testonly = True, + default_visibility = ["//visibility:public"], +) + +java_library( + name = "cel_user_test_template", + exports = ["//testing/src/main/java/dev/cel/testing/testrunner:cel_user_test_template"], +) + +java_library( + name = "junit_xml_reporter", + visibility = ["//:internal"], + exports = ["//testing/src/main/java/dev/cel/testing/testrunner:junit_xml_reporter"], +) + +java_library( + name = "test_runner_library", + exports = ["//testing/src/main/java/dev/cel/testing/testrunner:test_runner_library"], +) + +java_library( + name = "test_executor", + exports = ["//testing/src/main/java/dev/cel/testing/testrunner:test_executor"], +) + +java_library( + name = "cel_test_suite", + exports = ["//testing/src/main/java/dev/cel/testing/testrunner:cel_test_suite"], +) + +java_library( + name = "cel_test_context", + exports = ["//testing/src/main/java/dev/cel/testing/testrunner:cel_test_context"], +) + +java_library( + name = "cel_test_suite_yaml_parser", + visibility = ["//:internal"], + exports = ["//testing/src/main/java/dev/cel/testing/testrunner:cel_test_suite_yaml_parser"], +) + +java_library( + name = "cel_test_suite_text_proto_parser", + visibility = ["//:internal"], + exports = ["//testing/src/main/java/dev/cel/testing/testrunner:cel_test_suite_text_proto_parser"], +) + +java_library( + name = "cel_test_suite_exception", + exports = ["//testing/src/main/java/dev/cel/testing/testrunner:cel_test_suite_exception"], +) + +java_library( + name = "result_matcher", + exports = ["//testing/src/main/java/dev/cel/testing/testrunner:result_matcher"], +) + +java_library( + name = "default_result_matcher", + exports = ["//testing/src/main/java/dev/cel/testing/testrunner:default_result_matcher"], +) + +alias( + name = "test_runner_binary", + actual = "//testing/src/main/java/dev/cel/testing/testrunner:test_runner_binary", +) + +java_library( + name = "annotations", + exports = ["//testing/src/main/java/dev/cel/testing/testrunner:annotations"], +) + +exports_files( + srcs = ["run_testrunner_binary.sh"], +) + +java_library( + name = "registry_utils", + visibility = ["//:internal"], + exports = ["//testing/src/main/java/dev/cel/testing/testrunner:registry_utils"], +) + +java_library( + name = "class_loader_utils", + visibility = ["//:internal"], + exports = ["//testing/src/main/java/dev/cel/testing/utils:class_loader_utils"], +) + +java_library( + name = "proto_descriptor_utils", + visibility = ["//:internal"], + exports = ["//testing/src/main/java/dev/cel/testing/utils:proto_descriptor_utils"], +) + +java_library( + name = "cel_expression_source", + exports = ["//testing/src/main/java/dev/cel/testing/testrunner:cel_expression_source"], +) + +java_library( + name = "cel_coverage_index", + exports = ["//testing/src/main/java/dev/cel/testing/testrunner:cel_coverage_index"], +) + +bzl_library( + name = "cel_java_test", + srcs = ["cel_java_test.bzl"], + deps = [ + "@bazel_skylib//lib:paths", + "@rules_java//java:core_rules", + "@rules_proto//proto:defs", + ], +) diff --git a/testing/testrunner/cel_java_test.bzl b/testing/testrunner/cel_java_test.bzl new file mode 100644 index 000000000..d2dd796c0 --- /dev/null +++ b/testing/testrunner/cel_java_test.bzl @@ -0,0 +1,159 @@ +# 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. + +"""Rules for triggering the java impl of the CEL test runner.""" + +load("@rules_java//java:java_binary.bzl", "java_binary") +load("@rules_proto//proto:defs.bzl", "proto_descriptor_set") +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, + test_src, + is_raw_expr = False, + test_suite = "", + filegroup = "", + config = "", + deps = [], + proto_deps = [], + enable_coverage = False, + test_data_path = "", + data = []): + """Triggers the Java impl of the CEL test runner. + + 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. + + Args: + 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. + """ + + jvm_flags = [] + + # 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 + + # Add filegroup if provided + if filegroup: + resolved_data.append(filegroup) + + 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 _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_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 = descriptor_set_name, + deps = proto_deps, + ) + java_proto_library( + name = descriptor_set_name + "_java_proto", + deps = proto_deps, + ) + + 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 = resolved_data, + jvm_flags = jvm_flags, + testonly = True, + main_class = "dev.cel.testing.testrunner.TestRunnerBinary", + runtime_deps = [test_src], + deps = [ + "//testing/testrunner:test_executor", + "@maven//:com_google_guava_guava", + "@bazel_tools//tools/java/runfiles:runfiles", + ] + resolved_deps, + ) + + # Generate the execution shell test + sh_test( + name = name, + tags = ["nomsan"], + srcs = ["//testing/testrunner:run_testrunner_binary.sh"], + data = [":{}_test_runner_binary".format(name)], + args = [name], + ) diff --git a/testing/testrunner/run_testrunner_binary.sh b/testing/testrunner/run_testrunner_binary.sh new file mode 100755 index 000000000..551a41cc9 --- /dev/null +++ b/testing/testrunner/run_testrunner_binary.sh @@ -0,0 +1,28 @@ +#!/bin/bash +# Path: //third_party/java/cel/testing/testrunner/run_testrunner_binary.sh + +die() { + echo "ERROR: $*" >&2 + exit 1 +} + +# Get the name passed to the sh_test rule from the arguments. +# This is the name passed to the sh_test macro, +# and it's also the name of the java_binary +NAME=$1 + +# Find the test_runner_binary executable (wrapper script), using the NAME +TEST_RUNNER_BINARY="$(find -L "${TEST_SRCDIR}" -name "${NAME}_test_runner_binary" -type f -executable)" + +# This would have also worked but the above is more strict in finding +# a file that's a symlink to the executable. +# TEST_RUNNER_BINARY="$(find "${TEST_SRCDIR}" -name "${NAME}_test_runner_binary")" + +if [ -z "$TEST_RUNNER_BINARY" ]; then + die "Test runner binary (wrapper script) $TEST_RUNNER_BINARY not found in runfiles." +fi + +#Execute the symlink to the executable. +"$TEST_RUNNER_BINARY" || die "Some or all the tests failed." + +echo "PASS" diff --git a/validator/BUILD.bazel b/validator/BUILD.bazel index a5afcbd23..fe40153fc 100644 --- a/validator/BUILD.bazel +++ b/validator/BUILD.bazel @@ -1,3 +1,5 @@ +load("@rules_java//java:defs.bzl", "java_library") + package( default_applicable_licenses = ["//:license"], default_visibility = ["//visibility:public"], diff --git a/validator/src/main/java/dev/cel/validator/BUILD.bazel b/validator/src/main/java/dev/cel/validator/BUILD.bazel index a3d92d75d..95cf65f8a 100644 --- a/validator/src/main/java/dev/cel/validator/BUILD.bazel +++ b/validator/src/main/java/dev/cel/validator/BUILD.bazel @@ -1,3 +1,5 @@ +load("@rules_java//java:defs.bzl", "java_library") + package( default_applicable_licenses = ["//:license"], default_visibility = [ @@ -35,7 +37,7 @@ java_library( ], deps = [ ":ast_validator", - "//common", + "//common:cel_ast", "//common:compiler_common", "@maven//:com_google_errorprone_error_prone_annotations", ], @@ -52,7 +54,7 @@ java_library( ":ast_validator", ":validator_builder", "//bundle:cel", - "//common", + "//common:cel_ast", "//common:compiler_common", "//common/navigation", "@maven//:com_google_guava_guava", @@ -66,9 +68,12 @@ java_library( ], deps = [ "//bundle:cel", - "//common", + "//common:cel_source", "//common:compiler_common", + "//common:source_location", "//common/navigation", + "//common/types", + "//common/types:type_providers", "@maven//:com_google_guava_guava", ], ) diff --git a/validator/src/main/java/dev/cel/validator/CelAstValidator.java b/validator/src/main/java/dev/cel/validator/CelAstValidator.java index ca316019a..ae919696f 100644 --- a/validator/src/main/java/dev/cel/validator/CelAstValidator.java +++ b/validator/src/main/java/dev/cel/validator/CelAstValidator.java @@ -19,15 +19,24 @@ import dev.cel.common.CelIssue; import dev.cel.common.CelIssue.Severity; import dev.cel.common.CelSource; +import dev.cel.common.CelSourceLocation; import dev.cel.common.navigation.CelNavigableAst; +import dev.cel.common.types.CelType; +import dev.cel.common.types.SimpleType; +import java.util.Optional; /** Public interface for performing a single, custom validation on an AST. */ public interface CelAstValidator { void validate(CelNavigableAst navigableAst, Cel cel, IssuesFactory issuesFactory); + /** Enforces a specific expected result type during validation, if set. */ + default CelType expectedResultType() { + return SimpleType.DYN; + } + /** Factory for populating issues while performing AST validation. */ - public final class IssuesFactory { + final class IssuesFactory { private final ImmutableList.Builder issuesBuilder; private final CelNavigableAst navigableAst; @@ -53,12 +62,17 @@ public void addInfo(long exprId, String message) { private void add(long exprId, String message, Severity severity) { CelSource source = navigableAst.getAst().getSource(); - int position = source.getPositionsMap().get(exprId); + int position = Optional.ofNullable(source.getPositionsMap().get(exprId)).orElse(-1); + CelSourceLocation sourceLocation = CelSourceLocation.NONE; + if (position >= 0) { + sourceLocation = source.getOffsetLocation(position).get(); + } issuesBuilder.add( CelIssue.newBuilder() + .setExprId(exprId) .setSeverity(severity) .setMessage(message) - .setSourceLocation(source.getOffsetLocation(position).get()) + .setSourceLocation(sourceLocation) .build()); } diff --git a/validator/src/main/java/dev/cel/validator/CelValidatorImpl.java b/validator/src/main/java/dev/cel/validator/CelValidatorImpl.java index 971868c82..9561799ca 100644 --- a/validator/src/main/java/dev/cel/validator/CelValidatorImpl.java +++ b/validator/src/main/java/dev/cel/validator/CelValidatorImpl.java @@ -48,9 +48,11 @@ public CelValidationResult validate(CelAbstractSyntaxTree ast) { ImmutableList.Builder issueBuilder = ImmutableList.builder(); for (CelAstValidator validator : astValidators) { + Cel celEnv = this.cel.toCelBuilder().setResultType(validator.expectedResultType()).build(); + CelNavigableAst navigableAst = CelNavigableAst.fromAst(ast); IssuesFactory issuesFactory = new IssuesFactory(navigableAst); - validator.validate(navigableAst, cel, issuesFactory); + validator.validate(navigableAst, celEnv, issuesFactory); issueBuilder.addAll(issuesFactory.getIssues()); } diff --git a/validator/src/main/java/dev/cel/validator/validators/AstDepthLimitValidator.java b/validator/src/main/java/dev/cel/validator/validators/AstDepthLimitValidator.java new file mode 100644 index 000000000..c255624e5 --- /dev/null +++ b/validator/src/main/java/dev/cel/validator/validators/AstDepthLimitValidator.java @@ -0,0 +1,69 @@ +// 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. + +package dev.cel.validator.validators; + +import static com.google.common.base.Preconditions.checkArgument; + +import dev.cel.bundle.Cel; +import dev.cel.common.navigation.CelNavigableAst; +import dev.cel.validator.CelAstValidator; + +/** Enforces a compiled AST to stay below the configured depth limit. */ +public final class AstDepthLimitValidator implements CelAstValidator { + + // Protobuf imposes a default parse-depth limit of 100. We set it to half here because navigable + // expr does not include operands in the depth calculation. + // As an example, an expression 'x.y' has a depth of 2 in NavigableExpr, but the ParsedExpr has a + // depth of 4 as illustrated below: + // + // expr { + // id: 2 + // select_expr { + // operand { + // id: 1 + // ident_expr { + // name: "x" + // } + // } + // field: "y" + // } + // } + static final int DEFAULT_DEPTH_LIMIT = 50; + + public static final AstDepthLimitValidator DEFAULT = newInstance(DEFAULT_DEPTH_LIMIT); + private final int maxDepth; + + /** + * Constructs a new instance of {@link AstDepthLimitValidator} with the configured maxDepth as its + * limit. + */ + public static AstDepthLimitValidator newInstance(int maxDepth) { + checkArgument(maxDepth > 0); + return new AstDepthLimitValidator(maxDepth); + } + + @Override + public void validate(CelNavigableAst navigableAst, Cel cel, IssuesFactory issuesFactory) { + if (navigableAst.getRoot().height() >= maxDepth) { + issuesFactory.addError( + navigableAst.getRoot().id(), + String.format("AST's depth exceeds the configured limit: %s.", maxDepth)); + } + } + + private AstDepthLimitValidator(int maxDepth) { + this.maxDepth = maxDepth; + } +} diff --git a/validator/src/main/java/dev/cel/validator/validators/BUILD.bazel b/validator/src/main/java/dev/cel/validator/validators/BUILD.bazel index e9c07fb10..93a2c0d28 100644 --- a/validator/src/main/java/dev/cel/validator/validators/BUILD.bazel +++ b/validator/src/main/java/dev/cel/validator/validators/BUILD.bazel @@ -1,3 +1,5 @@ +load("@rules_java//java:defs.bzl", "java_library") + package( default_applicable_licenses = ["//:license"], default_visibility = [ @@ -15,7 +17,8 @@ java_library( ], deps = [ ":literal_validator", - "@maven//:com_google_protobuf_protobuf_java", + "//common/types", + "//common/types:type_providers", ], ) @@ -28,7 +31,8 @@ java_library( ], deps = [ ":literal_validator", - "@maven//:com_google_protobuf_protobuf_java", + "//common/types", + "//common/types:type_providers", ], ) @@ -57,7 +61,7 @@ java_library( ], deps = [ "//bundle:cel", - "//common", + "//common:cel_ast", "//common/ast", "//common/navigation", "//common/types:cel_types", @@ -67,6 +71,38 @@ java_library( ], ) +java_library( + name = "ast_depth_limit_validator", + srcs = [ + "AstDepthLimitValidator.java", + ], + tags = [ + ], + deps = [ + "//bundle:cel", + "//common/navigation", + "//validator:ast_validator", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "comprehension_nesting_limit_validator", + srcs = [ + "ComprehensionNestingLimitValidator.java", + ], + tags = [ + ], + deps = [ + "//bundle:cel", + "//common/ast", + "//common/navigation", + "//common/navigation:common", + "//validator:ast_validator", + "@maven//:com_google_guava_guava", + ], +) + java_library( name = "literal_validator", srcs = [ @@ -74,13 +110,17 @@ java_library( ], tags = [ ], - visibility = ["//visibility:private"], deps = [ "//bundle:cel", + "//common:cel_ast", + "//common:cel_source", + "//common:compiler_common", "//common/ast", "//common/ast:expr_factory", - "//common/ast:expr_util", "//common/navigation", + "//common/types:type_providers", + "//runtime", "//validator:ast_validator", + "@maven//:com_google_errorprone_error_prone_annotations", ], ) diff --git a/validator/src/main/java/dev/cel/validator/validators/ComprehensionNestingLimitValidator.java b/validator/src/main/java/dev/cel/validator/validators/ComprehensionNestingLimitValidator.java new file mode 100644 index 000000000..55dadacc5 --- /dev/null +++ b/validator/src/main/java/dev/cel/validator/validators/ComprehensionNestingLimitValidator.java @@ -0,0 +1,85 @@ +// 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.validator.validators; + +import static com.google.common.base.Preconditions.checkArgument; + +import dev.cel.bundle.Cel; +import dev.cel.common.ast.CelExpr.ExprKind; +import dev.cel.common.navigation.CelNavigableAst; +import dev.cel.common.navigation.CelNavigableExpr; +import dev.cel.common.navigation.TraversalOrder; +import dev.cel.validator.CelAstValidator; + +/** + * Checks that the nesting depth of comprehensions does not exceed the configured limit. Nesting + * occurs when a comprehension is used in the range expression or the body of a comprehension. + * + *

Trivial comprehensions (comprehensions over an empty range) do not count towards the limit. + */ +public final class ComprehensionNestingLimitValidator implements CelAstValidator { + private final int nestingLimit; + + /** + * Constructs a new instance of {@link ComprehensionNestingLimitValidator} with the configured + * maxNesting as its limit. A limit of 0 means no comprehensions are allowed. + */ + public static ComprehensionNestingLimitValidator newInstance(int maxNesting) { + checkArgument(maxNesting >= 0); + return new ComprehensionNestingLimitValidator(maxNesting); + } + + @Override + public void validate(CelNavigableAst navigableAst, Cel cel, IssuesFactory issuesFactory) { + navigableAst + .getRoot() + .allNodes(TraversalOrder.PRE_ORDER) + .filter(node -> node.getKind().equals(ExprKind.Kind.COMPREHENSION)) + .filter(node -> nestingLevel(node) > nestingLimit) + .forEach( + node -> + issuesFactory.addError( + node.id(), + String.format( + "comprehension nesting exceeds the configured limit: %s.", nestingLimit))); + } + + private static boolean isTrivialComprehension(CelNavigableExpr node) { + return (node.expr().comprehension().iterRange().getKind().equals(ExprKind.Kind.LIST) + && node.expr().comprehension().iterRange().list().elements().isEmpty()) + || (node.expr().comprehension().iterRange().getKind().equals(ExprKind.Kind.MAP) + && node.expr().comprehension().iterRange().map().entries().isEmpty()); + } + + private static int nestingLevel(CelNavigableExpr node) { + if (isTrivialComprehension(node)) { + return 0; + } + int count = 1; + while (node.parent().isPresent()) { + CelNavigableExpr parent = node.parent().get(); + + if (parent.getKind().equals(ExprKind.Kind.COMPREHENSION) && !isTrivialComprehension(parent)) { + count++; + } + node = parent; + } + return count; + } + + private ComprehensionNestingLimitValidator(int maxNesting) { + this.nestingLimit = maxNesting; + } +} diff --git a/validator/src/main/java/dev/cel/validator/validators/DurationLiteralValidator.java b/validator/src/main/java/dev/cel/validator/validators/DurationLiteralValidator.java index 349374ca2..0670893b3 100644 --- a/validator/src/main/java/dev/cel/validator/validators/DurationLiteralValidator.java +++ b/validator/src/main/java/dev/cel/validator/validators/DurationLiteralValidator.java @@ -14,14 +14,17 @@ package dev.cel.validator.validators; -import com.google.protobuf.Duration; +import dev.cel.common.types.CelType; +import dev.cel.common.types.SimpleType; +import java.time.Duration; /** DurationLiteralValidator ensures that duration literal arguments are valid. */ public final class DurationLiteralValidator extends LiteralValidator { public static final DurationLiteralValidator INSTANCE = - new DurationLiteralValidator("duration", Duration.class); + new DurationLiteralValidator("duration", Duration.class, SimpleType.DURATION); - private DurationLiteralValidator(String functionName, Class expectedResultType) { - super(functionName, expectedResultType); + private DurationLiteralValidator( + String functionName, Class expectedJavaType, CelType expectedResultType) { + super(functionName, expectedJavaType, expectedResultType); } } diff --git a/validator/src/main/java/dev/cel/validator/validators/HomogeneousLiteralValidator.java b/validator/src/main/java/dev/cel/validator/validators/HomogeneousLiteralValidator.java index 45ada6ca3..f83be31c6 100644 --- a/validator/src/main/java/dev/cel/validator/validators/HomogeneousLiteralValidator.java +++ b/validator/src/main/java/dev/cel/validator/validators/HomogeneousLiteralValidator.java @@ -19,7 +19,7 @@ import dev.cel.bundle.Cel; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.ast.CelExpr; -import dev.cel.common.ast.CelExpr.CelCreateMap; +import dev.cel.common.ast.CelExpr.CelMap; import dev.cel.common.ast.CelExpr.ExprKind.Kind; import dev.cel.common.navigation.CelNavigableAst; import dev.cel.common.navigation.CelNavigableExpr; @@ -38,16 +38,16 @@ public final class HomogeneousLiteralValidator implements CelAstValidator { private final ImmutableSet exemptFunctions; /** - * Construct a new instance of {@link HomogeneousLiteralValidator}. This validator will not for - * functions in {@code exemptFunctions}. + * Construct a new instance of {@link HomogeneousLiteralValidator}. This validator will not run + * for functions in {@code exemptFunctions}. */ public static HomogeneousLiteralValidator newInstance(Iterable exemptFunctions) { return new HomogeneousLiteralValidator(exemptFunctions); } /** - * Construct a new instance of {@link HomogeneousLiteralValidator}. This validator will not for - * functions in {@code exemptFunctions}. + * Construct a new instance of {@link HomogeneousLiteralValidator}. This validator will not run + * for functions in {@code exemptFunctions}. */ public static HomogeneousLiteralValidator newInstance(String... exemptFunctions) { return newInstance(Arrays.asList(exemptFunctions)); @@ -58,16 +58,14 @@ public void validate(CelNavigableAst navigableAst, Cel cel, IssuesFactory issues navigableAst .getRoot() .allNodes() - .filter( - node -> - node.getKind().equals(Kind.CREATE_LIST) || node.getKind().equals(Kind.CREATE_MAP)) + .filter(node -> node.getKind().equals(Kind.LIST) || node.getKind().equals(Kind.MAP)) .filter(node -> !isExemptFunction(node)) .map(CelNavigableExpr::expr) .forEach( expr -> { - if (expr.exprKind().getKind().equals(Kind.CREATE_LIST)) { + if (expr.exprKind().getKind().equals(Kind.LIST)) { validateList(navigableAst.getAst(), issuesFactory, expr); - } else if (expr.exprKind().getKind().equals(Kind.CREATE_MAP)) { + } else if (expr.exprKind().getKind().equals(Kind.MAP)) { validateMap(navigableAst.getAst(), issuesFactory, expr); } }); @@ -75,8 +73,8 @@ public void validate(CelNavigableAst navigableAst, Cel cel, IssuesFactory issues private void validateList(CelAbstractSyntaxTree ast, IssuesFactory issuesFactory, CelExpr expr) { CelType previousType = null; - HashSet optionalIndices = new HashSet<>(expr.createList().optionalIndices()); - ImmutableList elements = expr.createList().elements(); + HashSet optionalIndices = new HashSet<>(expr.list().optionalIndices()); + ImmutableList elements = expr.list().elements(); for (int i = 0; i < elements.size(); i++) { CelExpr element = elements.get(i); CelType currentType = ast.getType(element.id()).get(); @@ -96,7 +94,7 @@ private void validateList(CelAbstractSyntaxTree ast, IssuesFactory issuesFactory private void validateMap(CelAbstractSyntaxTree ast, IssuesFactory issuesFactory, CelExpr expr) { CelType previousKeyType = null; CelType previousValueType = null; - for (CelCreateMap.Entry entry : expr.createMap().entries()) { + for (CelMap.Entry entry : expr.map().entries()) { CelType currentKeyType = ast.getType(entry.key().id()).get(); CelType currentValueType = ast.getType(entry.value().id()).get(); if (entry.optionalEntry()) { diff --git a/validator/src/main/java/dev/cel/validator/validators/LiteralValidator.java b/validator/src/main/java/dev/cel/validator/validators/LiteralValidator.java index 0848870cc..6d48c4f0c 100644 --- a/validator/src/main/java/dev/cel/validator/validators/LiteralValidator.java +++ b/validator/src/main/java/dev/cel/validator/validators/LiteralValidator.java @@ -14,13 +14,18 @@ package dev.cel.validator.validators; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import dev.cel.bundle.Cel; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelSource; +import dev.cel.common.CelValidationException; import dev.cel.common.ast.CelExpr; import dev.cel.common.ast.CelExpr.ExprKind.Kind; import dev.cel.common.ast.CelExprFactory; -import dev.cel.common.ast.CelExprUtil; import dev.cel.common.navigation.CelNavigableAst; import dev.cel.common.navigation.CelNavigableExpr; +import dev.cel.common.types.CelType; +import dev.cel.runtime.CelEvaluationException; import dev.cel.validator.CelAstValidator; /** @@ -28,15 +33,23 @@ * call by evaluating it and ensuring that no errors are thrown (example: duration / timestamp * literals). */ -abstract class LiteralValidator implements CelAstValidator { +public abstract class LiteralValidator implements CelAstValidator { private final String functionName; - private final Class expectedResultType; + private final Class expectedJavaType; + private final CelType expectedResultType; - protected LiteralValidator(String functionName, Class expectedResultType) { + protected LiteralValidator( + String functionName, Class expectedJavaType, CelType expectedResultType) { this.functionName = functionName; + this.expectedJavaType = expectedJavaType; this.expectedResultType = expectedResultType; } + @Override + public CelType expectedResultType() { + return expectedResultType; + } + @Override public void validate(CelNavigableAst navigableAst, Cel cel, IssuesFactory issuesFactory) { CelExprFactory exprFactory = CelExprFactory.newInstance(); @@ -57,7 +70,7 @@ public void validate(CelNavigableAst navigableAst, Cel cel, IssuesFactory issues CelExpr callExpr = exprFactory.newGlobalCall(functionName, exprFactory.newConstant(expr.constant())); try { - CelExprUtil.evaluateExpr(cel, callExpr, expectedResultType); + evaluateExpr(cel, callExpr, expectedJavaType); } catch (Exception e) { issuesFactory.addError( expr.id(), @@ -66,4 +79,21 @@ public void validate(CelNavigableAst navigableAst, Cel cel, IssuesFactory issues } }); } + + @CanIgnoreReturnValue + private static Object evaluateExpr(Cel cel, CelExpr expr, Class expectedJavaType) + throws CelValidationException, CelEvaluationException { + CelAbstractSyntaxTree ast = + CelAbstractSyntaxTree.newParsedAst(expr, CelSource.newBuilder().build()); + ast = cel.check(ast).getAst(); + Object result = cel.createProgram(ast).eval(); + + if (!expectedJavaType.isInstance(result)) { + throw new IllegalStateException( + String.format( + "Expected %s type but got %s instead", + expectedJavaType.getName(), result.getClass().getName())); + } + return result; + } } diff --git a/validator/src/main/java/dev/cel/validator/validators/TimestampLiteralValidator.java b/validator/src/main/java/dev/cel/validator/validators/TimestampLiteralValidator.java index 4f6a5209e..da3630548 100644 --- a/validator/src/main/java/dev/cel/validator/validators/TimestampLiteralValidator.java +++ b/validator/src/main/java/dev/cel/validator/validators/TimestampLiteralValidator.java @@ -14,14 +14,17 @@ package dev.cel.validator.validators; -import com.google.protobuf.Timestamp; +import dev.cel.common.types.CelType; +import dev.cel.common.types.SimpleType; +import java.time.Instant; /** TimestampLiteralValidator ensures that timestamp literal arguments are valid. */ public final class TimestampLiteralValidator extends LiteralValidator { public static final TimestampLiteralValidator INSTANCE = - new TimestampLiteralValidator("timestamp", Timestamp.class); + new TimestampLiteralValidator("timestamp", Instant.class, SimpleType.TIMESTAMP); - private TimestampLiteralValidator(String functionName, Class expectedResultType) { - super(functionName, expectedResultType); + private TimestampLiteralValidator( + String functionName, Class expectedJavaType, CelType expectedResultType) { + super(functionName, expectedJavaType, expectedResultType); } } diff --git a/validator/src/test/java/dev/cel/validator/BUILD.bazel b/validator/src/test/java/dev/cel/validator/BUILD.bazel index f6f94f625..d7384bf74 100644 --- a/validator/src/test/java/dev/cel/validator/BUILD.bazel +++ b/validator/src/test/java/dev/cel/validator/BUILD.bazel @@ -1,6 +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,7 +14,7 @@ java_library( "//bundle:cel", "//common:compiler_common", "//compiler", - "//parser", + "//parser:parser_factory", "//runtime", "//validator", "//validator:validator_builder", diff --git a/validator/src/test/java/dev/cel/validator/validators/AstDepthLimitValidatorTest.java b/validator/src/test/java/dev/cel/validator/validators/AstDepthLimitValidatorTest.java new file mode 100644 index 000000000..acc2544c7 --- /dev/null +++ b/validator/src/test/java/dev/cel/validator/validators/AstDepthLimitValidatorTest.java @@ -0,0 +1,112 @@ +// 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. + +package dev.cel.validator.validators; + +import static com.google.common.truth.Truth.assertThat; +import static dev.cel.common.CelFunctionDecl.newFunctionDeclaration; +import static dev.cel.common.CelOverloadDecl.newGlobalOverload; +import static dev.cel.validator.validators.AstDepthLimitValidator.DEFAULT_DEPTH_LIMIT; +import static org.junit.Assert.assertThrows; + +import dev.cel.expr.CheckedExpr; +import com.google.protobuf.ByteString; +import com.google.protobuf.ExtensionRegistryLite; +import com.google.protobuf.InvalidProtocolBufferException; +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.CelIssue.Severity; +import dev.cel.common.CelProtoAbstractSyntaxTree; +import dev.cel.common.CelValidationResult; +import dev.cel.common.types.SimpleType; +import dev.cel.validator.CelValidator; +import dev.cel.validator.CelValidatorFactory; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public class AstDepthLimitValidatorTest { + + private static final Cel CEL = + CelFactory.standardCelBuilder() + .addVar("x", SimpleType.DYN) + .addFunctionDeclarations( + newFunctionDeclaration( + "f", newGlobalOverload("f_int64", SimpleType.INT, SimpleType.INT))) + .build(); + + private static final CelValidator CEL_VALIDATOR = + CelValidatorFactory.standardCelValidatorBuilder(CEL) + .addAstValidators(AstDepthLimitValidator.DEFAULT) + .build(); + + private enum DefaultTestCase { + NESTED_SELECTS( + "x.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y"), + NESTED_CALCS( + "0+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+35+36+37+38+39+40+41+42+43+44+45+46+47+48+49+50"), + NESTED_FUNCS( + "f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(0)))))))))))))))))))))))))))))))))))))))))))))))))))"); + + private final String expression; + + DefaultTestCase(String expression) { + this.expression = expression; + } + } + + @Test + public void astExceedsDefaultDepthLimit_populatesErrors(@TestParameter DefaultTestCase testCase) + throws Exception { + CelAbstractSyntaxTree ast = CEL.compile(testCase.expression).getAst(); + + CelValidationResult result = CEL_VALIDATOR.validate(ast); + + assertThat(result.hasError()).isTrue(); + assertThat(result.getAllIssues()).hasSize(1); + assertThat(result.getAllIssues().get(0).getSeverity()).isEqualTo(Severity.ERROR); + assertThat(result.getAllIssues().get(0).toDisplayString(ast.getSource())) + .contains("AST's depth exceeds the configured limit: 50."); + assertThrows(InvalidProtocolBufferException.class, () -> verifyProtoAstRoundTrips(ast)); + } + + @Test + public void astIsUnderDepthLimit_noErrors() throws Exception { + StringBuilder sb = new StringBuilder().append("x"); + for (int i = 0; i < DEFAULT_DEPTH_LIMIT - 1; i++) { + sb.append(".y"); + } + // Depth level of 49 + CelAbstractSyntaxTree ast = CEL.compile(sb.toString()).getAst(); + + CelValidationResult result = CEL_VALIDATOR.validate(ast); + + assertThat(result.hasError()).isFalse(); + assertThat(result.getAllIssues()).isEmpty(); + verifyProtoAstRoundTrips(ast); + } + + private void verifyProtoAstRoundTrips(CelAbstractSyntaxTree ast) throws Exception { + CheckedExpr checkedExpr = CelProtoAbstractSyntaxTree.fromCelAst(ast).toCheckedExpr(); + ByteString serialized = checkedExpr.toByteString(); + CheckedExpr deserializedCheckedExpr = + CheckedExpr.parseFrom(serialized, ExtensionRegistryLite.getEmptyRegistry()); + if (!checkedExpr.equals(deserializedCheckedExpr)) { + throw new IllegalStateException("Expected checked expressions to round trip!"); + } + } +} 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 eca252aa9..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,6 +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,23 +12,31 @@ java_library( deps = [ "//:java_truth", "//bundle:cel", - "//common", + "//common:cel_ast", "//common:compiler_common", "//common:options", + "//common:proto_ast", + "//common/internal:proto_time_utils", "//common/types", + "//extensions", "//extensions:optional_library", + "//parser:macro", "//runtime", + "//runtime:function_binding", "//validator", "//validator:validator_builder", + "//validator/validators:ast_depth_limit_validator", + "//validator/validators:comprehension_nesting_limit_validator", "//validator/validators:duration", "//validator/validators:homogeneous_literal", "//validator/validators:regex", "//validator/validators:timestamp", + "@cel_spec//proto/cel/expr:checked_java_proto", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", - "@maven//:com_google_protobuf_protobuf_java_util", "@maven//:com_google_testparameterinjector_test_parameter_injector", "@maven//:junit_junit", + "@maven_android//:com_google_protobuf_protobuf_javalite", ], ) diff --git a/validator/src/test/java/dev/cel/validator/validators/ComprehensionNestingLimitValidatorTest.java b/validator/src/test/java/dev/cel/validator/validators/ComprehensionNestingLimitValidatorTest.java new file mode 100644 index 000000000..ccf625d88 --- /dev/null +++ b/validator/src/test/java/dev/cel/validator/validators/ComprehensionNestingLimitValidatorTest.java @@ -0,0 +1,176 @@ +// 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.validator.validators; + +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.Cel; +import dev.cel.bundle.CelFactory; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelIssue.Severity; +import dev.cel.common.CelValidationException; +import dev.cel.common.CelValidationResult; +import dev.cel.common.types.SimpleType; +import dev.cel.extensions.CelExtensions; +import dev.cel.parser.CelStandardMacro; +import dev.cel.validator.CelValidator; +import dev.cel.validator.CelValidatorFactory; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public class ComprehensionNestingLimitValidatorTest { + + private static final Cel CEL = + CelFactory.standardCelBuilder() + .addCompilerLibraries( + CelExtensions.optional(), CelExtensions.comprehensions(), CelExtensions.bindings()) + .addRuntimeLibraries(CelExtensions.optional(), CelExtensions.comprehensions()) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addVar("x", SimpleType.DYN) + .build(); + + private static final CelValidator CEL_VALIDATOR = + CelValidatorFactory.standardCelValidatorBuilder(CEL) + .addAstValidators(ComprehensionNestingLimitValidator.newInstance(1)) + .build(); + + @Test + public void comprehensionNestingLimit_populatesErrors( + @TestParameter({ + "[1, 2, 3].map(x, [1, 2, 3].map(y, x + y))", + "[1, 2, 3].map(x, [1, 2, 3].map(y, x + y).size() > 0, x)", + "[1, 2, 3].all(x, [1, 2, 3].exists(y, x + y > 0))", + "[1, 2, 3].map(x, {x: [1, 2, 3].map(y, x + y)})", + "[1, 2, 3].exists(i, v, i < 3 && [1, 2, 3].all(j, v2, j < 3 && v2 > 0))", + "{1: 2}.all(k, {2: 3}.all(k2, k != k2))" + }) + String expr) + throws Exception { + CelAbstractSyntaxTree ast = CEL.compile(expr).getAst(); + + CelValidationResult result = CEL_VALIDATOR.validate(ast); + + assertThat(result.hasError()).isTrue(); + assertThat(result.getAllIssues()).hasSize(1); + assertThat(result.getAllIssues().get(0).getSeverity()).isEqualTo(Severity.ERROR); + assertThat(result.getAllIssues().get(0).toDisplayString(ast.getSource())) + .contains("comprehension nesting exceeds the configured limit: 1."); + } + + @Test + public void comprehensionNestingLimit_accumulatesErrors( + @TestParameter({ + "[1, 2, 3].map(x, [1, 2, 3].map(y, [1, 2, 3].map(z, x + y + z)))", + }) + String expr) + throws Exception { + CelAbstractSyntaxTree ast = CEL.compile(expr).getAst(); + + CelValidationResult result = CEL_VALIDATOR.validate(ast); + + assertThat(result.hasError()).isTrue(); + assertThat(result.getAllIssues()).hasSize(2); + } + + @Test + public void comprehensionNestingLimit_limitConfigurable( + @TestParameter({ + "[1, 2, 3].map(x, [1, 2, 3].map(y, x + y))", + "[1, 2, 3].map(x, [1, 2, 3].map(y, x + y).size() > 0, x)", + "[1, 2, 3].all(x, [1, 2, 3].exists(y, x + y > 0))", + "[1, 2, 3].map(x, {x: [1, 2, 3].map(y, x + y)})", + "[1, 2, 3].exists(i, v, i < 3 && [1, 2, 3].all(j, v2, j < 3 && v2 > 0))", + "{1: 2}.all(k, {2: 3}.all(k2, k != k2))" + }) + String expr) + throws Exception { + CelAbstractSyntaxTree ast = CEL.compile(expr).getAst(); + CelValidator celValidator = + CelValidatorFactory.standardCelValidatorBuilder(CEL) + .addAstValidators(ComprehensionNestingLimitValidator.newInstance(2)) + .build(); + + CelValidationResult result = celValidator.validate(ast); + + assertThat(result.hasError()).isFalse(); + } + + @Test + public void comprehensionNestingLimit_trivialLoopsDontCount( + @TestParameter({ + "cel.bind(x, [1, 2].map(x, x + 1), x + [1, 2].map(x, x + 1))", + "optional.of(1).optMap(x, [1, 2, 3].exists(y, y == x))", + "[].map(x, [1, 2, 3].map(y, x + y))", + "{}.map(k1, {1: 2, 3: 4}.map(k2, k1 + k2))", + "[1, 2, 3].map(x, cel.bind(y, 2, x + y))", + "[1, 2, 3].map(x, optional.of(1).optMap(y, x + y).orValue(0))", + }) + String expr) + throws Exception { + CelAbstractSyntaxTree ast = CEL.compile(expr).getAst(); + + CelValidationResult result = CEL_VALIDATOR.validate(ast); + + assertThat(result.hasError()).isFalse(); + } + + @Test + public void comprehensionNestingLimit_zeroLimitAcceptedComprehenions( + @TestParameter({ + "cel.bind(x, 1, x + 1)", + "optional.of(1).optMap(x, x + 1)", + "[].map(x, int(x))", + "cel.bind(x, 1 + [].map(x, int(x)).size(), x + 1)" + }) + String expr) + throws Exception { + CelAbstractSyntaxTree ast = CEL.compile(expr).getAst(); + + CelValidator celValidator = + CelValidatorFactory.standardCelValidatorBuilder(CEL) + .addAstValidators(ComprehensionNestingLimitValidator.newInstance(0)) + .build(); + + CelValidationResult result = celValidator.validate(ast); + + assertThat(result.hasError()).isFalse(); + } + + @Test + public void comprehensionNestingLimit_zeroLimitRejectedComprehensions( + @TestParameter({ + "[1].map(x, x)", + "[1].exists(x, x > 0)", + "[].exists(x, [1].all(y, y > 0))", + }) + String expr) + throws Exception { + CelAbstractSyntaxTree ast = CEL.compile(expr).getAst(); + + CelValidator celValidator = + CelValidatorFactory.standardCelValidatorBuilder(CEL) + .addAstValidators(ComprehensionNestingLimitValidator.newInstance(0)) + .build(); + + CelValidationException e = + assertThrows(CelValidationException.class, () -> celValidator.validate(ast).getAst()); + + assertThat(e.getMessage()).contains("comprehension nesting exceeds the configured limit: 0."); + } +} diff --git a/validator/src/test/java/dev/cel/validator/validators/DurationLiteralValidatorTest.java b/validator/src/test/java/dev/cel/validator/validators/DurationLiteralValidatorTest.java index 3fff1c2b1..1ec3ef4aa 100644 --- a/validator/src/test/java/dev/cel/validator/validators/DurationLiteralValidatorTest.java +++ b/validator/src/test/java/dev/cel/validator/validators/DurationLiteralValidatorTest.java @@ -20,8 +20,6 @@ import static org.junit.Assert.assertThrows; import com.google.common.collect.ImmutableMap; -import com.google.protobuf.Duration; -import com.google.protobuf.util.Durations; import com.google.testing.junit.testparameterinjector.TestParameterInjector; import com.google.testing.junit.testparameterinjector.TestParameters; import dev.cel.bundle.Cel; @@ -29,12 +27,14 @@ import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelIssue.Severity; import dev.cel.common.CelValidationResult; +import dev.cel.common.internal.ProtoTimeUtils; import dev.cel.common.types.SimpleType; import dev.cel.runtime.CelEvaluationException; -import dev.cel.runtime.CelRuntime.CelFunctionBinding; +import dev.cel.runtime.CelFunctionBinding; import dev.cel.validator.CelValidator; import dev.cel.validator.CelValidatorFactory; import java.text.ParseException; +import java.time.Duration; import org.junit.Test; import org.junit.runner.RunWith; @@ -89,7 +89,9 @@ public void duration_withVariable_noOp() throws Exception { assertThrows( CelEvaluationException.class, () -> CEL.createProgram(ast).eval(ImmutableMap.of("str_var", "bad"))); - assertThat(e).hasMessageThat().contains("evaluation error: invalid duration format"); + assertThat(e) + .hasMessageThat() + .contains("evaluation error at :8: invalid duration format"); } @Test @@ -106,7 +108,7 @@ public void duration_withFunction_noOp() throws Exception { String.class, stringArg -> { try { - return Durations.parse(stringArg).toString(); + return ProtoTimeUtils.parse(stringArg).toString(); } catch (ParseException e) { throw new RuntimeException(e); } @@ -124,7 +126,9 @@ public void duration_withFunction_noOp() throws Exception { // However, the same AST fails on evaluation when the function dispatch fails. assertThat(e) .hasMessageThat() - .contains("evaluation error: Function 'testFuncOverloadId' failed with arg(s) 'bad'"); + .contains( + "evaluation error at :17: Function 'testFuncOverloadId' failed with arg(s)" + + " 'bad'"); } @Test @@ -170,7 +174,7 @@ public void duration_unexpectedResultType_throws() throws Exception { assertThat(result.getAllIssues().get(0).toDisplayString(ast.getSource())) .isEqualTo( "ERROR: :1:10: duration validation failed. Reason: Expected" - + " com.google.protobuf.Duration type but got java.lang.Integer instead\n" + + " java.time.Duration type but got java.lang.Integer instead\n" + " | duration('1h')\n" + " | .........^"); } diff --git a/validator/src/test/java/dev/cel/validator/validators/HomogeneousLiteralValidatorTest.java b/validator/src/test/java/dev/cel/validator/validators/HomogeneousLiteralValidatorTest.java index b6de4a809..baaf3f05c 100644 --- a/validator/src/test/java/dev/cel/validator/validators/HomogeneousLiteralValidatorTest.java +++ b/validator/src/test/java/dev/cel/validator/validators/HomogeneousLiteralValidatorTest.java @@ -27,7 +27,7 @@ import dev.cel.common.CelValidationResult; import dev.cel.common.types.SimpleType; import dev.cel.extensions.CelOptionalLibrary; -import dev.cel.runtime.CelRuntime.CelFunctionBinding; +import dev.cel.runtime.CelFunctionBinding; import dev.cel.validator.CelValidator; import dev.cel.validator.CelValidatorFactory; import java.util.List; 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 f54e487d2..a41317371 100644 --- a/validator/src/test/java/dev/cel/validator/validators/RegexLiteralValidatorTest.java +++ b/validator/src/test/java/dev/cel/validator/validators/RegexLiteralValidatorTest.java @@ -31,7 +31,7 @@ import dev.cel.common.CelValidationResult; import dev.cel.common.types.SimpleType; import dev.cel.runtime.CelEvaluationException; -import dev.cel.runtime.CelRuntime.CelFunctionBinding; +import dev.cel.runtime.CelFunctionBinding; import dev.cel.validator.CelValidator; import dev.cel.validator.CelValidatorFactory; import org.junit.Test; @@ -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(); @@ -97,7 +96,8 @@ public void regex_globalWithVariable_noOp() throws Exception { assertThat(e) .hasMessageThat() .contains( - "evaluation error: error parsing regexp: missing argument to repetition operator: `*`"); + "evaluation error at :7: error parsing regexp: missing argument to repetition" + + " operator: `*`"); } @Test @@ -118,7 +118,8 @@ public void regex_receiverWithVariable_noOp() throws Exception { assertThat(e) .hasMessageThat() .contains( - "evaluation error: error parsing regexp: missing argument to repetition operator: `*`"); + "evaluation error at :14: error parsing regexp: missing argument to repetition" + + " operator: `*`"); } @Test @@ -144,7 +145,8 @@ public void regex_globalWithFunction_noOp() throws Exception { assertThat(e) .hasMessageThat() .contains( - "evaluation error: error parsing regexp: missing argument to repetition operator: `*`"); + "evaluation error at :7: error parsing regexp: missing argument to repetition" + + " operator: `*`"); } @Test @@ -170,7 +172,8 @@ public void regex_receiverWithFunction_noOp() throws Exception { assertThat(e) .hasMessageThat() .contains( - "evaluation error: error parsing regexp: missing argument to repetition operator: `*`"); + "evaluation error at :14: error parsing regexp: missing argument to repetition" + + " operator: `*`"); } @Test 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 0477dacef..404ed7f7e 100644 --- a/validator/src/test/java/dev/cel/validator/validators/TimestampLiteralValidatorTest.java +++ b/validator/src/test/java/dev/cel/validator/validators/TimestampLiteralValidatorTest.java @@ -20,8 +20,6 @@ import static org.junit.Assert.assertThrows; import com.google.common.collect.ImmutableMap; -import com.google.protobuf.Timestamp; -import com.google.protobuf.util.Timestamps; import com.google.testing.junit.testparameterinjector.TestParameterInjector; import com.google.testing.junit.testparameterinjector.TestParameters; import dev.cel.bundle.Cel; @@ -30,19 +28,20 @@ import dev.cel.common.CelIssue.Severity; import dev.cel.common.CelOptions; import dev.cel.common.CelValidationResult; +import dev.cel.common.internal.ProtoTimeUtils; import dev.cel.common.types.SimpleType; import dev.cel.runtime.CelEvaluationException; -import dev.cel.runtime.CelRuntime.CelFunctionBinding; +import dev.cel.runtime.CelFunctionBinding; import dev.cel.validator.CelValidator; import dev.cel.validator.CelValidatorFactory; import java.text.ParseException; +import java.time.Instant; import org.junit.Test; import org.junit.runner.RunWith; @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(); @@ -63,7 +62,7 @@ public void timestamp_validFormat(String source) throws Exception { assertThat(result.hasError()).isFalse(); assertThat(result.getAllIssues()).isEmpty(); - assertThat(CEL.createProgram(ast).eval()).isInstanceOf(Timestamp.class); + assertThat(CEL.createProgram(ast).eval()).isInstanceOf(Instant.class); } @Test @@ -100,7 +99,7 @@ public void timestamp_withVariable_noOp() throws Exception { () -> CEL.createProgram(ast).eval(ImmutableMap.of("str_var", "bad"))); assertThat(e) .hasMessageThat() - .contains("evaluation error: Failed to parse timestamp: invalid timestamp \"bad\""); + .contains("evaluation error at :9: Text 'bad' could not be parsed at index 0"); } @Test @@ -117,7 +116,7 @@ public void timestamp_withFunction_noOp() throws Exception { String.class, stringArg -> { try { - return Timestamps.parse(stringArg).getSeconds(); + return ProtoTimeUtils.parse(stringArg).getSeconds(); } catch (ParseException e) { throw new RuntimeException(e); } @@ -136,7 +135,9 @@ public void timestamp_withFunction_noOp() throws Exception { // However, the same AST fails on evaluation when the function dispatch fails. assertThat(e) .hasMessageThat() - .contains("evaluation error: Function 'testFuncOverloadId' failed with arg(s) 'bad'"); + .contains( + "evaluation error at :18: Function 'testFuncOverloadId' failed with arg(s)" + + " 'bad'"); } @Test @@ -150,8 +151,8 @@ public void timestamp_invalidFormat() throws Exception { assertThat(result.getAllIssues().get(0).getSeverity()).isEqualTo(Severity.ERROR); assertThat(result.getAllIssues().get(0).toDisplayString(ast.getSource())) .isEqualTo( - "ERROR: :1:11: timestamp validation failed. Reason: evaluation error: Failed to" - + " parse timestamp: invalid timestamp \"bad\"\n" + "ERROR: :1:11: timestamp validation failed. Reason: evaluation error: Text 'bad'" + + " could not be parsed at index 0\n" + " | timestamp('bad')\n" + " | ..........^"); } @@ -182,7 +183,7 @@ public void timestamp_unexpectedResultType_throws() throws Exception { assertThat(result.getAllIssues().get(0).toDisplayString(ast.getSource())) .isEqualTo( "ERROR: :1:11: timestamp validation failed. Reason: Expected" - + " com.google.protobuf.Timestamp type but got java.lang.Integer instead\n" + + " java.time.Instant type but got java.lang.Integer instead\n" + " | timestamp(0)\n" + " | ..........^"); } @@ -198,4 +199,23 @@ public void parentIsNotCallExpr_doesNotThrow(String source) throws Exception { assertThat(result.hasError()).isFalse(); assertThat(result.getAllIssues()).isEmpty(); } + + @Test + public void env_withSetResultType_success() throws Exception { + Cel cel = + CelFactory.standardCelBuilder() + .setOptions(CelOptions.current().build()) + .setResultType(SimpleType.BOOL) + .build(); + CelValidator validator = + CelValidatorFactory.standardCelValidatorBuilder(cel) + .addAstValidators(TimestampLiteralValidator.INSTANCE) + .build(); + CelAbstractSyntaxTree ast = cel.compile("timestamp(123) == timestamp(123)").getAst(); + + CelValidationResult result = validator.validate(ast); + + assertThat(result.hasError()).isFalse(); + assertThat(result.getAllIssues()).isEmpty(); + } } diff --git a/validator/validators/BUILD.bazel b/validator/validators/BUILD.bazel index e4f9ea54b..8054ae092 100644 --- a/validator/validators/BUILD.bazel +++ b/validator/validators/BUILD.bazel @@ -1,3 +1,5 @@ +load("@rules_java//java:defs.bzl", "java_library") + package( default_applicable_licenses = ["//:license"], default_visibility = ["//visibility:public"], @@ -22,3 +24,18 @@ java_library( name = "homogeneous_literal", exports = ["//validator/src/main/java/dev/cel/validator/validators:homogeneous_literal"], ) + +java_library( + name = "ast_depth_limit_validator", + exports = ["//validator/src/main/java/dev/cel/validator/validators:ast_depth_limit_validator"], +) + +java_library( + name = "comprehension_nesting_limit_validator", + exports = ["//validator/src/main/java/dev/cel/validator/validators:comprehension_nesting_limit_validator"], +) + +java_library( + name = "literal_validator", + exports = ["//validator/src/main/java/dev/cel/validator/validators:literal_validator"], +)