diff --git a/.bazelrc b/.bazelrc new file mode 100644 index 000000000..31bb2d7bc --- /dev/null +++ b/.bazelrc @@ -0,0 +1,6 @@ +common --enable_bzlmod +build --java_runtime_version=remotejdk_11 +build --java_language_version=11 + +# Hide Java 8 deprecation warnings. +common --javacopt=-Xlint:-options 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/unwanted_deps.sh b/.github/workflows/unwanted_deps.sh new file mode 100755 index 000000000..abef91a5f --- /dev/null +++ b/.github/workflows/unwanted_deps.sh @@ -0,0 +1,45 @@ +#!/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 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 +checkUnwantedDeps '//publish:cel_runtime_android' '@maven//:com_google_protobuf_protobuf_java' +exit 0 diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 16ebd2a5f..6f61e9e22 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -23,14 +23,57 @@ 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@v4 + - name: Setup Bazel + uses: bazel-contrib/setup-bazel@0.14.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 - name: Bazel Output Version run: bazelisk --version + - name: Java 8 Build + run: bazel build ... --java_language_version=8 --java_runtime_version=8 --build_tag_filters=-conformance_maven - name: Bazel Test - 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 + + # -- Start of Maven Conformance Tests (Ran only when there's version changes) -- + - name: Get changed file + id: changed_file + uses: tj-actions/changed-files@v44 + with: + files: publish/cel_version.bzl + - 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 'dev.cel:cel' MODULE.bazel | cut -d '"' -f 2 | cut -d ':' -f 3) + + 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 + # -- End of Maven Conformance Tests -- + + - name: Unwanted Dependencies + run: .github/workflows/unwanted_deps.sh - run: echo "🍏 This job's status is ${{ job.status }}." diff --git a/.gitignore b/.gitignore index 633d0c6be..e8052913c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ bazel-bin bazel-examples bazel-genfiles -bazel-grpc-java +bazel-cel-java bazel-out bazel-testlogs @@ -27,4 +27,7 @@ bin target # Temporary output dir for artifacts -mvn-artifacts \ No newline at end of file +mvn-artifacts + +*.swp +*.lock diff --git a/BUILD.bazel b/BUILD.bazel index 3f9a36090..06942bc50 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -15,6 +15,13 @@ # 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_license//rules:license.bzl", "license") licenses(["notice"]) # Apache License 2.0 @@ -66,7 +73,6 @@ java_library( ], neverlink = 1, exports = [ - "@maven//:com_google_auto_value_auto_value", "@maven//:com_google_auto_value_auto_value_annotations", ], ) @@ -82,25 +88,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 +186,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..de3cbdace --- /dev/null +++ b/MODULE.bazel @@ -0,0 +1,128 @@ +# 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.7.1") +bazel_dep(name = "rules_jvm_external", version = "6.7") +bazel_dep(name = "protobuf", version = "29.3", repo_name = "com_google_protobuf") # see https://github.com/bazelbuild/rules_android/issues/373 +bazel_dep(name = "googleapis", version = "0.0.0-20241220-5e258e33.bcr.1", repo_name = "com_google_googleapis") +bazel_dep(name = "rules_pkg", version = "1.0.1") +bazel_dep(name = "rules_license", version = "1.0.0") +bazel_dep(name = "rules_proto", version = "7.1.0") +bazel_dep(name = "rules_java", version = "8.12.0") +bazel_dep(name = "rules_android", version = "0.6.4") +bazel_dep(name = "rules_shell", version = "0.5.1") +bazel_dep(name = "googleapis-java", version = "1.0.0") +bazel_dep(name = "cel-spec", version = "0.24.0", repo_name = "cel_spec") + +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.4.8" + +TRUTH_VERSION = "1.4.4" + +PROTOBUF_JAVA_VERSION = "4.32.0" + +# 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.41.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:0.11.0"], + 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") diff --git a/README.md b/README.md index c317e960e..61ac687b9 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.11.0 ``` **Gradle** ```gradle -implementation 'dev.cel:cel:0.3.0' +implementation 'dev.cel:cel:0.11.0' ``` 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,8 +376,6 @@ 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 [2]: https://groups.google.com/forum/#!forum/cel-java-discuss [3]: https://github.com/google/guava diff --git a/WORKSPACE b/WORKSPACE deleted file mode 100644 index 74520223a..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:32.1.3-jre", - "com.google.guava:guava-testlib:32.1.3-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.14", - "com.google.truth.extensions:truth-java8-extension:1.1.5", - "com.google.truth.extensions:truth-proto-extension:1.1.5", - "com.google.truth:truth:1.1.5", - "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 = "6c2d9ec6dd5e2afbc41423dcaeca9fdd73edcd76554a897caee5b9a2b0e20491", - strip_prefix = "cel-spec-0.11.0", - urls = [ - "https://github.com/google/cel-spec/archive/refs/tags/v0.11.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..7f21cf219 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"], @@ -7,3 +9,24 @@ java_library( name = "cel", exports = ["//bundle/src/main/java/dev/cel/bundle:cel"], ) + +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", + visibility = ["//:internal"], + exports = ["//bundle/src/main/java/dev/cel/bundle:environment_exporter"], +) diff --git a/bundle/src/main/java/dev/cel/bundle/BUILD.bazel b/bundle/src/main/java/dev/cel/bundle/BUILD.bazel index c2c7d3f10..35b4679f7 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 = [ @@ -23,12 +25,14 @@ java_library( "//checker:checker_builder", "//checker:checker_legacy_environment", "//checker:proto_type_mask", - "//common", + "//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", @@ -37,10 +41,113 @@ java_library( "//parser:macro", "//parser:parser_builder", "//runtime", - "@cel_spec//proto/cel/expr:expr_java_proto", + "//runtime:function_binding", + "@cel_spec//proto/cel/expr:checked_java_proto", "@maven//:com_google_code_findbugs_annotations", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", ], ) + +java_library( + name = "environment", + srcs = [ + "CelEnvironment.java", + ], + tags = [ + ], + deps = [ + ":environment_exception", + ":required_fields_checker", + "//:auto_value", + "//bundle:cel", + "//checker:standard_decl", + "//common:compiler_common", + "//common:container", + "//common:options", + "//common:source", + "//common/types", + "//common/types:type_providers", + "//compiler:compiler_builder", + "//extensions", + "//extensions:optional_library", + "//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/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:standard_decl", + "//common:compiler_common", + "//common:options", + "//common/internal:env_visitor", + "//common/types:cel_proto_types", + "//common/types:cel_types", + "//common/types:type_providers", + "//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/Cel.java b/bundle/src/main/java/dev/cel/bundle/Cel.java index 964b21a5c..677a4af2c 100644 --- a/bundle/src/main/java/dev/cel/bundle/Cel.java +++ b/bundle/src/main/java/dev/cel/bundle/Cel.java @@ -20,4 +20,6 @@ /** Cel interface for parse, type-check, and evaluation of CEL programs. */ @ThreadSafe -public interface Cel extends CelCompiler, CelRuntime {} +public interface Cel extends CelCompiler, CelRuntime { + CelBuilder toCelBuilder(); +} diff --git a/bundle/src/main/java/dev/cel/bundle/CelBuilder.java b/bundle/src/main/java/dev/cel/bundle/CelBuilder.java index eb54c2ff6..9580dc555 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelBuilder.java +++ b/bundle/src/main/java/dev/cel/bundle/CelBuilder.java @@ -24,6 +24,7 @@ import com.google.protobuf.Message; 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,7 +34,7 @@ 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 java.util.function.Function; @@ -80,12 +81,15 @@ public interface CelBuilder { @CanIgnoreReturnValue CelBuilder addMacros(Iterable macros); - /** - * Set the {@code container} name to use as the namespace for resolving CEL expression variables - * and functions. + /** Retrieves the currently configured {@link CelContainer} in the builder. */ + CelContainer container(); + + /* + * 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 +148,20 @@ 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); /** Set the expected {@code resultType} for the type-checked expression. */ @CanIgnoreReturnValue 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..3bdc7c894 --- /dev/null +++ b/bundle/src/main/java/dev/cel/bundle/CelEnvironment.java @@ -0,0 +1,915 @@ +// 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.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.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.CelRuntimeBuilder; +import dev.cel.runtime.CelRuntimeLibrary; +import java.util.Arrays; +import java.util.Optional; + +/** + * 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, + "sets", CanonicalCelExtension.SETS, + "strings", CanonicalCelExtension.STRINGS); + + /** Environment source in textual format (ex: textproto, YAML). */ + public abstract Optional source(); + + /** Name of the environment. */ + public abstract String name(); + + /** + * An optional description of the config (example: location of the file containing the config + * content). + */ + public abstract String 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(); + + /** 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(String 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); + + 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("") + .setContainer("") + .setVariables(ImmutableSet.of()) + .setFunctions(ImmutableSet.of()); + } + + /** Extends the provided {@link CelCompiler} environment with this configuration. */ + public CelCompiler extend(CelCompiler celCompiler, CelOptions celOptions) + throws CelEnvironmentException { + try { + CelTypeProvider celTypeProvider = celCompiler.getTypeProvider(); + CelCompilerBuilder compilerBuilder = + celCompiler + .toCompilerBuilder() + .setTypeProvider(celTypeProvider) + .setContainer(CelContainer.ofName(container())) + .addVarDeclarations( + variables().stream() + .map(v -> v.toCelVarDecl(celTypeProvider)) + .collect(toImmutableList())) + .addFunctionDeclarations( + functions().stream() + .map(f -> f.toCelFunctionDecl(celTypeProvider)) + .collect(toImmutableList())); + + if (!container().isEmpty()) { + compilerBuilder.setContainer(CelContainer.ofName(container())); + } + + addAllCompilerExtensions(compilerBuilder, celOptions); + + applyStandardLibrarySubset(compilerBuilder); + + 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 { + try { + // Casting is necessary to only extend the compiler here + CelCompiler celCompiler = extend((CelCompiler) cel, celOptions); + + CelRuntimeBuilder celRuntimeBuilder = cel.toRuntimeBuilder(); + addAllRuntimeExtensions(celRuntimeBuilder, celOptions); + + return CelFactory.combine(celCompiler, celRuntimeBuilder.build()); + } catch (RuntimeException e) { + throw new CelEnvironmentException(e.getMessage(), e); + } + } + + 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 void addAllRuntimeExtensions(CelRuntimeBuilder celRuntimeBuilder, CelOptions 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); + } + } + } + + 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 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(); + + /** 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); + + @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 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 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(); + + /** 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(); + + public abstract OverloadDecl.Builder setArguments(ImmutableList args); + + @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); + default: + if (isTypeParam()) { + return TypeParamType.create(name()); + } + + 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 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); + } + } + + @VisibleForTesting + enum CanonicalCelExtension { + BINDINGS((options, version) -> CelExtensions.bindings()), + PROTOS((options, version) -> CelExtensions.protos()), + ENCODERS( + (options, version) -> CelExtensions.encoders(), + (options, version) -> CelExtensions.encoders()), + 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)), + LISTS((options, version) -> CelExtensions.lists(), (options, version) -> CelExtensions.lists()); + + @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(); + } + } + } +} 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..d303e0528 --- /dev/null +++ b/bundle/src/main/java/dev/cel/bundle/CelEnvironmentExporter.java @@ -0,0 +1,453 @@ +// 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.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.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.CelProtoTypes; +import dev.cel.common.types.CelType; +import dev.cel.common.types.CelTypes; +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) { + // 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. + + Set inventory = new HashSet<>(); + collectInventory(inventory, cel); + + CelEnvironment.Builder envBuilder = CelEnvironment.newBuilder(); + addExtensionConfigsAndRemoveFromInventory(envBuilder, inventory); + addStandardLibrarySubsetAndRemoveFromInventory(envBuilder, inventory); + addCustomDecls(envBuilder, inventory); + return envBuilder.build(); + } + + /** + * Collects all function overloads, variable declarations and macros from the given {@link Cel} + * instance and stores them in a map. + */ + private void collectInventory(Set inventory, Cel cel) { + ((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.create(CelTypes.format(type)); + } + + /** 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..3acb73fa5 --- /dev/null +++ b/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlParser.java @@ -0,0 +1,648 @@ +// 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.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.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 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()); + + /** 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 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 "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; + 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; + 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 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(newString(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; + 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..687bf4ff9 --- /dev/null +++ b/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlSerializer.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.bundle; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import dev.cel.bundle.CelEnvironment.LibrarySubset; +import dev.cel.bundle.CelEnvironment.LibrarySubset.FunctionSelector; +import dev.cel.bundle.CelEnvironment.LibrarySubset.OverloadSelector; +import java.util.Comparator; +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()); + } + + 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().isEmpty()) { + configMap.put("container", environment.container()); + } + 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()); + } + return represent(configMap.buildOrThrow()); + } + } + + 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())); + } + } +} diff --git a/bundle/src/main/java/dev/cel/bundle/CelFactory.java b/bundle/src/main/java/dev/cel/bundle/CelFactory.java index c69c4a3a5..a2080bc25 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelFactory.java +++ b/bundle/src/main/java/dev/cel/bundle/CelFactory.java @@ -17,8 +17,10 @@ import dev.cel.checker.CelCheckerLegacyImpl; import dev.cel.common.CelOptions; import dev.cel.compiler.CelCompiler; +import dev.cel.compiler.CelCompilerImpl; import dev.cel.parser.CelParserImpl; import dev.cel.runtime.CelRuntime; +import dev.cel.runtime.CelRuntimeLegacyImpl; /** Helper class to configure the entire CEL stack in a common interface. */ public final class CelFactory { @@ -33,7 +35,10 @@ private CelFactory() {} * evaluation are enabled by default. */ public static CelBuilder standardCelBuilder() { - return CelImpl.newBuilder(CelParserImpl.newBuilder(), CelCheckerLegacyImpl.newBuilder()) + return CelImpl.newBuilder( + CelCompilerImpl.newBuilder( + CelParserImpl.newBuilder(), CelCheckerLegacyImpl.newBuilder()), + CelRuntimeLegacyImpl.newBuilder()) .setOptions(CelOptions.current().build()) .setStandardEnvironmentEnabled(true); } diff --git a/bundle/src/main/java/dev/cel/bundle/CelImpl.java b/bundle/src/main/java/dev/cel/bundle/CelImpl.java index 20086b114..795e202e3 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelImpl.java +++ b/bundle/src/main/java/dev/cel/bundle/CelImpl.java @@ -31,6 +31,7 @@ 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,13 +40,12 @@ 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; -import dev.cel.compiler.CelCompilerImpl; import dev.cel.compiler.CelCompilerLibrary; import dev.cel.parser.CelMacro; import dev.cel.parser.CelParserBuilder; @@ -53,7 +53,6 @@ import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelRuntime; import dev.cel.runtime.CelRuntimeBuilder; -import dev.cel.runtime.CelRuntimeLegacyImpl; import dev.cel.runtime.CelRuntimeLibrary; import java.util.Arrays; import java.util.function.Function; @@ -89,6 +88,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); @@ -102,6 +106,31 @@ public void accept(EnvVisitor envVisitor) { } } + @Override + public CelBuilder toCelBuilder() { + return newBuilder(toCompilerBuilder(), toRuntimeBuilder()); + } + + @Override + public CelParserBuilder toParserBuilder() { + return compiler.get().toParserBuilder(); + } + + @Override + public CelCheckerBuilder toCheckerBuilder() { + return compiler.get().toCheckerBuilder(); + } + + @Override + public CelCompilerBuilder toCompilerBuilder() { + return compiler.get().toCompilerBuilder(); + } + + @Override + public CelRuntimeBuilder toRuntimeBuilder() { + return runtime.get().toRuntimeBuilder(); + } + /** Combines a prebuilt {@link CelCompiler} and {@link CelRuntime} into {@link CelImpl}. */ static CelImpl combine(CelCompiler compiler, CelRuntime runtime) { return new CelImpl(Suppliers.memoize(() -> compiler), Suppliers.memoize(() -> runtime)); @@ -112,8 +141,9 @@ static CelImpl combine(CelCompiler compiler, CelRuntime runtime) { * *

By default, {@link CelOptions#DEFAULT} are enabled, as is the CEL standard environment. */ - static CelBuilder newBuilder(CelParserBuilder parserBuilder, CelCheckerBuilder checkerBuilder) { - return new CelImpl.Builder(parserBuilder, checkerBuilder); + static CelBuilder newBuilder( + CelCompilerBuilder compilerBuilder, CelRuntimeBuilder celRuntimeBuilder) { + return new CelImpl.Builder(compilerBuilder, celRuntimeBuilder); } /** Builder class for CelImpl instances. */ @@ -122,9 +152,9 @@ public static final class Builder implements CelBuilder { private final CelCompilerBuilder compilerBuilder; private final CelRuntimeBuilder runtimeBuilder; - private Builder(CelParserBuilder parserBuilder, CelCheckerBuilder checkerBuilder) { - this.compilerBuilder = CelCompilerImpl.newBuilder(parserBuilder, checkerBuilder); - this.runtimeBuilder = CelRuntimeLegacyImpl.newBuilder(); + private Builder(CelCompilerBuilder celCompilerBuilder, CelRuntimeBuilder celRuntimeBuilder) { + this.compilerBuilder = celCompilerBuilder; + this.runtimeBuilder = celRuntimeBuilder; } @Override @@ -160,7 +190,12 @@ 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); return this; } @@ -226,13 +261,13 @@ 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; } @@ -240,7 +275,7 @@ public CelBuilder addFunctionBindings(Iterable bi @Override public CelBuilder setResultType(CelType resultType) { checkNotNull(resultType); - return setProtoResultType(CelTypes.celTypeToType(resultType)); + return setProtoResultType(CelProtoTypes.celTypeToType(resultType)); } @Override 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 68ecfa5b2..a4c6f513d 100644 --- a/bundle/src/test/java/dev/cel/bundle/BUILD.bazel +++ b/bundle/src/test/java/dev/cel/bundle/BUILD.bazel @@ -1,3 +1,4 @@ +load("@rules_java//java:defs.bzl", "java_library") load("//:testing.bzl", "junit4_test_suites") package(default_applicable_licenses = [ @@ -8,38 +9,59 @@ 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: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_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/internal:proto_time_utils", "//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", + "@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..fd178857f --- /dev/null +++ b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentExporterTest.java @@ -0,0 +1,242 @@ +// 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.CelFunctionDecl; +import dev.cel.common.CelOptions; +import dev.cel.common.CelOverloadDecl; +import dev.cel.common.CelVarDecl; +import dev.cel.common.types.OpaqueType; +import dev.cel.common.types.SimpleType; +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", "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( + "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()))); + + // 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"); + } +} + 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..bafa20ea6 --- /dev/null +++ b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentTest.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.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.CelOptions; +import dev.cel.common.CelValidationException; +import dev.cel.common.CelValidationResult; +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 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("sets"), + ExtensionConfig.latest("strings")); + 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 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 '_+_'"); + } +} 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..fb51f3f87 --- /dev/null +++ b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlParserTest.java @@ -0,0 +1,879 @@ +// 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.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.CelLateFunctionBindings; +import dev.cel.runtime.CelFunctionBinding; +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_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_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" + + " | ..........^"), + ; + + 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") + .setType(TypeDecl.create("cel.expr.conformance.proto3.TestAllTypes")) + .build()) + .setFunctions( + FunctionDecl.create( + "isEmpty", + ImmutableSet.of( + OverloadDecl.newBuilder() + .setId("wrapper_string_isEmpty") + .setTarget(TypeDecl.create("google.protobuf.StringValue")) + .setReturnType(TypeDecl.create("bool")) + .build(), + OverloadDecl.newBuilder() + .setId("list_isEmpty") + .setTarget( + TypeDecl.newBuilder() + .setName("list") + .addParams( + TypeDecl.newBuilder() + .setName("T") + .setIsTypeParam(true) + .build()) + .build()) + .setReturnType(TypeDecl.create("bool")) + .build()))) + .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..2bd39ad1f --- /dev/null +++ b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlSerializerTest.java @@ -0,0 +1,174 @@ +// 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 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("test.container") + .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( + "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()) + .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 4cb3a75e7..2a702538a 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,36 +41,43 @@ 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.FileDescriptor; import com.google.protobuf.Duration; +import com.google.protobuf.Empty; 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.WrappersProto; import com.google.rpc.context.AttributeContext; import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; import com.google.testing.junit.testparameterinjector.TestParameters; +import dev.cel.checker.CelCheckerLegacyImpl; import dev.cel.checker.DescriptorTypeProvider; import dev.cel.checker.ProtoTypeMask; import dev.cel.checker.TypeProvider; import dev.cel.common.CelAbstractSyntaxTree; -import dev.cel.common.CelFunctionDecl; +import dev.cel.common.CelContainer; +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.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.internal.ProtoTimeUtils; 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; @@ -75,30 +85,39 @@ 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.proto3.TestAllTypes; +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.testdata.proto3.StandaloneGlobalEnum; -import dev.cel.testing.testdata.proto3.TestAllTypesProto.TestAllTypes; 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; @@ -146,10 +165,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(); @@ -177,13 +196,13 @@ public void build_badFileDescriptorSet() { IllegalArgumentException.class, () -> standardCelBuilderWithMacros() - .setContainer("google.rpc.context.AttributeContext") + .setContainer(CelContainer.ofName("google.rpc.context.AttributeContext")) .addFileTypes( FileDescriptorSet.newBuilder() .addFile(AttributeContext.getDescriptor().getFile().toProto()) .build()) .setProtoResultType( - CelTypes.createMessage("google.rpc.context.AttributeContext.Resource")) + CelProtoTypes.createMessage("google.rpc.context.AttributeContext.Resource")) .build()); assertThat(e).hasMessageThat().contains("file descriptor set with unresolved proto file"); } @@ -209,7 +228,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); } @@ -223,7 +242,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); } @@ -241,13 +260,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"); @@ -262,7 +281,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) @@ -279,7 +298,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 @@ -311,9 +331,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 @@ -394,6 +414,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 { @@ -401,7 +422,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(), @@ -459,7 +480,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" @@ -477,9 +501,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 @@ -489,12 +513,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(); @@ -518,7 +544,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(); @@ -534,7 +560,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(); @@ -570,6 +596,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 = @@ -597,11 +670,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") @@ -610,21 +683,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( @@ -658,7 +731,7 @@ public void program_withThrowingFunction() throws Exception { .addOverloads( Overload.newBuilder() .setOverloadId("throws") - .setResultType(CelTypes.BOOL))) + .setResultType(CelProtoTypes.BOOL))) .build()) .addFunctionBindings( CelFunctionBinding.from( @@ -686,15 +759,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")); + throw CelEvaluationExceptionBuilder.newBuilder("this method always throws") + .setCause(new RuntimeException("reason")) + .build(); })) .setResultType(SimpleType.BOOL) .build(); @@ -724,7 +798,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) @@ -741,12 +815,12 @@ 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(ProtoTimeUtils.fromSecondsToTimestamp(12)); } @Test @@ -754,12 +828,12 @@ public void program_hermeticDescriptors_wellKnownProtobuf() throws Exception { Cel cel = standardCelBuilderWithMacros() .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(ProtoTimeUtils.fromSecondsToTimestamp(12)); } @Test @@ -776,7 +850,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()); @@ -793,7 +867,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 = @@ -812,7 +886,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()); @@ -826,7 +900,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(); @@ -852,7 +926,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'. @@ -883,12 +957,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(ProtoTimeUtils.fromSecondsToTimestamp(12)); } @Test @@ -902,7 +976,7 @@ public void program_protoActivation() throws Exception { .setIdent( IdentDecl.newBuilder() .setType( - CelTypes.createMessage( + CelProtoTypes.createMessage( "google.rpc.context.AttributeContext.Resource"))) .build()) .setResultType(SimpleType.STRING) @@ -925,7 +999,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(); @@ -946,10 +1021,13 @@ public void program_enumTypeReferenceResolution(boolean resolveTypeDependencies) Cel cel = standardCelBuilderWithMacros() .setOptions( - CelOptions.current().resolveTypeDependencies(resolveTypeDependencies).build()) + CelOptions.current() + .evaluateCanonicalTypesToNativeValues(true) + .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. @@ -964,17 +1042,20 @@ public void program_enumTypeReferenceResolution(boolean resolveTypeDependencies) public void program_enumTypeTransitiveResolution() throws Exception { Cel cel = standardCelBuilderWithMacros() - .setOptions(CelOptions.current().resolveTypeDependencies(true).build()) - .addMessageTypes(TestAllTypes.getDescriptor()) + .setOptions( + CelOptions.current() + .evaluateCanonicalTypesToNativeValues(true) + .resolveTypeDependencies(true) + .build()) + .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); @@ -999,9 +1080,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 @@ -1015,6 +1096,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 = @@ -1022,7 +1131,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(); @@ -1045,6 +1154,15 @@ public void program_wrongTypeComprehensionThrows() throws Exception { assertThat(e).hasMessageThat().contains("expected a list or a map"); } + @Test + public void program_stringFormatInjection_throwsEvaluationException() throws Exception { + Cel cel = standardCelBuilderWithMacros().build(); + CelRuntime.Program program = cel.createProgram(cel.compile("{}['%2000222222s']").getAst()); + + CelEvaluationException e = assertThrows(CelEvaluationException.class, program::eval); + assertThat(e).hasMessageThat().contains("evaluation error"); + } + @Test public void program_emptyTypeProviderConfig() throws Exception { Cel cel = standardCelBuilderWithMacros().build(); @@ -1056,7 +1174,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( @@ -1235,7 +1353,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(); @@ -1264,7 +1382,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(); @@ -1343,7 +1461,7 @@ public void programAdvanceEvaluation_argumentMergeErrorPriority() throws Excepti .setResultType( Type.newBuilder().setPrimitive(PrimitiveType.BOOL)))) .build()) - .setContainer("") + .setContainer(CelContainer.ofName("")) .addFunctionBindings() .setResultType(SimpleType.BOOL) .build(); @@ -1386,7 +1504,7 @@ public void programAdvanceEvaluation_argumentMergeUnknowns() throws Exception { .setResultType( Type.newBuilder().setPrimitive(PrimitiveType.BOOL)))) .build()) - .setContainer("") + .setContainer(CelContainer.ofName("")) .addFunctionBindings() .setResultType(SimpleType.BOOL) .build(); @@ -1413,7 +1531,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(); @@ -1439,7 +1557,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(); @@ -1468,7 +1586,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(); @@ -1497,7 +1615,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(); @@ -1517,9 +1635,13 @@ public void programAdvanceEvaluation_indexOnUnknownContainer() throws Exception public void programAdvanceEvaluation_unsupportedIndexIgnored() throws Exception { Cel cel = standardCelBuilderWithMacros() - .setOptions(CelOptions.current().enableUnknownTracking(true).build()) + .setOptions( + CelOptions.current() + .evaluateCanonicalTypesToNativeValues(true) + .enableUnknownTracking(true) + .build()) .addVar("unk", MapType.create(SimpleType.STRING, SimpleType.BOOL)) - .setContainer("") + .setContainer(CelContainer.ofName("")) .addFunctionBindings() .setResultType(SimpleType.BOOL) .build(); @@ -1545,7 +1667,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); } @@ -1556,7 +1678,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(); @@ -1589,7 +1711,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(); @@ -1639,7 +1761,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(); @@ -1651,7 +1773,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 @@ -1663,7 +1786,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 = @@ -1694,7 +1817,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 = @@ -1724,13 +1847,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 = @@ -1787,9 +1910,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 @@ -1803,13 +1926,215 @@ 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 + @TestParameters("{expression: 'string(123)'}") + @TestParameters("{expression: 'string(123u)'}") + @TestParameters("{expression: 'string(1.5)'}") + @TestParameters("{expression: 'string(\"foo\")'}") + @TestParameters("{expression: 'string(b\"foo\")'}") + @TestParameters("{expression: 'string(timestamp(100))'}") + @TestParameters("{expression: 'string(duration(\"1h\"))'}") + public void program_stringConversionDisabled_throws(String expression) throws Exception { + Cel cel = + CelFactory.standardCelBuilder() + .setOptions( + CelOptions.current() + .enableTimestampEpoch(true) + .enableStringConversion(false) + .build()) + .build(); + CelAbstractSyntaxTree ast = cel.compile(expression).getAst(); + + CelEvaluationException e = + assertThrows(CelEvaluationException.class, () -> cel.createProgram(ast).eval()); + assertThat(e).hasMessageThat().contains("No matching overload for function 'string'"); + assertThat(e.getErrorCode()).isEqualTo(CelErrorCode.OVERLOAD_NOT_FOUND); + } + + @Test + public void program_stringConcatenationDisabled_throws() throws Exception { + Cel cel = + CelFactory.standardCelBuilder() + .setOptions(CelOptions.current().enableStringConcatenation(false).build()) + .build(); + CelAbstractSyntaxTree ast = cel.compile("'foo' + 'bar'").getAst(); + + CelEvaluationException e = + assertThrows(CelEvaluationException.class, () -> cel.createProgram(ast).eval()); + assertThat(e).hasMessageThat().contains("No matching overload for function '_+_'"); + assertThat(e.getErrorCode()).isEqualTo(CelErrorCode.OVERLOAD_NOT_FOUND); + } + + @Test + public void program_listConcatenationDisabled_throws() throws Exception { + Cel cel = + CelFactory.standardCelBuilder() + .setOptions(CelOptions.current().enableListConcatenation(false).build()) + .build(); + CelAbstractSyntaxTree ast = cel.compile("[1] + [2]").getAst(); + + CelEvaluationException e = + assertThrows(CelEvaluationException.class, () -> cel.createProgram(ast).eval()); + assertThat(e).hasMessageThat().contains("No matching overload for function '_+_'"); + assertThat(e.getErrorCode()).isEqualTo(CelErrorCode.OVERLOAD_NOT_FOUND); + } + + @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 toBuilder_isImmutable() { + CelBuilder celBuilder = CelFactory.standardCelBuilder(); + CelImpl celImpl = (CelImpl) celBuilder.build(); + + CelImpl.Builder newCelBuilder = (CelImpl.Builder) celImpl.toCelBuilder(); + CelParserImpl.Builder newParserBuilder = (CelParserImpl.Builder) celImpl.toParserBuilder(); + CelCheckerLegacyImpl.Builder newCheckerBuilder = + (CelCheckerLegacyImpl.Builder) celImpl.toCheckerBuilder(); + CelCompilerImpl.Builder newCompilerBuilder = + (CelCompilerImpl.Builder) celImpl.toCompilerBuilder(); + CelRuntimeLegacyImpl.Builder newRuntimeBuilder = + (CelRuntimeLegacyImpl.Builder) celImpl.toRuntimeBuilder(); + + assertThat(newCelBuilder).isNotEqualTo(celBuilder); + assertThat(newParserBuilder).isNotEqualTo(celImpl.toParserBuilder()); + assertThat(newCheckerBuilder).isNotEqualTo(celImpl.toCheckerBuilder()); + assertThat(newCompilerBuilder).isNotEqualTo(celImpl.toCompilerBuilder()); + assertThat(newRuntimeBuilder).isNotEqualTo(celImpl.toRuntimeBuilder()); + } + 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; } @@ -1822,7 +2147,7 @@ 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; } diff --git a/cel_android_rules.bzl b/cel_android_rules.bzl new file mode 100644 index 000000000..5a94a7ef5 --- /dev/null +++ b/cel_android_rules.bzl @@ -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. +"""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"]) + filtered_kwargs = {k: v for k, v in kwargs.items() if k != "visibility"} + + android_library( + name = name, + visibility = provided_visibility_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 d5832a4f8..80f6e1a8a 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,22 +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_descriptors", + "//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", ], ) @@ -95,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", ], @@ -130,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", ], @@ -148,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", @@ -164,9 +174,11 @@ java_library( ], deps = [ ":cel_ident_decl", + ":standard_decl", "//:auto_value", + "//common:cel_ast", "//common:compiler_common", - "//common:features", + "//common:container", "//common:options", "//common:proto_ast", "//common/annotations", @@ -175,11 +187,13 @@ java_library( "//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", @@ -210,6 +224,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/types", + "//common/types:cel_types", + "//common/types:type_providers", + "//parser:operator", + "@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 5a6e9c28b..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 @@ -28,4 +29,9 @@ public interface CelChecker { *

Check validates the type-agreement of the parsed {@code CelAbstractSyntaxTree}. */ 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..e19cf5b70 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; @@ -35,11 +36,14 @@ public interface CelCheckerBuilder { CelCheckerBuilder 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 - 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 +156,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 ccc9c2955..41d1ca073 100644 --- a/checker/src/main/java/dev/cel/checker/CelCheckerLegacyImpl.java +++ b/checker/src/main/java/dev/cel/checker/CelCheckerLegacyImpl.java @@ -20,6 +20,7 @@ import dev.cel.expr.Decl; import dev.cel.expr.Type; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; @@ -30,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; @@ -39,38 +41,54 @@ import dev.cel.common.CelSourceLocation; import dev.cel.common.CelValidationResult; import dev.cel.common.CelVarDecl; +import dev.cel.common.annotations.Internal; import dev.cel.common.ast.CelExprConverter; 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, * consistent interface for type-checking. + * + *

CEL Library Internals. Do Not Use. Consumers should use {@code CelCompilerFactory} to + * instantiate a type-checker. */ @Immutable +@Internal public final class CelCheckerLegacyImpl implements CelChecker, EnvVisitable { private final CelOptions celOptions; - private final String container; - private final ImmutableList identDeclarations; - private final ImmutableList functionDeclarations; + private final CelContainer container; + private final ImmutableSet identDeclarations; + private final ImmutableSet functionDeclarations; private final Optional expectedResultType; @SuppressWarnings("Immutable") private final TypeProvider typeProvider; + private final CelTypeProvider celTypeProvider; private final boolean standardEnvironmentEnabled; + private final CelStandardDeclarations overriddenStandardDeclarations; + + // This does not affect the type-checking behavior in any manner. + @SuppressWarnings("Immutable") + private final ImmutableSet checkerLibraries; + + private final ImmutableSet fileDescriptors; + private final ImmutableSet protoTypeMasks; + @Override public CelValidationResult check(CelAbstractSyntaxTree ast) { CelSource source = ast.getSource(); @@ -88,6 +106,36 @@ public CelValidationResult check(CelAbstractSyntaxTree ast) { return new CelValidationResult(checkedAst, ImmutableList.of()); } + @Override + public CelTypeProvider getTypeProvider() { + return this.celTypeProvider; + } + + @Override + public CelCheckerBuilder toCheckerBuilder() { + 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 public void accept(EnvVisitor envVisitor) { Errors errors = new Errors("", ""); @@ -116,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); } @@ -132,18 +182,19 @@ public static CelCheckerBuilder newBuilder() { /** Builder class for the legacy {@code CelChecker} implementation. */ public static final class Builder implements CelCheckerBuilder { - private final ImmutableList.Builder identDeclarations; - private final ImmutableList.Builder functionDeclarations; - private final ImmutableList.Builder protoTypeMasks; + private final ImmutableSet.Builder identDeclarations; + private final ImmutableSet.Builder functionDeclarations; + private final ImmutableSet.Builder protoTypeMasks; 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) { @@ -152,12 +203,17 @@ public CelCheckerBuilder setOptions(CelOptions celOptions) { } @Override - public CelCheckerBuilder setContainer(String container) { + 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); @@ -173,7 +229,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()); @@ -252,7 +308,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 @@ -299,7 +355,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; } @@ -316,11 +378,71 @@ public CelCheckerBuilder addLibraries(Iterable libr return this; } + @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 functionDecls() { + return this.functionDeclarations; + } + + @VisibleForTesting + ImmutableSet.Builder identDecls() { + return this.identDeclarations; + } + + @VisibleForTesting + ImmutableSet.Builder protoTypeMasks() { + return this.protoTypeMasks; + } + + @VisibleForTesting + ImmutableSet.Builder messageTypes() { + return this.messageTypes; + } + + @VisibleForTesting + ImmutableSet.Builder fileTypes() { + return this.fileTypes; + } + + @VisibleForTesting + ImmutableSet.Builder checkerLibraries() { + return this.celCheckerLibraries; + } + + @VisibleForTesting + CelStandardDeclarations standardDeclarations() { + return this.standardDeclarations; + } + + @VisibleForTesting + CelOptions options() { + return this.celOptions; + } + + @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(); @@ -348,13 +470,13 @@ public CelCheckerLegacyImpl build() { // Configure the declaration set, and possibly alter the type provider if ProtoDecl values // are provided as they may prevent the use of certain field selection patterns against the // proto. - ImmutableList identDeclarationSet = identDeclarations.build(); - ImmutableList protoTypeMaskSet = protoTypeMasks.build(); + ImmutableSet identDeclarationSet = identDeclarations.build(); + ImmutableSet protoTypeMaskSet = protoTypeMasks.build(); if (!protoTypeMaskSet.isEmpty()) { ProtoTypeMaskTypeProvider protoTypeMaskTypeProvider = new ProtoTypeMaskTypeProvider(messageTypeProvider, protoTypeMaskSet); identDeclarationSet = - ImmutableList.builder() + ImmutableSet.builder() .addAll(identDeclarationSet) .addAll(protoTypeMaskTypeProvider.computeDeclsFromProtoTypeMasks()) .build(); @@ -375,36 +497,51 @@ public CelCheckerLegacyImpl build() { functionDeclarations.build(), Optional.fromNullable(expectedResultType), legacyProvider, - standardEnvironmentEnabled); + messageTypeProvider, + standardEnvironmentEnabled, + standardDeclarations, + checkerLibraries, + fileTypeSet, + protoTypeMaskSet); } private Builder() { this.celOptions = CelOptions.newBuilder().build(); - this.identDeclarations = ImmutableList.builder(); - this.functionDeclarations = ImmutableList.builder(); + this.identDeclarations = ImmutableSet.builder(); + this.functionDeclarations = ImmutableSet.builder(); this.fileTypes = ImmutableSet.builder(); this.messageTypes = ImmutableSet.builder(); - this.protoTypeMasks = ImmutableList.builder(); + this.protoTypeMasks = ImmutableSet.builder(); this.celCheckerLibraries = ImmutableSet.builder(); - this.container = ""; + this.container = CelContainer.ofName(""); } } private CelCheckerLegacyImpl( CelOptions celOptions, - String container, - ImmutableList identDeclarations, - ImmutableList functionDeclarations, + CelContainer container, + ImmutableSet identDeclarations, + ImmutableSet functionDeclarations, Optional expectedResultType, TypeProvider typeProvider, - boolean standardEnvironmentEnabled) { + CelTypeProvider celTypeProvider, + boolean standardEnvironmentEnabled, + @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.overriddenStandardDeclarations = overriddenStandardDeclarations; + this.checkerLibraries = checkerLibraries; + this.fileDescriptors = fileDescriptors; + this.protoTypeMasks = protoTypeMasks; } private static ImmutableList errorsToIssues(Errors errors) { @@ -415,7 +552,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..3865430ec --- /dev/null +++ b/checker/src/main/java/dev/cel/checker/CelStandardDeclarations.java @@ -0,0 +1,1785 @@ +// 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.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. + * + *

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; + } + } + + 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; + } + + 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 17d563dc8..d5692d48c 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,10 +28,12 @@ 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; @@ -38,6 +41,7 @@ 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 +52,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 +98,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 +113,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 - 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 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, 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; } @@ -296,7 +293,7 @@ public Map getTypeMap() { @Deprecated public Type getType(Expr expr) { Preconditions.checkNotNull(expr); - return CelTypes.celTypeToType(getType(CelExprConverter.fromExpr(expr))); + return CelProtoTypes.celTypeToType(getType(CelExprConverter.fromExpr(expr))); } /** @@ -349,7 +346,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 +391,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 +419,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 +434,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; @@ -454,8 +453,8 @@ public Env add(String name, Type type) { * *

Returns {@code null} if the function cannot be found. */ - public @Nullable CelIdentDecl tryLookupCelIdent(String container, String name) { - for (String cand : qualifiedTypeNameCandidates(container, name)) { + public @Nullable CelIdentDecl tryLookupCelIdent(CelContainer container, String name) { + for (String cand : container.resolveCandidateNames(name)) { // First determine whether we know this name already. CelIdentDecl decl = findIdentDecl(cand); if (decl != null) { @@ -493,10 +492,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 +510,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 +552,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 +560,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 +607,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 +623,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 +689,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 +707,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 +789,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 +814,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()); @@ -958,7 +949,7 @@ private static CelFunctionDecl sanitizeFunction(CelFunctionDecl func) { } CelFunctionDecl.Builder funcBuilder = func.toBuilder(); - ImmutableList.Builder overloadsBuilder = new ImmutableList.Builder<>(); + ImmutableSet.Builder overloadsBuilder = new ImmutableSet.Builder<>(); for (CelOverloadDecl overloadDecl : funcBuilder.overloads()) { CelOverloadDecl.Builder overloadBuilder = overloadDecl.toBuilder(); CelType resultType = overloadBuilder.build().resultType(); diff --git a/checker/src/main/java/dev/cel/checker/ExprChecker.java b/checker/src/main/java/dev/cel/checker/ExprChecker.java index 5c0c21ffd..c2a2929b1 100644 --- a/checker/src/main/java/dev/cel/checker/ExprChecker.java +++ b/checker/src/main/java/dev/cel/checker/ExprChecker.java @@ -22,11 +22,12 @@ 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.CelOverloadDecl; import dev.cel.common.CelProtoAbstractSyntaxTree; @@ -35,6 +36,7 @@ import dev.cel.common.ast.CelExpr; 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; @@ -47,7 +49,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; -import org.jspecify.nullness.Nullable; +import org.jspecify.annotations.Nullable; /** * The expression type checker. @@ -61,8 +63,7 @@ public final class ExprChecker { /** - * 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 +75,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 +86,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,14 +117,14 @@ 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(), @@ -143,7 +144,7 @@ public static CelAbstractSyntaxTree typecheck( 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; @@ -152,17 +153,17 @@ public static CelAbstractSyntaxTree typecheck( 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; @@ -180,12 +181,12 @@ public CelExpr visit(CelExpr expr) { return visit(expr, expr.select()); 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()); + case LIST: + return visit(expr, expr.list()); + case STRUCT: + return visit(expr, expr.struct()); + case MAP: + return visit(expr, expr.map()); case COMPREHENSION: return visit(expr, expr.comprehension()); default: @@ -231,7 +232,7 @@ private CelExpr visit(CelExpr expr, CelConstant constant) { @CheckReturnValue private CelExpr visit(CelExpr expr, CelExpr.CelIdent ident) { - CelIdentDecl decl = env.lookupIdent(getPosition(expr), inContainer, ident.name()); + CelIdentDecl decl = env.lookupIdent(expr.id(), getPosition(expr), container, ident.name()); checkNotNull(decl); if (decl.equals(Env.ERROR_IDENT_DECL)) { // error reported @@ -253,10 +254,10 @@ private CelExpr visit(CelExpr expr, CelExpr.CelSelect 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 { if (namespacedDeclarations) { @@ -307,8 +308,8 @@ 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) { @@ -320,9 +321,9 @@ private CelExpr visit(CelExpr expr, CelExpr.CelCall call) { // 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 @@ -341,8 +342,9 @@ private CelExpr visit(CelExpr expr, CelExpr.CelCall call) { } resolution = resolveOverload( + expr.id(), position, - env.lookupFunction(getPosition(expr), inContainer, call.function()), + env.lookupFunction(expr.id(), getPosition(expr), container, call.function()), target, call.args()); } @@ -355,21 +357,25 @@ private CelExpr visit(CelExpr expr, CelExpr.CelCall call) { } @CheckReturnValue - private CelExpr visit(CelExpr expr, CelExpr.CelCreateStruct createStruct) { + private CelExpr visit(CelExpr expr, CelExpr.CelStruct 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()); env.setRef(expr, CelReference.newBuilder().setName(decl.name()).build()); CelType type = decl.type(); if (type.kind() != CelKind.ERROR) { if (type.kind() != 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) { 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 +389,31 @@ private CelExpr visit(CelExpr expr, CelExpr.CelCreateStruct createStruct) { } // Check the field initializers. - ImmutableList entriesList = createStruct.entries(); + ImmutableList entriesList = struct.entries(); for (int i = 0; i < entriesList.size(); i++) { - CelExpr.CelCreateStruct.Entry entry = entriesList.get(i); + CelExpr.CelStruct.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 fieldType = + getFieldType(entry.id(), getPosition(entry), messageType, entry.fieldKey()).celType(); CelType valueType = env.getType(visitedValueExpr); if (entry.optionalEntry()) { if (valueType instanceof OptionalType) { valueType = unwrapOptional(valueType); } else { assertIsAssignable( - getPosition(visitedValueExpr), valueType, OptionalType.create(valueType)); + visitedValueExpr.id(), + getPosition(visitedValueExpr), + 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(), @@ -414,19 +425,23 @@ private CelExpr visit(CelExpr expr, CelExpr.CelCreateStruct createStruct) { } @CheckReturnValue - private CelExpr visit(CelExpr expr, CelExpr.CelCreateMap createMap) { + private CelExpr visit(CelExpr expr, CelExpr.CelMap map) { CelType mapKeyType = null; CelType mapValueType = null; - ImmutableList entriesList = createMap.entries(); + ImmutableList entriesList = map.entries(); for (int i = 0; i < entriesList.size(); i++) { - CelExpr.CelCreateMap.Entry entry = entriesList.get(i); + CelExpr.CelMap.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)); + joinTypes( + visitedMapKeyExpr.id(), + getPosition(visitedMapKeyExpr), + mapKeyType, + env.getType(visitedMapKeyExpr)); CelExpr visitedValueExpr = visit(entry.value()); if (namespacedDeclarations && !visitedValueExpr.equals(entry.value())) { @@ -439,11 +454,15 @@ private CelExpr visit(CelExpr expr, CelExpr.CelCreateMap createMap) { valueType = unwrapOptional(valueType); } else { assertIsAssignable( - getPosition(visitedValueExpr), valueType, OptionalType.create(valueType)); + visitedValueExpr.id(), + getPosition(visitedValueExpr), + valueType, + OptionalType.create(valueType)); } } - mapValueType = joinTypes(getPosition(visitedValueExpr), mapValueType, valueType); + mapValueType = + joinTypes(visitedValueExpr.id(), getPosition(visitedValueExpr), mapValueType, valueType); } if (mapKeyType == null) { // If the map is empty, assign free type variables to key and value type. @@ -455,10 +474,10 @@ private CelExpr visit(CelExpr expr, CelExpr.CelCreateMap createMap) { } @CheckReturnValue - private CelExpr visit(CelExpr expr, CelExpr.CelCreateList createList) { + private CelExpr visit(CelExpr expr, CelExpr.CelList list) { CelType elemsType = null; - ImmutableList elementsList = createList.elements(); - HashSet optionalIndices = new HashSet<>(createList.optionalIndices()); + ImmutableList 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))) { @@ -470,11 +489,12 @@ private CelExpr visit(CelExpr expr, CelExpr.CelCreateList createList) { if (elemType instanceof OptionalType) { elemType = unwrapOptional(elemType); } else { - assertIsAssignable(getPosition(visitedElem), elemType, OptionalType.create(elemType)); + assertIsAssignable( + visitedElem.id(), getPosition(visitedElem), elemType, OptionalType.create(elemType)); } } - elemsType = joinTypes(getPosition(visitedElem), elemsType, elemType); + elemsType = joinTypes(visitedElem.id(), getPosition(visitedElem), elemsType, elemType); } if (elemsType == null) { // If the list is empty, assign free type var to elem type. @@ -487,24 +507,30 @@ private CelExpr visit(CelExpr expr, CelExpr.CelCreateList createList) { @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); + CelExpr visitedInit = visit(compre.accuInit()); + CelType accuType = env.getType(visitedInit); CelType rangeType = inferenceContext.specialize(env.getType(visitedRange)); 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 +539,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( + expr.id(), getPosition(visitedRange), "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,20 +559,31 @@ private CelExpr visit(CelExpr expr, CelExpr.CelComprehension compre) { // Declare iteration variable on inner scope. env.enterScope(); env.add(CelIdentDecl.newIdentDeclaration(compre.iterVar(), varType)); + if (!Strings.isNullOrEmpty(compre.iterVar2())) { + env.add(CelIdentDecl.newIdentDeclaration(compre.iterVar2(), varType2)); + } CelExpr condition = visit(compre.loopCondition()); assertType(condition, SimpleType.BOOL); CelExpr visitedStep = visit(compre.loopStep()); - if (namespacedDeclarations && !visitedStep.equals(compre.loopStep())) { - expr = replaceComprehensionStepSubtree(expr, visitedStep); - } assertType(visitedStep, 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); - } env.exitScope(); + if (namespacedDeclarations) { + if (!visitedRange.equals(compre.iterRange())) { + expr = replaceComprehensionRangeSubtree(expr, visitedRange); + } + if (!visitedInit.equals(compre.accuInit())) { + expr = replaceComprehensionAccuInitSubtree(expr, visitedInit); + } + if (!visitedStep.equals(compre.loopStep())) { + expr = replaceComprehensionStepSubtree(expr, visitedStep); + } + if (!visitedResult.equals(compre.result())) { + expr = replaceComprehensionResultSubtree(expr, visitedResult); + } + } env.setType(expr, inferenceContext.specialize(env.getType(visitedResult))); return expr; } @@ -557,6 +597,7 @@ private CelReference makeReference(CelIdentDecl decl) { } private OverloadResolution resolveOverload( + long callExprId, int position, @Nullable CelFunctionDecl function, @Nullable CelExpr target, @@ -614,6 +655,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 +670,7 @@ private OverloadResolution resolveOverload( } if (resultType == null) { env.reportError( + callExprId, position, "found no matching overload for '%s' applied to '%s'%s", function.name(), @@ -654,7 +697,8 @@ private CelType visitSelectField( if (!Types.isDynOrError(operandType)) { if (operandType.kind() == CelKind.STRUCT) { - TypeProvider.FieldType fieldType = getFieldType(getPosition(expr), operandType, field); + TypeProvider.FieldType fieldType = + getFieldType(expr.id(), getPosition(expr), operandType, field); // Type of the field resultType = fieldType.celType(); } else if (operandType.kind() == CelKind.MAP) { @@ -673,6 +717,7 @@ private CelType visitSelectField( resultType = SimpleType.DYN; } else { env.reportError( + expr.id(), getPosition(expr), "type '%s' does not support field selection", CelTypes.format(operandType)); @@ -693,14 +738,14 @@ private CelExpr visitOptionalCall(CelExpr expr, CelExpr.CelCall call) { 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"); + env.reportError(expr.id(), getPosition(field), "unsupported optional field selection"); return expr; } CelExpr visitedOperand = visit(operand); if (namespacedDeclarations && !operand.equals(visitedOperand)) { // Subtree has been rewritten. Replace the operand. - expr = replaceSelectOperandSubtree(expr, visitedOperand); + expr = replaceCallArgumentSubtree(expr, visitedOperand, 0); } CelType resultType = visitSelectField(expr, operand, field.constant().stringValue(), true); env.setType(expr, resultType); @@ -730,7 +775,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 +788,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), @@ -784,7 +833,7 @@ private CelType unwrapOptional(CelType type) { } private void assertType(CelExpr expr, CelType type) { - assertIsAssignable(getPosition(expr), env.getType(expr), type); + assertIsAssignable(expr.id(), getPosition(expr), env.getType(expr), type); } private int getPosition(CelExpr expr) { @@ -792,7 +841,7 @@ private int getPosition(CelExpr expr) { return pos == null ? 0 : pos; } - private int getPosition(CelExpr.CelCreateStruct.Entry entry) { + private int getPosition(CelExpr.CelStruct.Entry entry) { Integer pos = positionMap.get(entry.id()); return pos == null ? 0 : pos; } @@ -843,33 +892,36 @@ private static CelExpr replaceCallSubtree(CelExpr expr, CelExpr target) { } 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(); + CelExpr.CelList newList = expr.list().toBuilder().setElement(index, element).build(); + return expr.toBuilder().setList(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(); + CelExpr.CelStruct struct = expr.struct(); + CelExpr.CelStruct.Entry newEntry = + struct.entries().get(index).toBuilder().setValue(newValue).build(); + struct = struct.toBuilder().setEntry(index, newEntry).build(); + return expr.toBuilder().setStruct(struct).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(); + CelExpr.CelMap map = expr.map(); + CelExpr.CelMap.Entry newEntry = map.entries().get(index).toBuilder().setKey(newKey).build(); + map = map.toBuilder().setEntry(index, newEntry).build(); + return expr.toBuilder().setMap(map).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(); + CelExpr.CelMap map = expr.map(); + CelExpr.CelMap.Entry newEntry = map.entries().get(index).toBuilder().setValue(newValue).build(); + map = map.toBuilder().setEntry(index, newEntry).build(); + return expr.toBuilder().setMap(map).build(); + } + + private static CelExpr replaceComprehensionAccuInitSubtree(CelExpr expr, CelExpr newAccuInit) { + CelExpr.CelComprehension newComprehension = + expr.comprehension().toBuilder().setAccuInit(newAccuInit).build(); + return expr.toBuilder().setComprehension(newComprehension).build(); } private static CelExpr replaceComprehensionRangeSubtree(CelExpr expr, CelExpr newRange) { diff --git a/checker/src/main/java/dev/cel/checker/ProtoTypeMask.java b/checker/src/main/java/dev/cel/checker/ProtoTypeMask.java index 1a129a91f..c019604d5 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); } @@ -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 07325db00..025dfaf1a 100644 --- a/checker/src/main/java/dev/cel/checker/ProtoTypeMaskTypeProvider.java +++ b/checker/src/main/java/dev/cel/checker/ProtoTypeMaskTypeProvider.java @@ -44,10 +44,10 @@ public final class ProtoTypeMaskTypeProvider implements CelTypeProvider { @SuppressWarnings("Immutable") private final ImmutableMap allTypes; - private final ImmutableList protoTypeMasks; + private final ImmutableSet protoTypeMasks; ProtoTypeMaskTypeProvider( - CelTypeProvider delegateProvider, ImmutableList protoTypeMasks) { + CelTypeProvider delegateProvider, ImmutableSet protoTypeMasks) { this.protoTypeMasks = protoTypeMasks; this.allTypes = computeVisibleFieldsMap(delegateProvider, protoTypeMasks); } @@ -89,14 +89,19 @@ ImmutableList computeDeclsFromProtoTypeMasks() { } private static ImmutableMap computeVisibleFieldsMap( - CelTypeProvider delegateProvider, ImmutableList protoTypeMasks) { + 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 02bfefdf8..000000000 --- a/checker/src/main/java/dev/cel/checker/Standard.java +++ /dev/null @@ -1,782 +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(); - - /** - * 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( - "type", - CelOverloadDecl.newGlobalOverload( - "type", "returns type of value", TypeType.create(SimpleType.DYN), typeParamA))); - - // Conversions to int - celFunctionDeclBuilder.add( - CelFunctionDecl.newFunctionDeclaration( - "int", - 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( - "uint", - 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( - "double", - 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( - "string", - 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( - "list", - CelOverloadDecl.newGlobalOverload( - "to_list", "type conversion", listOfA, TypeType.create(typeParamA), listOfA))); - - // Conversions to map - celFunctionDeclBuilder.add( - CelFunctionDecl.newFunctionDeclaration( - "map", - CelOverloadDecl.newGlobalOverload( - "to_map", - "type conversion", - mapOfAb, - TypeType.create(typeParamA), - TypeType.create(typeParamB), - mapOfAb))); - - // Conversions to bytes - celFunctionDeclBuilder.add( - CelFunctionDecl.newFunctionDeclaration( - "bytes", - CelOverloadDecl.newGlobalOverload( - "string_to_bytes", "type conversion", SimpleType.BYTES, SimpleType.STRING))); - - // Conversions to dyn - celFunctionDeclBuilder.add( - CelFunctionDecl.newFunctionDeclaration( - "dyn", - CelOverloadDecl.newGlobalOverload( - "to_dyn", "type conversion", SimpleType.DYN, typeParamA))); - - // Conversions to Duration - celFunctionDeclBuilder.add( - CelFunctionDecl.newFunctionDeclaration( - "duration", - 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( - "matches", - CelOverloadDecl.newGlobalOverload( - "matches", - "matches first argument against regular expression in second argument", - SimpleType.BOOL, - SimpleType.STRING, - SimpleType.STRING))); - celFunctionDeclBuilder.add( - CelFunctionDecl.newFunctionDeclaration( - "matches", - CelOverloadDecl.newMemberOverload( - "matches_string", - "matches the self argument against regular expression in first argument", - SimpleType.BOOL, - SimpleType.STRING, - SimpleType.STRING))); - - celFunctionDeclBuilder.add( - CelFunctionDecl.newFunctionDeclaration( - "contains", - CelOverloadDecl.newMemberOverload( - "contains_string", - "tests whether the string operand contains the substring", - SimpleType.BOOL, - SimpleType.STRING, - SimpleType.STRING))); - celFunctionDeclBuilder.add( - CelFunctionDecl.newFunctionDeclaration( - "endsWith", - 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( - "startsWith", - 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( - "getFullYear", - 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( - "getMonth", - 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( - "getDayOfYear", - 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( - "getDayOfMonth", - 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( - "getDate", - 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( - "getDayOfWeek", - 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( - "getHours", - 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( - "getMinutes", - 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( - "getSeconds", - 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( - "getMilliseconds", - 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 1498a6b9b..1b8ee9557 100644 --- a/checker/src/test/java/dev/cel/checker/BUILD.bazel +++ b/checker/src/test/java/dev/cel/checker/BUILD.bazel @@ -1,3 +1,4 @@ +load("@rules_java//java:defs.bzl", "java_library") load("//:testing.bzl", "junit4_test_suites") package(default_applicable_licenses = [ @@ -14,24 +15,27 @@ java_library( "//: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: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", @@ -41,13 +45,15 @@ java_library( "//parser:operator", "//testing:adorner", "//testing:cel_baseline_test_case", - "@maven//:com_google_errorprone_error_prone_annotations", - "@maven//:org_jspecify_jspecify", "@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 new file mode 100644 index 000000000..c0c54381d --- /dev/null +++ b/checker/src/test/java/dev/cel/checker/CelCheckerLegacyImplTest.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.checker; + +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.expr.conformance.proto3.TestAllTypes; +import java.util.Optional; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class CelCheckerLegacyImplTest { + + @Test + public void toCheckerBuilder_isNewInstance() { + CelCheckerBuilder celCheckerBuilder = CelCompilerFactory.standardCelCheckerBuilder(); + CelCheckerLegacyImpl celChecker = (CelCheckerLegacyImpl) celCheckerBuilder.build(); + + CelCheckerLegacyImpl.Builder newCheckerBuilder = + (CelCheckerLegacyImpl.Builder) celChecker.toCheckerBuilder(); + + assertThat(newCheckerBuilder).isNotEqualTo(celCheckerBuilder); + } + + @Test + public void toCheckerBuilder_isImmutable() { + CelCheckerBuilder originalCheckerBuilder = CelCompilerFactory.standardCelCheckerBuilder(); + CelCheckerLegacyImpl celChecker = (CelCheckerLegacyImpl) originalCheckerBuilder.build(); + originalCheckerBuilder.addLibraries(new CelCheckerLibrary() {}); + + CelCheckerLegacyImpl.Builder newCheckerBuilder = + (CelCheckerLegacyImpl.Builder) celChecker.toCheckerBuilder(); + + assertThat(newCheckerBuilder.checkerLibraries().build()).isEmpty(); + } + + @Test + public void toCheckerBuilder_singularFields_copied() { + CelStandardDeclarations subsetDecls = + CelStandardDeclarations.newBuilder().includeFunctions(StandardFunction.BOOL).build(); + CelOptions celOptions = CelOptions.current().enableTimestampEpoch(true).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 + public void toCheckerBuilder_collectionProperties_copied() { + CelCheckerBuilder celCheckerBuilder = + CelCompilerFactory.standardCelCheckerBuilder() + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "test", CelOverloadDecl.newGlobalOverload("test_id", SimpleType.INT))) + .addVarDeclarations(CelVarDecl.newVarDeclaration("ident", SimpleType.INT)) + .addMessageTypes(TestAllTypes.getDescriptor()) + .addFileTypes(TestAllTypes.getDescriptor().getFile()) + .addProtoTypeMasks( + ProtoTypeMask.ofAllFields("cel.expr.conformance.proto3.TestAllTypes")) + .addLibraries(new CelCheckerLibrary() {}); + CelCheckerLegacyImpl celChecker = (CelCheckerLegacyImpl) celCheckerBuilder.build(); + + CelCheckerLegacyImpl.Builder newCheckerBuilder = + (CelCheckerLegacyImpl.Builder) celChecker.toCheckerBuilder(); + + 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 + public void toCheckerBuilder_collectionProperties_areImmutable() { + CelCheckerBuilder celCheckerBuilder = CelCompilerFactory.standardCelCheckerBuilder(); + CelCheckerLegacyImpl celChecker = (CelCheckerLegacyImpl) celCheckerBuilder.build(); + CelCheckerLegacyImpl.Builder newCheckerBuilder = + (CelCheckerLegacyImpl.Builder) celChecker.toCheckerBuilder(); + + // Mutate the original builder containing collections + celCheckerBuilder.addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "test", CelOverloadDecl.newGlobalOverload("test_id", SimpleType.INT))); + celCheckerBuilder.addVarDeclarations(CelVarDecl.newVarDeclaration("ident", SimpleType.INT)); + celCheckerBuilder.addMessageTypes(TestAllTypes.getDescriptor()); + celCheckerBuilder.addFileTypes(TestAllTypes.getDescriptor().getFile()); + celCheckerBuilder.addProtoTypeMasks( + ProtoTypeMask.ofAllFields("cel.expr.conformance.proto3.TestAllTypes")); + celCheckerBuilder.addLibraries(new CelCheckerLibrary() {}); + + 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/CelCompilerImplTest.java b/checker/src/test/java/dev/cel/checker/CelCompilerImplTest.java new file mode 100644 index 000000000..b3abc4c6b --- /dev/null +++ b/checker/src/test/java/dev/cel/checker/CelCompilerImplTest.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.checker; + +import static com.google.common.truth.Truth.assertThat; + +import dev.cel.common.CelFunctionDecl; +import dev.cel.common.CelOverloadDecl; +import dev.cel.common.types.SimpleType; +import dev.cel.compiler.CelCompilerBuilder; +import dev.cel.compiler.CelCompilerFactory; +import dev.cel.compiler.CelCompilerImpl; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class CelCompilerImplTest { + + @Test + public void toCompilerBuilder_isImmutable() { + CelCompilerBuilder celCompilerBuilder = CelCompilerFactory.standardCelCompilerBuilder(); + CelCompilerImpl celCompiler = (CelCompilerImpl) celCompilerBuilder.build(); + celCompilerBuilder.addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "test", CelOverloadDecl.newGlobalOverload("test_id", SimpleType.INT))); + + CelCompilerImpl.Builder newCompilerBuilder = + (CelCompilerImpl.Builder) celCompiler.toCompilerBuilder(); + + assertThat(newCompilerBuilder).isNotEqualTo(celCompilerBuilder); + } +} diff --git a/checker/src/test/java/dev/cel/checker/CelFunctionDeclTest.java b/checker/src/test/java/dev/cel/checker/CelFunctionDeclTest.java index fe386d8c7..662761f41 100644 --- a/checker/src/test/java/dev/cel/checker/CelFunctionDeclTest.java +++ b/checker/src/test/java/dev/cel/checker/CelFunctionDeclTest.java @@ -22,6 +22,7 @@ import dev.cel.common.CelFunctionDecl; import dev.cel.common.CelOverloadDecl; import dev.cel.common.types.SimpleType; +import java.util.Iterator; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -38,7 +39,7 @@ public void declareGlobalFunction_success() { assertThat(functionDecl.name()).isEqualTo("testGlobalFunction"); assertThat(functionDecl.overloads()).hasSize(1); - CelOverloadDecl overloadDecl = functionDecl.overloads().get(0); + CelOverloadDecl overloadDecl = functionDecl.overloads().iterator().next(); assertThat(overloadDecl.overloadId()).isEqualTo("overloadId"); assertThat(overloadDecl.isInstanceFunction()).isFalse(); assertThat(overloadDecl.resultType()).isEqualTo(SimpleType.BOOL); @@ -54,7 +55,7 @@ public void declareMemberFunction_success() { assertThat(functionDecl.name()).isEqualTo("testMemberFunction"); assertThat(functionDecl.overloads()).hasSize(1); - CelOverloadDecl overloadDecl = functionDecl.overloads().get(0); + CelOverloadDecl overloadDecl = functionDecl.overloads().iterator().next(); assertThat(overloadDecl.overloadId()).isEqualTo("overloadId"); assertThat(overloadDecl.isInstanceFunction()).isTrue(); assertThat(overloadDecl.resultType()).isEqualTo(SimpleType.TIMESTAMP); @@ -74,13 +75,14 @@ public void declareFunction_withBuilder_success() { assertThat(functionDecl.name()).isEqualTo("testFunction"); assertThat(functionDecl.overloads()).hasSize(2); - CelOverloadDecl memberOverloadDecl = functionDecl.overloads().get(0); + Iterator iterator = functionDecl.overloads().iterator(); + CelOverloadDecl memberOverloadDecl = iterator.next(); assertThat(memberOverloadDecl.overloadId()).isEqualTo("memberOverloadId"); assertThat(memberOverloadDecl.isInstanceFunction()).isTrue(); assertThat(memberOverloadDecl.resultType()).isEqualTo(SimpleType.INT); assertThat(memberOverloadDecl.parameterTypes()).containsExactly(SimpleType.UINT); - CelOverloadDecl globalOverloadDecl = functionDecl.overloads().get(1); + CelOverloadDecl globalOverloadDecl = iterator.next(); assertThat(globalOverloadDecl.overloadId()).isEqualTo("globalOverloadId"); assertThat(globalOverloadDecl.isInstanceFunction()).isFalse(); assertThat(globalOverloadDecl.resultType()).isEqualTo(SimpleType.STRING); diff --git a/checker/src/test/java/dev/cel/checker/CelIdentDeclTest.java b/checker/src/test/java/dev/cel/checker/CelIdentDeclTest.java index 424d83452..6dd221ae1 100644 --- a/checker/src/test/java/dev/cel/checker/CelIdentDeclTest.java +++ b/checker/src/test/java/dev/cel/checker/CelIdentDeclTest.java @@ -15,7 +15,6 @@ package dev.cel.checker; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth8.assertThat; import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; import dev.cel.expr.Constant; 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..f4b389552 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.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(); @@ -215,7 +216,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(); 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 461eb3574..360a8c64a 100644 --- a/checker/src/test/java/dev/cel/checker/ExprCheckerTest.java +++ b/checker/src/test/java/dev/cel/checker/ExprCheckerTest.java @@ -14,67 +14,67 @@ 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( + TestAllTypes.getDescriptor(), + dev.cel.expr.conformance.proto2.TestAllTypes.getDescriptor())); if (ast != null) { testOutput() .println( @@ -87,10 +87,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 +107,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 +179,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 +190,22 @@ 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(); } @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 +220,43 @@ 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 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 +266,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 +278,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 +300,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 +315,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 +329,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 +348,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 +371,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 +395,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 +405,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 +431,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 +447,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 " @@ -415,15 +478,14 @@ public void jsonType() throws Exception { @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 +496,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 +510,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 +518,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 +539,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 +549,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 +610,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 +622,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(); } @@ -652,6 +711,8 @@ public void unexpectedAggregateMapError() throws Exception { public void types() throws Exception { source = "list == type([1]) && map == type({1:2u})"; runTest(); + source = "{}.map(c,[c,type(c)])"; + runTest(); } // Enum Values @@ -659,19 +720,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(); @@ -682,7 +743,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(); } @@ -692,7 +753,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(); @@ -722,7 +783,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) " @@ -731,9 +792,84 @@ 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(); @@ -741,7 +877,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(); @@ -755,16 +891,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(); } @@ -774,15 +910,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(); @@ -790,36 +925,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. @@ -829,26 +950,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(); @@ -856,33 +968,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(); } @@ -890,12 +992,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(); @@ -903,7 +1005,7 @@ public void optionals() throws Exception { runTest(); clearAllDeclarations(); - declareVariable("d", createOptionalType(CelTypes.DYN)); + declareVariable("d", OptionalType.create(SimpleType.DYN)); source = "d.dynamic"; runTest(); @@ -911,7 +1013,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(); @@ -922,13 +1024,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(); @@ -947,33 +1049,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 a2a9f88ef..b4b52bd26 100644 --- a/checker/src/test/java/dev/cel/checker/ProtoTypeMaskTypeProviderTest.java +++ b/checker/src/test/java/dev/cel/checker/ProtoTypeMaskTypeProviderTest.java @@ -17,10 +17,8 @@ import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableSet.toImmutableSet; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth8.assertThat; import static org.junit.Assert.assertThrows; -import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.protobuf.FieldMask; import com.google.rpc.context.AttributeContext; @@ -30,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; @@ -53,7 +52,7 @@ public void protoTypeMaskProvider_badFieldMask() { () -> new ProtoTypeMaskTypeProvider( celTypeProvider, - ImmutableList.of( + ImmutableSet.of( ProtoTypeMask.of( "google.rpc.context.AttributeContext", FieldMask.newBuilder().addPaths("missing").build())))); @@ -63,18 +62,18 @@ public void protoTypeMaskProvider_badFieldMask() { public void lookupFieldNames_undeclaredMessageType() { CelTypeProvider celTypeProvider = new ProtoMessageTypeProvider(); ProtoTypeMaskTypeProvider protoTypeMaskProvider = - new ProtoTypeMaskTypeProvider(celTypeProvider, ImmutableList.of()); + new ProtoTypeMaskTypeProvider(celTypeProvider, ImmutableSet.of()); assertThat(protoTypeMaskProvider.findType(ATTRIBUTE_CONTEXT_TYPE)).isEmpty(); } @Test public void lookupFieldNames_noProtoDecls() { CelTypeProvider celTypeProvider = - new ProtoMessageTypeProvider(ImmutableList.of(AttributeContext.getDescriptor())); + new ProtoMessageTypeProvider(ImmutableSet.of(AttributeContext.getDescriptor())); ProtoTypeMaskTypeProvider protoTypeMaskProvider = - new ProtoTypeMaskTypeProvider(celTypeProvider, ImmutableList.of()); + 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", @@ -89,13 +88,48 @@ 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 = - new ProtoMessageTypeProvider(ImmutableList.of(AttributeContext.getDescriptor())); + new ProtoMessageTypeProvider(ImmutableSet.of(AttributeContext.getDescriptor())); ProtoTypeMaskTypeProvider protoTypeMaskProvider = new ProtoTypeMaskTypeProvider( - celTypeProvider, ImmutableList.of(ProtoTypeMask.ofAllFields(ATTRIBUTE_CONTEXT_TYPE))); + celTypeProvider, ImmutableSet.of(ProtoTypeMask.ofAllFields(ATTRIBUTE_CONTEXT_TYPE))); ProtoMessageType protoType = assertTypeFound(protoTypeMaskProvider, ATTRIBUTE_CONTEXT_TYPE); assertTypeHasFields( protoType, @@ -115,11 +149,11 @@ public void lookupFieldNames_fullProtoDecl() { @Test public void lookupFieldNames_partialProtoDecl() { CelTypeProvider celTypeProvider = - new ProtoMessageTypeProvider(ImmutableList.of(AttributeContext.getDescriptor())); + new ProtoMessageTypeProvider(ImmutableSet.of(AttributeContext.getDescriptor())); ProtoTypeMaskTypeProvider protoTypeMaskProvider = new ProtoTypeMaskTypeProvider( celTypeProvider, - ImmutableList.of( + ImmutableSet.of( ProtoTypeMask.of( "google.rpc.context.AttributeContext", FieldMask.newBuilder() @@ -170,7 +204,7 @@ public void computeDecls() { ProtoTypeMaskTypeProvider protoTypeMaskProvider = new ProtoTypeMaskTypeProvider( celTypeProvider, - ImmutableList.of( + ImmutableSet.of( ProtoTypeMask.of( "google.rpc.context.AttributeContext", FieldMask.newBuilder() @@ -192,11 +226,11 @@ public void computeDecls() { @Test public void lookupFieldType() { CelTypeProvider celTypeProvider = - new ProtoMessageTypeProvider(ImmutableList.of(AttributeContext.getDescriptor())); + new ProtoMessageTypeProvider(ImmutableSet.of(AttributeContext.getDescriptor())); ProtoTypeMaskTypeProvider protoTypeMaskProvider = new ProtoTypeMaskTypeProvider( celTypeProvider, - ImmutableList.of( + ImmutableSet.of( ProtoTypeMask.of( "google.rpc.context.AttributeContext", FieldMask.newBuilder() @@ -229,11 +263,11 @@ public void lookupFieldType() { @Test public void lookupFieldType_notExposedField() { CelTypeProvider celTypeProvider = - new ProtoMessageTypeProvider(ImmutableList.of(AttributeContext.getDescriptor())); + new ProtoMessageTypeProvider(ImmutableSet.of(AttributeContext.getDescriptor())); ProtoTypeMaskTypeProvider protoTypeMaskProvider = new ProtoTypeMaskTypeProvider( celTypeProvider, - ImmutableList.of( + ImmutableSet.of( ProtoTypeMask.of( "google.rpc.context.AttributeContext", FieldMask.newBuilder().addPaths("resource.name").build()))); @@ -244,11 +278,11 @@ public void lookupFieldType_notExposedField() { @Test public void lookupType_notExposed() { CelTypeProvider celTypeProvider = - new ProtoMessageTypeProvider(ImmutableList.of(AttributeContext.getDescriptor())); + new ProtoMessageTypeProvider(ImmutableSet.of(AttributeContext.getDescriptor())); ProtoTypeMaskTypeProvider protoTypeMaskProvider = new ProtoTypeMaskTypeProvider( celTypeProvider, - ImmutableList.of( + ImmutableSet.of( ProtoTypeMask.of( "google.rpc.context.AttributeContext", FieldMask.newBuilder().addPaths("resource.name").build()))); @@ -265,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/TypeInferencerTest.java b/checker/src/test/java/dev/cel/checker/TypeInferencerTest.java index efdd5a758..ee167def7 100644 --- a/checker/src/test/java/dev/cel/checker/TypeInferencerTest.java +++ b/checker/src/test/java/dev/cel/checker/TypeInferencerTest.java @@ -15,7 +15,6 @@ package dev.cel.checker; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth8.assertThat; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; 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/abstractTypeParameterLess.baseline b/checker/src/test/resources/abstractTypeParameterLess.baseline index 8f1e0ea0a..10cfae312 100644 --- a/checker/src/test/resources/abstractTypeParameterLess.baseline +++ b/checker/src/test/resources/abstractTypeParameterLess.baseline @@ -13,9 +13,8 @@ _&&_( _==_( type( make_abs()~abs^make_abs - )~type(dyn)^type, + )~type(abs)^type, abs~type(abs)^abs )~bool^equals, make_abs()~abs^make_abs.as_bool()~bool^as_bool )~bool^logical_and - diff --git a/checker/src/test/resources/abstractTypeParameterized.baseline b/checker/src/test/resources/abstractTypeParameterized.baseline index c1e65941b..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,21 +14,20 @@ _&&_( [ 1~int ]~list(int) - )~vector(int)^vector - )~type(dyn)^type, + )~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..7bace8021 100644 --- a/checker/src/test/resources/aggregateMessage.baseline +++ b/checker/src/test/resources/aggregateMessage.baseline @@ -3,5 +3,5 @@ Source: TestAllTypes{single_int32: 1, single_int64: 2} 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 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/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/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..211fbf4c0 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,12 +18,12 @@ 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 @@ -31,7 +31,7 @@ declare single_nested_enum { =====> _==_( 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, + 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 diff --git a/checker/src/test/resources/nullableMessage.baseline b/checker/src/test/resources/nullableMessage.baseline index e2db74762..6b5c13a68 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 + 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, + TestAllTypes{}~cel.expr.conformance.proto3.TestAllTypes^cel.expr.conformance.proto3.TestAllTypes, null~null )~bool^equals )~bool^logical_or 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..a86fb1fc7 100644 --- a/checker/src/test/resources/optionals.baseline +++ b/checker/src/test/resources/optionals.baseline @@ -77,7 +77,7 @@ TestAllTypes{ {}~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 { 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..7680854d4 100644 --- a/checker/src/test/resources/referenceTypeAbsolute.baseline +++ b/checker/src/test/resources/referenceTypeAbsolute.baseline @@ -1,3 +1,3 @@ -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 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 028a9188c..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) - function type (A) -> 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 b58cd143c..939e0ed97 100644 --- a/checker/src/test/resources/types.baseline +++ b/checker/src/test/resources/types.baseline @@ -7,7 +7,7 @@ _&&_( [ 1~int ]~list(int) - )~type(dyn)^type + )~type(list(int))^type )~bool^equals, _==_( map~type(map(dyn, dyn))^map, @@ -15,7 +15,34 @@ _&&_( { 1~int:2u~uint }~map(int, uint) - )~type(dyn)^type + )~type(map(int, uint))^type )~bool^equals )~bool^logical_and +Source: {}.map(c,[c,type(c)]) +=====> +__comprehension__( + // Variable + c, + // Target + {}~map(dyn, dyn), + // Accumulator + @result, + // Init + []~list(list(dyn)), + // LoopCondition + true~bool, + // LoopStep + _+_( + @result~list(list(dyn))^@result, + [ + [ + c~dyn^c, + type( + c~dyn^c + )~type(dyn)^type + ]~list(dyn) + ]~list(list(dyn)) + )~list(list(dyn))^add_list, + // Result + @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..6de1a2fb0 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. @@ -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: Failed to" + + " parse timestamp: invalid timestamp \"bad\"\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..9ebbc505a --- /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: Failed to" + + " parse timestamp: invalid timestamp \"bad\"\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..ac39340e0 --- /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: Failed to" + + " parse timestamp: invalid timestamp \"bad\"\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..77a336dbf 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,84 @@ 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 = "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 = "runtime_exception", - visibility = ["//visibility:public"], + # used_by_android + visibility = ["//:internal"], exports = ["//common/src/main/java/dev/cel/common:runtime_exception"], ) + +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_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", + exports = ["//common/src/main/java/dev/cel/common:cel_descriptors"], +) 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/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 95b367684..a39bb9365 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( @@ -13,8 +16,14 @@ java_library( 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 +32,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 +62,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"], @@ -67,3 +91,49 @@ java_library( name = "cel_descriptor_pools", 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_java_qualified_names", + exports = ["//common/src/main/java/dev/cel/common/internal:proto_java_qualified_names"], +) + +java_library( + name = "proto_time_utils", + exports = ["//common/src/main/java/dev/cel/common/internal:proto_time_utils"], +) + +cel_android_library( + name = "proto_time_utils_android", + exports = ["//common/src/main/java/dev/cel/common/internal:proto_time_utils_android"], +) 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 dd39523fc..255ae5d8d 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,35 @@ COMPILER_COMMON_SOURCES = [ "CelVarDecl.java", ] +# keep sorted +SOURCE_SOURCES = [ + "CelSourceHelper.java", + "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_descriptors", + srcs = [ + "CelDescriptorUtil.java", + "CelDescriptors.java", + ], tags = [ ], deps = [ - ":error_codes", "//:auto_value", "//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_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", @@ -61,44 +60,79 @@ java_library( tags = [ ], deps = [ - ":common", + ":cel_ast", + ":cel_exception", + ":cel_source", + ":source", + ":source_location", "//:auto_value", "//common/annotations", - "//common/types:cel_types", + "//common/internal:safe_string_formatter", + "//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( @@ -107,7 +141,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", @@ -119,6 +154,7 @@ java_library( java_library( name = "error_codes", srcs = ["CelErrorCode.java"], + # used_by_android tags = [ ], ) @@ -126,6 +162,7 @@ java_library( java_library( name = "runtime_exception", srcs = ["CelRuntimeException.java"], + # used_by_android tags = [ ], deps = [ @@ -133,3 +170,173 @@ java_library( "//common/annotations", ], ) + +java_library( + name = "mutable_ast", + srcs = ["CelMutableAst.java"], + tags = [ + ], + deps = [ + ":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", + "//:auto_value", + "//common/ast:mutable_expr", + "@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: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 = [ + ":source", + ":source_location", + "//:auto_value", + "//common/annotations", + "//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 = [ + ":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_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 = [ + ":source_location", + "//common/annotations", + "//common/internal", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "source_android", + srcs = SOURCE_SOURCES, + visibility = ["//visibility:private"], + deps = [ + ":source_location_android", + "//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", + ], +) diff --git a/common/src/main/java/dev/cel/common/CelAbstractSyntaxTree.java b/common/src/main/java/dev/cel/common/CelAbstractSyntaxTree.java index 3d55bde38..6b3b6a74f 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,27 @@ 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 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 +121,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..6d9fcd612 --- /dev/null +++ b/common/src/main/java/dev/cel/common/CelContainer.java @@ -0,0 +1,306 @@ +// 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 com.google.auto.value.AutoValue; +import com.google.common.base.Preconditions; +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.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 aliases(); + + /** Builder for {@link CelContainer} */ + @AutoValue.Builder + public abstract static class Builder { + + private final LinkedHashMap aliases = new LinkedHashMap<>(); + + abstract String name(); + + /** Sets the fully-qualified name of the container. */ + public abstract Builder setName(String name); + + abstract Builder setAliases(ImmutableMap aliases); + + /** 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. + */ + @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); + aliases.put(alias, 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)); + } + + int index = qualifiedName.lastIndexOf("."); + if (index <= 0 || index == qualifiedName.length() - 1) { + throw new IllegalArgumentException( + String.format("%s must refer to a valid qualified name: %s", kind, qualifiedName)); + } + + String aliasRef = aliases.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)); + } + + 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() { + setAliases(ImmutableMap.copyOf(aliases)); + 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.aliases.putAll(aliases()); + 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); + } + String alias = aliases().get(simple); + if (alias == null) { + return Optional.empty(); + } + + return Optional.of(alias + qualifier); + } + + private static boolean isIdentifierChar(int r) { + if (r > 127) { + // Not ASCII + return false; + } + + return r == '.' || r == '_' || Character.isLetter(r) || Character.isDigit(r); + } + + private enum AliasKind { + ALIAS, + ABBREVIATION; + + @Override + public String toString() { + return this.name().toLowerCase(Locale.getDefault()); + } + } +} diff --git a/common/src/main/java/dev/cel/common/CelDescriptorUtil.java b/common/src/main/java/dev/cel/common/CelDescriptorUtil.java index 28aff8291..db1f22120 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}. diff --git a/common/src/main/java/dev/cel/common/CelException.java b/common/src/main/java/dev/cel/common/CelException.java index 55c8623a4..9d80a9ba1 100644 --- a/common/src/main/java/dev/cel/common/CelException.java +++ b/common/src/main/java/dev/cel/common/CelException.java @@ -27,6 +27,11 @@ public CelException(String message, Throwable cause) { super(message, cause); } + public CelException(String message, CelErrorCode errorCode) { + super(message); + this.errorCode = errorCode; + } + public CelException(String message, Throwable cause, CelErrorCode errorCode) { super(message, cause); this.errorCode = errorCode; diff --git a/common/src/main/java/dev/cel/common/CelFunctionDecl.java b/common/src/main/java/dev/cel/common/CelFunctionDecl.java index 68b5ff406..12beb53d7 100644 --- a/common/src/main/java/dev/cel/common/CelFunctionDecl.java +++ b/common/src/main/java/dev/cel/common/CelFunctionDecl.java @@ -20,7 +20,7 @@ import dev.cel.expr.Decl; import dev.cel.expr.Decl.FunctionDecl; 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 com.google.errorprone.annotations.CheckReturnValue; import com.google.errorprone.annotations.Immutable; @@ -36,7 +36,7 @@ public abstract class CelFunctionDecl { public abstract String name(); /** Required. List of function overloads. Must contain at least one overload. */ - public abstract ImmutableList overloads(); + public abstract ImmutableSet overloads(); /** Builder for configuring the {@link CelFunctionDecl}. */ @AutoValue.Builder @@ -46,12 +46,12 @@ public abstract static class Builder { /** Sets the function name {@link #name()} */ public abstract Builder setName(String name); - public abstract ImmutableList overloads(); + public abstract ImmutableSet overloads(); - public abstract ImmutableList.Builder overloadsBuilder(); + public abstract ImmutableSet.Builder overloadsBuilder(); @CanIgnoreReturnValue - public abstract Builder setOverloads(ImmutableList overloads); + public abstract Builder setOverloads(ImmutableSet overloads); /** Adds one or more function overloads */ @CanIgnoreReturnValue @@ -77,7 +77,7 @@ public Builder addOverloads(Iterable overloads) { /** Create a new builder to construct a {@code CelFunctionDecl} instance. */ public static Builder newBuilder() { - return new AutoValue_CelFunctionDecl.Builder().setOverloads(ImmutableList.of()); + return new AutoValue_CelFunctionDecl.Builder().setOverloads(ImmutableSet.of()); } /** Constructs a function declaration with any number of {@link CelOverloadDecl} */ diff --git a/common/src/main/java/dev/cel/common/CelIssue.java b/common/src/main/java/dev/cel/common/CelIssue.java index 4ad48ceac..4de369470 100644 --- a/common/src/main/java/dev/cel/common/CelIssue.java +++ b/common/src/main/java/dev/cel/common/CelIssue.java @@ -14,9 +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; @@ -27,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 { @@ -45,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); @@ -72,11 +90,17 @@ 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 = - String.format( + SafeStringFormatter.format( "%s: %s:%d:%d: %s", getSeverity(), source.getDescription(), @@ -125,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..fd3fe28f8 --- /dev/null +++ b/common/src/main/java/dev/cel/common/CelMutableAst.java @@ -0,0 +1,111 @@ +// 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 CelAbstractSyntaxTree.newParsedAst( + CelMutableExprConverter.fromMutableExpr(mutatedExpr), source.toCelSource()); + } + + /** + * 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..459042c6d --- /dev/null +++ b/common/src/main/java/dev/cel/common/CelMutableSource.java @@ -0,0 +1,129 @@ +// 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.errorprone.annotations.CanIgnoreReturnValue; +import dev.cel.common.CelSource.Extension; +import dev.cel.common.ast.CelMutableExpr; +import dev.cel.common.ast.CelMutableExprConverter; +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; + + @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() { + return CelSource.newBuilder() + .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<>()); + } + + 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()); + } + + CelMutableSource( + String description, Map macroCalls, Set extensions) { + this.description = checkNotNull(description); + this.macroCalls = checkNotNull(macroCalls); + this.extensions = checkNotNull(extensions); + } +} diff --git a/common/src/main/java/dev/cel/common/CelOptions.java b/common/src/main/java/dev/cel/common/CelOptions.java index eaa812e2b..23654235d 100644 --- a/common/src/main/java/dev/cel/common/CelOptions.java +++ b/common/src/main/java/dev/cel/common/CelOptions.java @@ -15,9 +15,9 @@ 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; +import dev.cel.common.annotations.Beta; /** * Options to configure how the CEL parser, type-checker, and evaluator behave. @@ -30,6 +30,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 +67,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(); @@ -71,6 +87,8 @@ public abstract class CelOptions { public abstract boolean disableCelStandardEquality(); + public abstract boolean enableShortCircuiting(); + public abstract boolean enableRegexPartialMatch(); public abstract boolean enableUnsignedComparisonAndArithmeticIsUnsigned(); @@ -91,61 +109,23 @@ 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 enableStringConversion(); + + public abstract boolean enableStringConcatenation(); + + public abstract boolean enableListConcatenation(); + + 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,6 +142,8 @@ public static Builder newBuilder() { .populateMacroCalls(false) .retainRepeatedUnaryOperators(false) .retainUnbalancedLogicalExpressions(false) + .enableHiddenAccumulatorVar(true) + .enableQuotedIdentifierSyntax(false) // Type-Checker options .enableCompileTimeOverloadResolution(false) .enableHomogeneousLiterals(false) @@ -170,6 +152,8 @@ public static Builder newBuilder() { .enableNamespacedDeclarations(true) // Evaluation options .disableCelStandardEquality(true) + .evaluateCanonicalTypesToNativeValues(false) + .enableShortCircuiting(true) .enableRegexPartialMatch(false) .enableUnsignedComparisonAndArithmeticIsUnsigned(false) .enableUnsignedLongs(false) @@ -179,7 +163,14 @@ public static Builder newBuilder() { .resolveTypeDependencies(true) .enableUnknownTracking(false) .enableCelValue(false) - .comprehensionMaxIterations(-1); + .comprehensionMaxIterations(-1) + .unwrapWellKnownTypesOnFunctionDispatch(true) + .fromProtoUnsetFieldOption(ProtoUnsetFieldOptions.BIND_DEFAULT) + .enableStringConversion(true) + .enableStringConcatenation(true) + .enableListConcatenation(true) + .enableComprehension(true) + .maxRegexProgramSize(-1); } /** @@ -190,6 +181,7 @@ public static Builder current() { return newBuilder() .enableReservedIds(true) .enableUnsignedComparisonAndArithmeticIsUnsigned(true) + .enableUnsignedLongs(true) .enableRegexPartialMatch(true) .errorOnDuplicateMapKeys(true) .errorOnIntWrap(true) @@ -197,33 +189,6 @@ public static Builder current() { .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 +250,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 /** @@ -364,6 +349,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 +380,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); /** @@ -438,6 +438,7 @@ public abstract static class Builder { * *

Warning: This option is experimental. */ + @Beta public abstract Builder enableCelValue(boolean value); /** @@ -452,6 +453,76 @@ 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}. + *
+ */ + 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 string() overloads for the runtime. This option exists to maintain parity with + * cel-cpp interpreter options. + */ + public abstract Builder enableStringConversion(boolean value); + + /** + * Enables string concatenation overload for the runtime. This option exists to maintain parity + * with cel-cpp interpreter options. + */ + public abstract Builder enableStringConcatenation(boolean value); + + /** + * Enables list concatenation overload for the runtime. This option exists to maintain parity + * with cel-cpp interpreter options. + */ + public abstract Builder enableListConcatenation(boolean 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); + 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 247f86bc1..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; @@ -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 fa0719e87..7230c80af 100644 --- a/common/src/main/java/dev/cel/common/CelProtoAbstractSyntaxTree.java +++ b/common/src/main/java/dev/cel/common/CelProtoAbstractSyntaxTree.java @@ -15,16 +15,22 @@ package dev.cel.common; import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableMap.toImmutableMap; import dev.cel.expr.CheckedExpr; import dev.cel.expr.Expr; import dev.cel.expr.ParsedExpr; import dev.cel.expr.SourceInfo; +import dev.cel.expr.SourceInfo.Extension; +import dev.cel.expr.SourceInfo.Extension.Component; +import dev.cel.expr.SourceInfo.Extension.Version; import dev.cel.expr.Type; +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; /** @@ -49,6 +55,9 @@ private CelProtoAbstractSyntaxTree(CheckedExpr checkedExpr) { .addAllMacroCalls( CelExprConverter.exprMacroCallsToCelExprMacroCalls( checkedExpr.getSourceInfo().getMacroCallsMap())) + .addAllExtensions( + fromExprExtensionsToCelExtensions( + checkedExpr.getSourceInfo().getExtensionsList())) .setDescription(checkedExpr.getSourceInfo().getLocation()) .build(), checkedExpr.getReferenceMapMap().entrySet().stream() @@ -57,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) { @@ -69,6 +79,8 @@ private CelProtoAbstractSyntaxTree(CelAbstractSyntaxTree ast) { SourceInfo.newBuilder() .setLocation(ast.getSource().getDescription()) .addAllLineOffsets(ast.getSource().getLineOffsets()) + .addAllExtensions( + fromCelExtensionsToExprExtensions(ast.getSource().getExtensions())) .putAllMacroCalls( ast.getSource().getMacroCalls().entrySet().stream() .collect( @@ -87,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() @@ -126,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 @@ -135,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. */ @@ -148,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 @@ -157,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 @@ -171,7 +184,71 @@ public ParsedExpr toParsedExpr() { */ @CheckReturnValue public Type getProtoResultType() { - return CelTypes.celTypeToType(ast.getResultType()); + return CelProtoTypes.celTypeToType(ast.getResultType()); + } + + private static ImmutableList fromCelExtensionsToExprExtensions( + Collection extensions) { + return extensions.stream() + .map( + celSourceExtension -> + Extension.newBuilder() + .setId(celSourceExtension.id()) + .setVersion( + Version.newBuilder() + .setMajor(celSourceExtension.version().major()) + .setMinor(celSourceExtension.version().minor())) + .addAllAffectedComponents( + celSourceExtension.affectedComponents().stream() + .map( + component -> { + switch (component) { + case COMPONENT_UNSPECIFIED: + return Component.COMPONENT_UNSPECIFIED; + case COMPONENT_PARSER: + return Component.COMPONENT_PARSER; + case COMPONENT_TYPE_CHECKER: + return Component.COMPONENT_TYPE_CHECKER; + case COMPONENT_RUNTIME: + return Component.COMPONENT_RUNTIME; + } + throw new AssertionError( + "Unexpected component kind: " + component); + }) + .collect(toImmutableList())) + .build()) + .collect(toImmutableList()); + } + + private static ImmutableList fromExprExtensionsToCelExtensions( + Collection extensions) { + return extensions.stream() + .map( + exprExtension -> + CelSource.Extension.create( + exprExtension.getId(), + CelSource.Extension.Version.of( + exprExtension.getVersion().getMajor(), + exprExtension.getVersion().getMinor()), + exprExtension.getAffectedComponentsList().stream() + .map( + component -> { + switch (component) { + case COMPONENT_UNSPECIFIED: + return CelSource.Extension.Component.COMPONENT_UNSPECIFIED; + case COMPONENT_PARSER: + return CelSource.Extension.Component.COMPONENT_PARSER; + case COMPONENT_TYPE_CHECKER: + return CelSource.Extension.Component.COMPONENT_TYPE_CHECKER; + case COMPONENT_RUNTIME: + return CelSource.Extension.Component.COMPONENT_RUNTIME; + case UNRECOGNIZED: + // fall-through + } + throw new AssertionError("Unexpected component kind: " + component); + }) + .collect(toImmutableList()))) + .collect(toImmutableList()); } } // LINT.ThenChange(CelProtoV1Alpha1AbstractSyntaxTree.java) 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..e0afb8c4e --- /dev/null +++ b/common/src/main/java/dev/cel/common/CelProtoJsonAdapter.java @@ -0,0 +1,166 @@ +// 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.ProtoTimeUtils; +import dev.cel.common.values.CelByteString; +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 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 88ab19c65..2678a0a2c 100644 --- a/common/src/main/java/dev/cel/common/CelSource.java +++ b/common/src/main/java/dev/cel/common/CelSource.java @@ -16,16 +16,19 @@ 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.common.base.Splitter; +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.CheckReturnValue; import com.google.errorprone.annotations.Immutable; import dev.cel.common.ast.CelExpr; import dev.cel.common.internal.CelCodePointArray; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -33,34 +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 CelSource(Builder builder) { - codePoints = checkNotNull(builder.codePoints); - description = checkNotNull(builder.description); - positions = checkNotNull(builder.positions.buildOrThrow()); - lineOffsets = checkNotNull(ImmutableList.copyOf(builder.lineOffsets)); - macroCalls = checkNotNull(ImmutableMap.copyOf(builder.macroCalls)); - } +@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(); } /** @@ -70,11 +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 ImmutableSet getExtensions() { + return extensions(); } /** See {@link #getLocationOffset(int, int)}. */ @@ -91,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); } /** @@ -130,54 +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) - .addAllMacroCalls(macroCalls); + return new Builder(codePoints(), lineOffsets()) + .setDescription(description()) + .addPositionsMap(positions()) + .addAllExtensions(extensions()) + .addAllMacroCalls(macroCalls()); } public static Builder newBuilder() { @@ -185,13 +145,11 @@ 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 codePointArray) { + return new Builder(codePointArray, codePointArray.lineOffsets()); } /** Builder for {@link CelSource}. */ @@ -199,9 +157,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 ImmutableSet.Builder extensions; + private final boolean lineOffsetsAlreadyComputed; private String description; private Builder() { @@ -211,9 +171,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<>(); - description = ""; + this.extensions = ImmutableSet.builder(); + this.description = ""; + this.lineOffsetsAlreadyComputed = !lineOffsets.isEmpty(); } @CanIgnoreReturnValue @@ -225,19 +187,13 @@ 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; } - @CanIgnoreReturnValue - public Builder addLineOffsets(int... lineOffsets) { - // Purposefully not using Arrays.asList to avoid int boxing/unboxing. - for (int index = 0; index != lineOffsets.length; index++) { - addLineOffsets(lineOffsets[index]); - } - return this; - } - @CanIgnoreReturnValue public Builder addAllLineOffsets(Iterable lineOffsets) { for (int lineOffset : lineOffsets) { @@ -259,6 +215,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); @@ -271,12 +233,30 @@ public Builder addAllMacroCalls(Map macroCalls) { 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 clearMacroCall(long exprId) { - this.macroCalls.remove(exprId); + public Builder addAllExtensions(Iterable extensions) { + checkNotNull(extensions); + this.extensions.addAll(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)); + } + /** See {@link #getLocationOffset(int, int)}. */ public Optional getLocationOffset(CelSourceLocation location) { checkNotNull(location); @@ -299,12 +279,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 @@ -319,18 +299,86 @@ public boolean containsMacroCalls(long exprId) { @CheckReturnValue public CelSource build() { - return new CelSource(this); + return new AutoValue_CelSource( + codePoints, + description, + ImmutableList.copyOf(lineOffsets), + ImmutableMap.copyOf(positions), + ImmutableMap.copyOf(macroCalls), + extensions.build()); } } - private static final class LineAndOffset { + /** + * Tag for an extension that were used while parsing or type checking the source expression. For + * example, optimizations that require special runtime support may be specified. These are used to + * check feature support between components in separate implementations. This can be used to + * either skip redundant work or report an error if the extension is unsupported. + */ + @AutoValue + @Immutable + public abstract static class Extension { - private LineAndOffset(int line, int offset) { - this.line = line; - this.offset = offset; + /** Identifier for the extension. Example: constant_folding */ + abstract String id(); + + /** + * Version info. May be skipped if it isn't meaningful for the extension. (for example + * constant_folding might always be v0.0). + */ + abstract Version version(); + + /** + * If set, the listed components must understand the extension for the expression to evaluate + * correctly. + */ + abstract ImmutableList affectedComponents(); + + /** Version of the extension */ + @AutoValue + @Immutable + public abstract static class Version { + + /** + * Major version changes indicate different required support level from the required + * components. + */ + abstract long major(); + + /** + * Minor version changes must not change the observed behavior from existing implementations, + * but may be provided informational. + */ + abstract long minor(); + + /** Create a new instance of Version with the provided major and minor values. */ + public static Version of(long major, long minor) { + return new AutoValue_CelSource_Extension_Version(major, minor); + } + } + + /** CEL component specifier. */ + public enum Component { + /** Unspecified, default. */ + COMPONENT_UNSPECIFIED, + /** Parser. Converts a CEL string to an AST. */ + COMPONENT_PARSER, + /** 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 + } + + @CheckReturnValue + public static Extension create(String id, Version version, Iterable components) { + checkNotNull(version); + checkNotNull(components); + return new AutoValue_CelSource_Extension(id, version, ImmutableList.copyOf(components)); } - int line; - int offset; + @CheckReturnValue + public static Extension create(String id, Version version, Component... components) { + return create(id, version, Arrays.asList(components)); + } } } 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 84f5bcbf0..18bec2fe6 100644 --- a/common/src/main/java/dev/cel/common/CelValidationException.java +++ b/common/src/main/java/dev/cel/common/CelValidationException.java @@ -15,21 +15,21 @@ 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 = 100; private final CelSource source; private final ImmutableList errors; @VisibleForTesting - public CelValidationException(CelSource source, Iterable errors) { - super(JOINER.join(Iterables.transform(errors, error -> error.toDisplayString(source)))); + public CelValidationException(CelSource source, List errors) { + super(safeJoinErrorMessage(source, errors)); this.source = source; this.errors = ImmutableList.copyOf(errors); } @@ -41,6 +41,18 @@ public CelValidationException(CelSource source, Iterable errors) { this.errors = ImmutableList.copyOf(errors); } + private static String safeJoinErrorMessage(CelSource source, List errors) { + if (errors.size() <= MAX_ERRORS_TO_REPORT) { + return CelIssue.toDisplayString(errors, source); + } + + List truncatedErrors = errors.subList(0, MAX_ERRORS_TO_REPORT); + + return CelIssue.toDisplayString(truncatedErrors, source) + + String.format( + "%n...and %d more errors (truncated)", errors.size() - MAX_ERRORS_TO_REPORT); + } + /** Returns the {@link CelSource} that was being validated. */ public CelSource getSource() { return source; 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/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 c8663e6e3..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", ], ) @@ -88,23 +122,35 @@ 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 b075de5f3..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(); } /** @@ -216,6 +212,7 @@ public CelComprehension comprehensionOrDefault() { /** Builder for CelExpr. */ @AutoValue.Builder public abstract static class Builder { + public abstract long id(); public abstract Builder setId(long value); @@ -261,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(); } /** @@ -296,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)); } @@ -352,9 +357,9 @@ public enum Kind { IDENT, SELECT, CALL, - CREATE_LIST, - CREATE_STRUCT, - CREATE_MAP, + LIST, + STRUCT, + MAP, COMPREHENSION, } @@ -370,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(); } @@ -392,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. */ @@ -420,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. */ @@ -474,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(); @@ -524,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); @@ -571,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(); @@ -650,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(); } @@ -669,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(); } @@ -755,38 +726,32 @@ 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 { + public abstract long id(); + public abstract CelExpr value(); public abstract Builder setId(long value); @@ -798,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(); } @@ -887,126 +845,88 @@ 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(); public abstract CelExpr key(); 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. */ @AutoValue.Builder public abstract static class Builder { + public abstract String accuVar(); + public abstract CelExpr iterRange(); public abstract CelExpr accuInit(); @@ -1019,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); @@ -1040,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()) @@ -1056,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( @@ -1071,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( @@ -1085,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); @@ -1096,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) @@ -1141,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) @@ -1160,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 f73cb0564..9ad23952e 100644 --- a/common/src/main/java/dev/cel/common/ast/CelExprFactory.java +++ b/common/src/main/java/dev/cel/common/ast/CelExprFactory.java @@ -18,12 +18,14 @@ 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.MonotonicIdGenerator idGenerator; + private long exprId = 0L; public static CelExprFactory newInstance() { return new CelExprFactory(); @@ -40,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. */ @@ -88,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) @@ -117,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, @@ -173,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. */ + /** 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 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. */ - public final CelExpr fold( - String iterVar, - 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. */ @@ -543,10 +261,15 @@ public final CelExpr newSelect(CelExpr operand, String field, boolean testOnly) /** Returns the next unique expression ID. */ protected long nextExprId() { - return idGenerator.nextExprId(); + return ++exprId; } - protected CelExprFactory() { - idGenerator = CelExprIdGeneratorFactory.newMonotonicIdGenerator(0); + /** Attempts to decrement the next expr ID if possible. */ + protected void maybeDeleteId(long id) { + if (id == exprId - 1) { + exprId--; + } } + + 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 4c8add1fa..d0eee2f50 100644 --- a/common/src/main/java/dev/cel/common/ast/CelExprFormatter.java +++ b/common/src/main/java/dev/cel/common/ast/CelExprFormatter.java @@ -14,11 +14,20 @@ 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 { private final StringBuilder indent = new StringBuilder(); private final StringBuilder exprBuilder = new StringBuilder(); + /** Denotes a set of expression kinds that will not have a new line inserted. */ + private static final ImmutableSet EXCLUDED_NEWLINE_KINDS = + ImmutableSet.of(CelExpr.ExprKind.Kind.CONSTANT, CelExpr.ExprKind.Kind.NOT_SET); + static String format(CelExpr celExpr) { CelExprFormatter formatter = new CelExprFormatter(); formatter.formatExpr(celExpr); @@ -26,9 +35,9 @@ 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(); - if (!exprKind.equals(CelExpr.ExprKind.Kind.CONSTANT)) { + append(String.format(Locale.getDefault(), "%s [%d] {", exprKind, celExpr.id())); + if (!EXCLUDED_NEWLINE_KINDS.contains(exprKind)) { appendNewline(); } @@ -45,24 +54,29 @@ 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()); break; + case NOT_SET: + break; default: + // This should be unreachable unless if we've added any other kinds. + indent(); append("Unknown kind: " + exprKind); + outdent(); break; } - if (!exprKind.equals(CelExpr.ExprKind.Kind.CONSTANT)) { + if (!EXCLUDED_NEWLINE_KINDS.contains(exprKind)) { appendNewline(); append("}"); } else { @@ -92,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()); @@ -111,8 +130,8 @@ private void appendSelect(CelExpr.CelSelect celSelect) { indent(); formatExpr(celSelect.operand()); outdent(); - append("."); - append(celSelect.field()); + appendWithoutIndent("."); + appendWithoutIndent(celSelect.field()); if (celSelect.testOnly()) { appendWithoutIndent("~presence_test"); } @@ -141,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(", "); } } @@ -166,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()) { @@ -194,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(); @@ -229,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/CelExprIdGeneratorFactory.java b/common/src/main/java/dev/cel/common/ast/CelExprIdGeneratorFactory.java index c434150cd..9b7bc60fc 100644 --- a/common/src/main/java/dev/cel/common/ast/CelExprIdGeneratorFactory.java +++ b/common/src/main/java/dev/cel/common/ast/CelExprIdGeneratorFactory.java @@ -59,8 +59,50 @@ public static class StableIdGenerator { private final HashMap idSet; private long exprId; + /** Checks if the given ID has been encountered before. */ + public boolean hasId(long id) { + return idSet.containsKey(id); + } + + /** Generates the next available ID. */ + public long nextExprId() { + return ++exprId; + } + + /** + * Generate the next available ID while memoizing the existing ID. + * + *

The main purpose of this is to sanitize a new AST to replace an existing AST's node with. + * The incoming AST may not have its IDs consistently numbered (often, the expr IDs are just + * zeroes). In those cases, we just want to return an incremented expr ID. + * + *

The memoization becomes necessary if the incoming AST contains an expression with macro + * map populated, requiring a normalization pass. In this case, the method behaves largely the + * same as {@link #renumberId}. + * + * @param id Existing ID to memoize. Providing 0 or less will skip the memoization, in which + * case this behaves just like a {@link MonotonicIdGenerator}. + */ + public long nextExprId(long id) { + long nextExprId = ++exprId; + if (id > 0) { + idSet.put(id, nextExprId); + } + return nextExprId; + } + + /** Memoize a given expression ID with a newly generated ID. */ + public void memoize(long existingId, long newId) { + idSet.put(existingId, newId); + } + + /** + * Renumbers the existing expression ID to a newly generated unique ID. The existing ID is + * memoized, and calling this method again with the same ID will always return the same + * generated ID. + */ public long renumberId(long id) { - Preconditions.checkArgument(id >= 0); + Preconditions.checkArgument(id >= 0, "Expr ID must be positive. Got: %s", id); if (id == 0) { return 0; } @@ -81,5 +123,13 @@ private StableIdGenerator(long exprId) { } } + /** Functional interface for generating the next unique expression ID. */ + @FunctionalInterface + public interface ExprIdGenerator { + + /** Generates an expression ID with the provided expr ID as the context. */ + long generate(long exprId); + } + private CelExprIdGeneratorFactory() {} } 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..d3940405e --- /dev/null +++ b/common/src/main/java/dev/cel/common/ast/CelMutableExpr.java @@ -0,0 +1,1205 @@ +// 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( + 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/formats/BUILD.bazel b/common/src/main/java/dev/cel/common/formats/BUILD.bazel new file mode 100644 index 000000000..16918cb3a --- /dev/null +++ b/common/src/main/java/dev/cel/common/formats/BUILD.bazel @@ -0,0 +1,80 @@ +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: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..0bdfdb299 --- /dev/null +++ b/common/src/main/java/dev/cel/common/formats/ParserContext.java @@ -0,0 +1,47 @@ +// 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(); + + /** NewString creates a new ValueString from the YAML node. */ + ValueString newValueString(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..e0780b01f --- /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.newValueString(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..456872803 --- /dev/null +++ b/common/src/main/java/dev/cel/common/formats/YamlParserContextImpl.java @@ -0,0 +1,157 @@ +// 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 newValueString(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 88d7dd517..c508649a3 100644 --- a/common/src/main/java/dev/cel/common/internal/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/internal/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", @@ -40,6 +43,8 @@ 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", @@ -47,6 +52,25 @@ java_library( ], ) +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//:org_antlr_antlr4_runtime", + "@maven_android//:com_google_guava_guava", + "@maven_android//:com_google_protobuf_protobuf_javalite", + ], +) + java_library( name = "comparison_functions", srcs = ["ComparisonFunctions.java"], @@ -60,6 +84,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 +103,7 @@ java_library( "BidiConverter.java", "Converter.java", ], + # used_by_android tags = [ ], deps = [ @@ -98,17 +135,33 @@ DYNAMIC_PROTO_SOURCES = [ java_library( name = "default_instance_message_factory", - srcs = ["DefaultInstanceMessageFactory.java"], + srcs = [ + "DefaultInstanceMessageFactory.java", + "DefaultInstanceMessageLiteFactory.java", + ], tags = [ ], deps = [ + ":proto_java_qualified_names", + ":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 +170,39 @@ java_library( ], deps = [ ":converter", + ":proto_lite_adapter", ":proto_message_factory", ":well_known_proto", "//:auto_value", "//common:error_codes", + "//common:options", "//common:runtime_exception", "//common/annotations", - "@cel_spec//proto/cel/expr:expr_java_proto", + "//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:error_codes", + "//common:proto_json_adapter", + "//common:runtime_exception", + "//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", ], ) @@ -170,7 +244,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 +261,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,10 +307,125 @@ java_library( ], deps = [ ":well_known_proto", - "//common", + "//common:cel_descriptors", + "//common/annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + ], +) + +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 = [ + "//common/annotations", + "@maven//:com_google_re2j_re2j", + ], +) + +java_library( + name = "proto_java_qualified_names", + srcs = ["ProtoJavaQualifiedNames.java"], + tags = [ + ], + deps = [ + "//common/annotations", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + ], +) + +java_library( + name = "reflection_util", + srcs = ["ReflectionUtil.java"], + deps = [ + "//common/annotations", + ], +) + +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_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..2bc606981 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(); } @@ -53,6 +57,7 @@ public boolean isEmpty() { @Override public abstract String toString(); + @SuppressWarnings("AndroidJdkLibsChecker") // PrimitiveIterator added in 23 public static CelCodePointArray fromString(String text) { if (isNullOrEmpty(text)) { return EmptyCodePointArray.INSTANCE; @@ -60,8 +65,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 +84,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 +98,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 +118,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/internal/CelLiteDescriptorPool.java b/common/src/main/java/dev/cel/common/internal/CelLiteDescriptorPool.java new file mode 100644 index 000000000..73fce4fbb --- /dev/null +++ b/common/src/main/java/dev/cel/common/internal/CelLiteDescriptorPool.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.common.internal; + +import com.google.errorprone.annotations.Immutable; +import com.google.protobuf.MessageLite; +import dev.cel.protobuf.CelLiteDescriptor.MessageLiteDescriptor; +import java.util.Optional; + +/** + * CelLiteDescriptorPool allows lookup of {@link MessageLiteDescriptor} by its fully qualified name. + */ +@Immutable +public interface CelLiteDescriptorPool { + Optional findDescriptor(String protoTypeName); + + Optional findDescriptor(MessageLite messageLite); + + 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..daec1a464 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; @@ -114,5 +115,47 @@ 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..7a9ffa1ec 100644 --- a/common/src/main/java/dev/cel/common/internal/Constants.java +++ b/common/src/main/java/dev/cel/common/internal/Constants.java @@ -19,18 +19,20 @@ 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. */ @Internal +@SuppressWarnings("AndroidJdkLibsChecker") // PrimitiveIterator added in 23 public final class Constants { private static final String DOUBLE_QUOTE = "\""; @@ -153,7 +155,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 { @@ -497,7 +499,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/Converter.java b/common/src/main/java/dev/cel/common/internal/Converter.java index 2c52d0219..2437d3093 100644 --- a/common/src/main/java/dev/cel/common/internal/Converter.java +++ b/common/src/main/java/dev/cel/common/internal/Converter.java @@ -21,6 +21,7 @@ * *

CEL Library Internals. Do Not Use. */ +@SuppressWarnings("AndroidJdkLibsChecker") // FunctionalInterface added in 24 @FunctionalInterface @Internal public interface Converter { 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..df1907e42 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); 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..fcb0e7056 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,11 @@ 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.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 +26,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 +43,29 @@ 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(), + ProtoJavaQualifiedNames.getFullyQualifiedJavaClassName(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/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/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..3461106ae 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,33 @@ 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.CelOptions; import dev.cel.common.CelRuntimeException; import dev.cel.common.annotations.Internal; +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 +119,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.enableUnsignedLongs()); + this.celOptions = celOptions; } /** @@ -158,7 +142,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 +151,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()) { @@ -227,6 +195,7 @@ public Optional adaptFieldToValue(FieldDescriptor fieldDescriptor, Objec } return Optional.of(AdaptingTypes.adaptingList((List) fieldValue, bidiConverter)); } + return Optional.of( fieldToValueConverter(fieldDescriptor).forwardConverter().convert(fieldValue)); } @@ -237,9 +206,6 @@ public Optional adaptValueToFieldType( if (isWrapperType(fieldDescriptor) && fieldValue.equals(NullValue.NULL_VALUE)) { return Optional.empty(); } - if (isUnknown(fieldValue)) { - return Optional.of(fieldValue); - } if (fieldDescriptor.isMapField()) { Descriptor entryDescriptor = fieldDescriptor.getMessageType(); FieldDescriptor keyDescriptor = entryDescriptor.findFieldByNumber(1); @@ -268,6 +234,7 @@ public Optional adaptValueToFieldType( AdaptingTypes.adaptingList( (List) fieldValue, fieldToValueConverter(fieldDescriptor).reverse())); } + return Optional.of( fieldToValueConverter(fieldDescriptor).backwardConverter().convert(fieldValue)); } @@ -281,18 +248,25 @@ private BidiConverter fieldToValueConverter(FieldDescriptor fieldDescriptor) { return INT_CONVERTER; case FIXED32: case UINT32: - if (enableUnsignedLongs) { + if (celOptions.enableUnsignedLongs()) { return UNSIGNED_UINT32_CONVERTER; } return SIGNED_UINT32_CONVERTER; case FIXED64: case UINT64: - if (enableUnsignedLongs) { + if (celOptions.enableUnsignedLongs()) { return UNSIGNED_UINT64_CONVERTER; } return BidiConverter.IDENTITY; case FLOAT: return DOUBLE_CONVERTER; + case BYTES: + if (celOptions.evaluateCanonicalTypesToNativeValues()) { + return BidiConverter.of( + ProtoAdapter::adaptProtoByteStringToValue, ProtoAdapter::adaptCelByteStringToProto); + } + + return BidiConverter.IDENTITY; case ENUM: return BidiConverter.of( value -> (long) ((EnumValueDescriptor) value).getNumber(), @@ -303,17 +277,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 +307,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 +336,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) { @@ -639,13 +354,7 @@ private static boolean isWrapperType(FieldDescriptor fieldDescriptor) { 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; + return WellKnownProto.isWrapperType(fieldTypeName); } private static int intCheckedCast(long value) { diff --git a/common/src/main/java/dev/cel/common/internal/ProtoJavaQualifiedNames.java b/common/src/main/java/dev/cel/common/internal/ProtoJavaQualifiedNames.java new file mode 100644 index 000000000..cf393455b --- /dev/null +++ b/common/src/main/java/dev/cel/common/internal/ProtoJavaQualifiedNames.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.common.internal; + +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.GenericDescriptor; +import com.google.protobuf.Descriptors.ServiceDescriptor; +import dev.cel.common.annotations.Internal; +import java.util.ArrayDeque; + +/** + * Helper class for constructing a fully qualified Java class name from a protobuf descriptor. + * + *

CEL Library Internals. Do Not Use. + */ +@Internal +public final class ProtoJavaQualifiedNames { + // Controls how many times we should recursively inspect a nested message for building fully + // qualified java class name before aborting. + private static final int SAFE_RECURSE_LIMIT = 50; + + /** + * Retrieves the full Java class name from the given descriptor + * + * @return fully qualified class name. + *

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

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

Example 3: com.google.api.expr.cel.internal.testdata$SingleFileProto$SingleFile$Path + * (Nested class with java multiple files disabled) + */ + public static String getFullyQualifiedJavaClassName(Descriptor descriptor) { + return getFullyQualifiedJavaClassNameImpl(descriptor); + } + + private static String getFullyQualifiedJavaClassNameImpl(GenericDescriptor descriptor) { + StringBuilder fullClassName = new StringBuilder(); + + fullClassName.append(getJavaPackageName(descriptor.getFile())).append("."); + + String javaOuterClass = getJavaOuterClassName(descriptor.getFile()); + 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<>(); + GenericDescriptor d = descriptor; + + int recurseCount = 0; + while (d != null) { + classNames.push(d.getName()); + + if (d instanceof EnumDescriptor) { + d = ((EnumDescriptor) d).getContainingType(); + } else { + d = ((Descriptor) 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 + */ + public static String getJavaPackageName(FileDescriptor fileDescriptor) { + FileOptions options = fileDescriptor.getFile().getOptions(); + StringBuilder javaPackageName = new StringBuilder(); + if (options.hasJavaPackage()) { + javaPackageName.append(options.getJavaPackage()); + } else { + javaPackageName + // CEL-Internal-1 + .append(fileDescriptor.getPackage()); + } + + // CEL-Internal-2 + + 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 static String getJavaOuterClassName(FileDescriptor descriptor) { + FileOptions options = descriptor.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 static 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; + } + + private ProtoJavaQualifiedNames() {} +} 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..eb8c42862 --- /dev/null +++ b/common/src/main/java/dev/cel/common/internal/ProtoLiteAdapter.java @@ -0,0 +1,330 @@ +// 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.CelErrorCode; +import dev.cel.common.CelProtoJsonAdapter; +import dev.cel.common.CelRuntimeException; +import dev.cel.common.annotations.Internal; +import dev.cel.common.values.CelByteString; +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 boolean enableUnsignedLongs; + + @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 (Duration) value; + case TIMESTAMP: + return (Timestamp) 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 NullValue) { + 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 { + 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 (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; + } + } + + 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 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); + } + } + + 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 CelRuntimeException(e, CelErrorCode.NUMERIC_OVERFLOW); + } + } + + private static int unsignedIntCheckedCast(long value) { + try { + return UnsignedInts.checkedCast(value); + } catch (IllegalArgumentException e) { + throw new CelRuntimeException(e, CelErrorCode.NUMERIC_OVERFLOW); + } + } + + 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(); + } + + public ProtoLiteAdapter(boolean enableUnsignedLongs) { + this.enableUnsignedLongs = enableUnsignedLongs; + } +} 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..cc705fccd --- /dev/null +++ b/common/src/main/java/dev/cel/common/internal/ProtoTimeUtils.java @@ -0,0 +1,568 @@ +// 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.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" + 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 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 { + return normalizedTimestamp(seconds, nanos); + } 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); + } + Timestamp timestamp = Timestamp.newBuilder().setSeconds(seconds).setNanos(nanos).build(); + return checkValid(timestamp); + } + + 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) + } + Duration duration = Duration.newBuilder().setSeconds(seconds).setNanos(nanos).build(); + return checkValid(duration); + } + + 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..e513a446b --- /dev/null +++ b/common/src/main/java/dev/cel/common/internal/ReflectionUtil.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.common.internal; + +import dev.cel.common.annotations.Internal; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * 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); + } + } + + private ReflectionUtil() {} +} diff --git a/common/src/main/java/dev/cel/common/internal/SafeStringFormatter.java b/common/src/main/java/dev/cel/common/internal/SafeStringFormatter.java new file mode 100644 index 000000000..7c22e834d --- /dev/null +++ b/common/src/main/java/dev/cel/common/internal/SafeStringFormatter.java @@ -0,0 +1,48 @@ +// 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 com.google.re2j.Pattern; +import dev.cel.common.annotations.Internal; + +/** + * {@link SafeStringFormatter} is a wrapper around JDK's {@link String#format}. It prevents any + * unsafe string.format calls by only allowing known formatting specifiers to be provided. + * + *

CEL Library Internals. Do Not Use. + */ +@Internal +public final class SafeStringFormatter { + // Allow format specifiers of %d, %f, %s and %n only. + private static final Pattern FORBIDDEN_FORMAT_SPECIFIERS = Pattern.compile("%[^dfsn]"); + + /** + * Performs a safe {@link String#format}. + * + * @param format A format string. Only %d, %f, %s and %n are allowed as formatting specifiers. All + * other formatting specifiers will be stripped out. + * @return A formatted string + */ + public static String format(String format, Object... args) { + if (args.length == 0) { + return format; + } + + String sanitizedMessage = FORBIDDEN_FORMAT_SPECIFIERS.matcher(format).replaceAll(""); + return String.format(sanitizedMessage, args); + } + + private SafeStringFormatter() {} +} 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..78041e3be 100644 --- a/common/src/main/java/dev/cel/common/internal/WellKnownProto.java +++ b/common/src/main/java/dev/cel/common/internal/WellKnownProto.java @@ -21,9 +21,10 @@ 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 +36,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 +46,79 @@ */ @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; - private final boolean isWrapperType; + ANY_VALUE("google.protobuf.Any", Any.class), + DURATION("google.protobuf.Duration", Duration.class), + JSON_LIST_VALUE("google.protobuf.ListValue", ListValue.class), + JSON_STRUCT_VALUE("google.protobuf.Struct", Struct.class), + JSON_VALUE("google.protobuf.Value", Value.class), + TIMESTAMP("google.protobuf.Timestamp", Timestamp.class), + // Wrapper types + FLOAT_VALUE("google.protobuf.FloatValue", FloatValue.class, /* isWrapperType= */ true), + INT32_VALUE("google.protobuf.Int32Value", Int32Value.class, /* isWrapperType= */ true), + INT64_VALUE("google.protobuf.Int64Value", Int64Value.class, /* isWrapperType= */ true), + STRING_VALUE("google.protobuf.StringValue", StringValue.class, /* isWrapperType= */ true), + BOOL_VALUE("google.protobuf.BoolValue", BoolValue.class, /* isWrapperType= */ true), + BYTES_VALUE("google.protobuf.BytesValue", BytesValue.class, /* isWrapperType= */ true), + DOUBLE_VALUE("google.protobuf.DoubleValue", DoubleValue.class, /* isWrapperType= */ true), + UINT32_VALUE("google.protobuf.UInt32Value", UInt32Value.class, /* isWrapperType= */ true), + UINT64_VALUE("google.protobuf.UInt64Value", 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", Empty.class, /* isWrapperType= */ true), + FIELD_MASK("google.protobuf.FieldMask", 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 WELL_KNOWN_PROTO_MAP; + private static final ImmutableMap, WellKnownProto> + CLASS_TO_NAME_TO_WELL_KNOWN_PROTO_MAP = + stream(WellKnownProto.values()) + .collect(toImmutableMap(WellKnownProto::messageClass, Function.identity())); - static { - WELL_KNOWN_PROTO_MAP = - stream(WellKnownProto.values()) - .collect(toImmutableMap(WellKnownProto::typeName, Function.identity())); + private final String wellKnownProtoTypeName; + private final Class clazz; + private final boolean isWrapperType; + + /** Gets the fully qualified prototype name (ex: google.protobuf.FloatValue) */ + public String typeName() { + return wellKnownProtoTypeName; } - WellKnownProto(Descriptor descriptor) { - this(descriptor, /* isWrapperType= */ false); + /** Gets the underlying java class for this WellKnownProto. */ + public Class messageClass() { + return clazz; } - 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, Class clazz) { + this(wellKnownProtoTypeName, clazz, /* isWrapperType= */ false); + } + + WellKnownProto(String wellKnownProtoFullName, Class clazz, boolean isWrapperType) { + this.wellKnownProtoTypeName = wellKnownProtoFullName; + 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 a9ca1c4e9..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,19 +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", ], 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 76fa5bfb0..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,121 +16,95 @@ 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 abstract Optional parent(); - - /** Represents the count of transitive parents. Depth of an AST's root is 0. */ - public abstract int depth(); +// 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) { - return CelNavigableExpr.builder().setExpr(expr).build(); + ExprPropertyCalculator exprHeightCalculator = new ExprPropertyCalculator<>(expr); + ExprProperty exprProperty = exprHeightCalculator.getProperty(expr.id()); + + return builder() + .setExpr(expr) + .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.equals(this)); + 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.equals(this)); + 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); + 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 static class Builder + implements BaseNavigableExpr.Builder { - public abstract CelExpr expr(); + @Override + public abstract Builder setParent(CelNavigableExpr value); + @Override public abstract Builder setExpr(CelExpr value); - public abstract Builder setParent(CelNavigableExpr value); - + @Override public abstract Builder setDepth(int value); - public abstract CelNavigableExpr build(); + @Override + public abstract Builder setMaxId(long value); + + @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 8abcf017f..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,27 +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 Stream.Builder streamBuilder; + private final ExprPropertyCalculator exprPropertyCalculator; private final TraversalOrder traversalOrder; private final int maxDepth; - private CelNavigableExprVisitor(int maxDepth, TraversalOrder traversalOrder) { + private CelNavigableExprVisitor( + int maxDepth, + ExprPropertyCalculator exprPropertyCalculator, + TraversalOrder traversalOrder) { this.maxDepth = maxDepth; + this.exprPropertyCalculator = exprPropertyCalculator; this.traversalOrder = traversalOrder; this.streamBuilder = Stream.builder(); } @@ -57,8 +56,8 @@ private CelNavigableExprVisitor(int maxDepth, TraversalOrder traversalOrder) { * 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); } @@ -80,23 +79,25 @@ static Stream collect( * maxDepth of 2: a, b, d, e, c * */ - static Stream collect( - CelNavigableExpr navigableExpr, int maxDepth, TraversalOrder traversalOrder) { - CelNavigableExprVisitor visitor = new CelNavigableExprVisitor(maxDepth, 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."); } - if (navigableExpr.depth() > maxDepth) { - return; - } - if (traversalOrder.equals(TraversalOrder.PRE_ORDER)) { + + boolean addToStream = navigableExpr.depth() <= maxDepth; + if (addToStream && traversalOrder.equals(TraversalOrder.PRE_ORDER)) { streamBuilder.add(navigableExpr); } @@ -104,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()); @@ -123,30 +124,29 @@ private void visit(CelNavigableExpr navigableExpr) { break; } - if (traversalOrder.equals(TraversalOrder.POST_ORDER)) { + if (addToStream && traversalOrder.equals(TraversalOrder.POST_ORDER)) { streamBuilder.add(navigableExpr); } } - private void visit(CelNavigableExpr navigableExpr, CelCall call) { + private void visit(T navigableExpr, Expression.Call call) { if (call.target().isPresent()) { - CelNavigableExpr target = newNavigableChild(navigableExpr, call.target().get()); - visit(target); + visit(newNavigableChild(navigableExpr, call.target().get())); } 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())); @@ -154,34 +154,37 @@ 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()) { - CelNavigableExpr value = newNavigableChild(navigableExpr, entry.value()); - visit(value); + 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) { - CelNavigableExpr arg = newNavigableChild(parent, expr); - visit(arg); + private void visitExprList(java.util.List list, T parent) { + for (E expr : list) { + visit(newNavigableChild(parent, expr)); } } - private CelNavigableExpr newNavigableChild(CelNavigableExpr parent, CelExpr expr) { - return CelNavigableExpr.builder() + 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/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/testing/RepeatedTestProvider.java b/common/src/main/java/dev/cel/common/testing/RepeatedTestProvider.java index a8502e0b5..974c504b7 100644 --- a/common/src/main/java/dev/cel/common/testing/RepeatedTestProvider.java +++ b/common/src/main/java/dev/cel/common/testing/RepeatedTestProvider.java @@ -18,7 +18,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; -import com.google.testing.junit.testparameterinjector.TestParameter.TestParameterValuesProvider; +import com.google.testing.junit.testparameterinjector.TestParameterValuesProvider; import dev.cel.common.annotations.Internal; import java.util.stream.IntStream; @@ -34,11 +34,11 @@ */ @Internal @VisibleForTesting -public final class RepeatedTestProvider implements TestParameterValuesProvider { +public final class RepeatedTestProvider extends TestParameterValuesProvider { private static final int REPEATED_TEST_RUN_COUNT = 50; @Override - public ImmutableList provideValues() { + public ImmutableList provideValues(Context context) { return IntStream.rangeClosed(1, REPEATED_TEST_RUN_COUNT).boxed().collect(toImmutableList()); } } 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..ccb72f9e5 100644 --- a/common/src/main/java/dev/cel/common/types/BUILD.bazel +++ b/common/src/main/java/dev/cel/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", @@ -69,16 +72,57 @@ 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", + ], +) + java_library( name = "cel_v1alpha1_types", srcs = ["CelV1AlphaTypes.java"], @@ -130,10 +174,59 @@ java_library( ":type_providers", ":types", "//:auto_value", - "//common", + "//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", ], ) + +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/EnumType.java b/common/src/main/java/dev/cel/common/types/EnumType.java index 836b68a6c..076ebe527 100644 --- a/common/src/main/java/dev/cel/common/types/EnumType.java +++ b/common/src/main/java/dev/cel/common/types/EnumType.java @@ -62,6 +62,7 @@ public Optional findNameByNumber(Integer enumNumber) { /** Functional interface for lookup up an enum number by its local or fully qualified name. */ @Immutable @FunctionalInterface + @SuppressWarnings("AndroidJdkLibsChecker") // FunctionalInterface added in 24 public interface EnumNumberResolver { Optional findNumber(String enumName); } @@ -69,6 +70,7 @@ public interface EnumNumberResolver { /** Functional interface for looking up an enum name by its number. */ @Immutable @FunctionalInterface + @SuppressWarnings("AndroidJdkLibsChecker") // FunctionalInterface added in 24 public interface EnumNameResolver { Optional findName(Integer enumNumber); } 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..4b97178d0 100644 --- a/common/src/main/java/dev/cel/common/types/ProtoMessageTypeProvider.java +++ b/common/src/main/java/dev/cel/common/types/ProtoMessageTypeProvider.java @@ -142,8 +142,11 @@ private ImmutableMap createProtoMessageTypes( 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,7 +154,7 @@ private ImmutableMap createEnumTypes( enumTypes.put( enumDescriptor.getFullName(), EnumType.create(enumDescriptor.getFullName(), values)); } - return enumTypes.buildOrThrow(); + return ImmutableMap.copyOf(enumTypes); } private static class FieldResolver { 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..cbf02e9c6 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,19 @@ 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"); + private static final ImmutableMap TYPE_MAP = + ImmutableMap.of( + DYN.name(), DYN, + 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 +71,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..ba4251559 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,8 @@ public static StructType create( */ @Immutable @FunctionalInterface - public static interface FieldResolver { + @SuppressWarnings("AndroidJdkLibsChecker") // FunctionalInterface added in 24 + 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..9ffa5bad3 100644 --- a/common/src/main/java/dev/cel/common/values/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/values/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", @@ -50,11 +53,21 @@ 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", - ], + srcs = ["CelValueProvider.java"], tags = [ ], deps = [ @@ -64,6 +77,44 @@ java_library( ], ) +cel_android_library( + name = "cel_value_provider_android", + srcs = ["CelValueProvider.java"], + tags = [ + ], + deps = [ + ":cel_value_android", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", + ], +) + +java_library( + name = "combined_cel_value_provider", + srcs = ["CombinedCelValueProvider.java"], + tags = [ + ], + deps = [ + "//common/values:cel_value", + "//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 = [ + ":cel_value_android", + "//common/values:cel_value_provider_android", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", + ], +) + java_library( name = "values", srcs = CEL_VALUES_SOURCES, @@ -74,7 +125,6 @@ java_library( ":cel_value", "//:auto_value", "//common:error_codes", - "//common:options", "//common:runtime_exception", "//common/annotations", "//common/types", @@ -85,14 +135,70 @@ java_library( ], ) +cel_android_library( + name = "values_android", + srcs = CEL_VALUES_SOURCES, + tags = [ + ], + deps = [ + ":cel_byte_string", + ":cel_value_android", + "//:auto_value", + "//common:error_codes", + "//common:runtime_exception", + "//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", + ":cel_value", + ":values", + "//common/annotations", + "//common/internal:proto_time_utils", + "//common/internal:well_known_proto", + "@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", + ":cel_value_android", + ":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,22 +208,19 @@ java_library( tags = [ ], deps = [ + ":base_proto_cel_value_converter", ":cel_value", ":values", "//:auto_value", - "//common:options", "//common/annotations", "//common/internal:cel_descriptor_pools", "//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 +231,137 @@ java_library( tags = [ ], deps = [ + ":base_proto_message_value_provider", ":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:base_proto_cel_value_converter", "@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", + ":cel_value", + ":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//:com_google_protobuf_protobuf_java", + "@maven//:org_jspecify_jspecify", + "@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", + ":cel_value_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//:org_jspecify_jspecify", + "@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", + ":cel_value", + ":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", + ":cel_value_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..38741f555 --- /dev/null +++ b/common/src/main/java/dev/cel/common/values/BaseProtoCelValueConverter.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.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.errorprone.annotations.Immutable; +import com.google.protobuf.BoolValue; +import com.google.protobuf.ByteString; +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.MessageLite; +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; +import java.util.Optional; + +/** + * {@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 { + + public abstract CelValue fromProtoMessageToCelValue(MessageLite msg); + + /** + * 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 ProtoTimeUtils.toProtoTimestamp(((TimestampValue) celValue).value()); + } else if (celValue instanceof DurationValue) { + return ProtoTimeUtils.toProtoDuration(((DurationValue) celValue).value()); + } + + return super.fromCelValueToJavaObject(celValue); + } + + /** {@inheritDoc} Protobuf semantics take precedence for conversion. */ + @Override + public CelValue fromJavaObjectToCelValue(Object value) { + Preconditions.checkNotNull(value); + + Optional wellKnownProto = WellKnownProto.getByClass(value.getClass()); + if (wellKnownProto.isPresent()) { + return fromWellKnownProtoToCelValue((MessageLiteOrBuilder) value, wellKnownProto.get()); + } + + if (value instanceof ByteString) { + return BytesValue.create(CelByteString.of(((ByteString) value).toByteArray())); + } else if (value instanceof com.google.protobuf.NullValue) { + return NullValue.NULL_VALUE; + } + + return super.fromJavaObjectToCelValue(value); + } + + protected CelValue fromWellKnownProtoToCelValue( + MessageLiteOrBuilder message, WellKnownProto wellKnownProto) { + switch (wellKnownProto) { + 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: + return DurationValue.create(ProtoTimeUtils.toJavaDuration((Duration) message)); + case TIMESTAMP: + return TimestampValue.create(ProtoTimeUtils.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()); + case UINT64_VALUE: + return UintValue.create(((UInt64Value) message).getValue()); + default: + throw new UnsupportedOperationException( + "Unsupported message to CelValue conversion - " + message); + } + } + + 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 -> { + CelValue key = fromJavaObjectToCelValue(e.getKey()); + if (!(key instanceof dev.cel.common.values.StringValue)) { + throw new IllegalStateException( + "Expected a string type for the key, but instead got: " + key); + } + return (dev.cel.common.values.StringValue) key; + }, + e -> adaptJsonValueToCelValue(e.getValue())))); + } + + protected BaseProtoCelValueConverter() {} +} diff --git a/common/src/main/java/dev/cel/common/values/BaseProtoMessageValueProvider.java b/common/src/main/java/dev/cel/common/values/BaseProtoMessageValueProvider.java new file mode 100644 index 000000000..f42a16179 --- /dev/null +++ b/common/src/main/java/dev/cel/common/values/BaseProtoMessageValueProvider.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.values; + +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.annotations.Internal; + +/** + * {@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 BaseProtoMessageValueProvider implements CelValueProvider { + + 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/CelValueConverter.java b/common/src/main/java/dev/cel/common/values/CelValueConverter.java index 83275e3c1..a281318f8 100644 --- a/common/src/main/java/dev/cel/common/values/CelValueConverter.java +++ b/common/src/main/java/dev/cel/common/values/CelValueConverter.java @@ -18,7 +18,7 @@ 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.Map; import java.util.Map.Entry; @@ -32,10 +32,9 @@ */ @SuppressWarnings("unchecked") // Unchecked cast of generics due to type-erasure (ex: MapValue). @Internal +@Immutable abstract class CelValueConverter { - protected final CelOptions celOptions; - /** Adapts a {@link CelValue} to a plain old Java Object. */ public Object fromCelValueToJavaObject(CelValue celValue) { Preconditions.checkNotNull(celValue); @@ -112,7 +111,9 @@ protected CelValue fromJavaPrimitiveToCelValue(Object 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 UintValue.create((UnsignedLong) value); + } else if (value instanceof CelByteString) { + return BytesValue.create((CelByteString) value); } // Fall back to an Opaque value, as a custom class was supplied in the runtime. The legacy @@ -145,8 +146,5 @@ private MapValue toMapValue(Map map) { return ImmutableMapValue.create(mapBuilder.buildOrThrow()); } - protected CelValueConverter(CelOptions celOptions) { - Preconditions.checkNotNull(celOptions); - this.celOptions = celOptions; - } + protected CelValueConverter() {} } 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..e5080e180 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; @@ -31,32 +29,4 @@ public interface CelValueProvider { * special cases such as wrappers where its primitive is returned. */ 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(); - } - } } 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..a674beb8d --- /dev/null +++ b/common/src/main/java/dev/cel/common/values/CombinedCelValueProvider.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.common.values; + +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.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(); + } + + /** 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/ListValue.java b/common/src/main/java/dev/cel/common/values/ListValue.java index 9cf6aa806..814884d34 100644 --- a/common/src/main/java/dev/cel/common/values/ListValue.java +++ b/common/src/main/java/dev/cel/common/values/ListValue.java @@ -24,7 +24,7 @@ import java.util.Comparator; import java.util.List; import java.util.function.UnaryOperator; -import org.jspecify.nullness.Nullable; +import org.jspecify.annotations.Nullable; /** * ListValue is an abstract representation of a generic list containing zero or more {@link diff --git a/common/src/main/java/dev/cel/common/values/MapValue.java b/common/src/main/java/dev/cel/common/values/MapValue.java index ff1bb3f56..0f79b0464 100644 --- a/common/src/main/java/dev/cel/common/values/MapValue.java +++ b/common/src/main/java/dev/cel/common/values/MapValue.java @@ -26,7 +26,7 @@ import java.util.Optional; import java.util.function.BiFunction; import java.util.function.Function; -import org.jspecify.nullness.Nullable; +import org.jspecify.annotations.Nullable; /** * MapValue is an abstract representation of a generic map containing {@link CelValue} as keys and 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/OptionalValue.java b/common/src/main/java/dev/cel/common/values/OptionalValue.java index 45270ebb2..3866ef72b 100644 --- a/common/src/main/java/dev/cel/common/values/OptionalValue.java +++ b/common/src/main/java/dev/cel/common/values/OptionalValue.java @@ -21,7 +21,7 @@ import dev.cel.common.types.SimpleType; 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 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..7a679270d 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,30 @@ 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.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.MessageLite; +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,39 +46,23 @@ */ @Immutable @Internal -public final class ProtoCelValueConverter extends CelValueConverter { +public final class ProtoCelValueConverter extends BaseProtoCelValueConverter { private final CelDescriptorPool celDescriptorPool; private final DynamicProto dynamicProto; /** 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) { + return new ProtoCelValueConverter(celDescriptorPool, dynamicProto); } - /** - * 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); + public CelValue fromProtoMessageToCelValue(MessageLite msg) { + return fromDescriptorMessageToCelValue((MessageOrBuilder) msg); } /** Adapts a Protobuf message into a {@link CelValue}. */ - public CelValue fromProtoMessageToCelValue(MessageOrBuilder message) { + public CelValue fromDescriptorMessageToCelValue(MessageOrBuilder message) { Preconditions.checkNotNull(message); // Attempt to convert the proto from a dynamic message into a concrete message if possible. @@ -107,11 +71,18 @@ public CelValue fromProtoMessageToCelValue(MessageOrBuilder message) { } WellKnownProto wellKnownProto = - WellKnownProto.getByDescriptorName(message.getDescriptorForType().getFullName()); + WellKnownProto.getByTypeName(message.getDescriptorForType().getFullName()).orElse(null); if (wellKnownProto == null) { return ProtoMessageValue.create((Message) message, celDescriptorPool, this); } + return fromWellKnownProtoToCelValue(message, wellKnownProto); + } + + @Override + protected CelValue fromWellKnownProtoToCelValue( + MessageLiteOrBuilder msg, WellKnownProto wellKnownProto) { + MessageOrBuilder message = (MessageOrBuilder) msg; switch (wellKnownProto) { case ANY_VALUE: Message unpackedMessage; @@ -122,62 +93,14 @@ public CelValue fromProtoMessageToCelValue(MessageOrBuilder message) { "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()); + default: + return super.fromWellKnownProtoToCelValue(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) { + if (value instanceof EnumValueDescriptor) { // (b/178627883) Strongly typed enum is not supported yet return IntValue.create(((EnumValueDescriptor) value).getNumber()); } @@ -186,7 +109,7 @@ public CelValue fromJavaObjectToCelValue(Object value) { } /** Adapts the protobuf message field into {@link CelValue}. */ - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "rawtypes"}) public CelValue fromProtoMessageFieldToCelValue( Message message, FieldDescriptor fieldDescriptor) { Preconditions.checkNotNull(message); @@ -195,23 +118,44 @@ public CelValue fromProtoMessageFieldToCelValue( Object result = message.getField(fieldDescriptor); switch (fieldDescriptor.getType()) { case MESSAGE: - if (CelTypes.isWrapperType(fieldDescriptor.getMessageType().getFullName()) + if (WellKnownProto.isWrapperType(fieldDescriptor.getMessageType().getFullName()) && !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); } - break; + + return fromDescriptorMessageToCelValue((MessageOrBuilder) result); case UINT32: - return UintValue.create((int) result, celOptions.enableUnsignedLongs()); + return UintValue.create((int) result); case UINT64: - return UintValue.create((long) result, celOptions.enableUnsignedLongs()); + return UintValue.create((long) result); default: break; } @@ -219,99 +163,7 @@ public CelValue fromProtoMessageFieldToCelValue( 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); - } - 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); - } - } - - private ProtoCelValueConverter( - CelOptions celOptions, CelDescriptorPool celDescriptorPool, DynamicProto dynamicProto) { - super(celOptions); + private ProtoCelValueConverter(CelDescriptorPool celDescriptorPool, DynamicProto dynamicProto) { Preconditions.checkNotNull(celDescriptorPool); Preconditions.checkNotNull(dynamicProto); this.celDescriptorPool = celDescriptorPool; 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..c23ad54de --- /dev/null +++ b/common/src/main/java/dev/cel/common/values/ProtoLiteCelValueConverter.java @@ -0,0 +1,394 @@ +// 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.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(); + } + + CelValue getDefaultCelValue(String protoTypeName, String fieldName) { + MessageLiteDescriptor messageDescriptor = descriptorPool.getDescriptorOrThrow(protoTypeName); + FieldLiteDescriptor fieldDescriptor = messageDescriptor.getByFieldNameOrThrow(fieldName); + + Object defaultValue = getDefaultValue(fieldDescriptor); + if (defaultValue instanceof MessageLite) { + return fromProtoMessageToCelValue((MessageLite) defaultValue); + } + + return fromJavaObjectToCelValue(defaultValue); + } + + 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); + } + } + + @Override + @SuppressWarnings("LiteProtoToString") // No alternative identifier to use. Debug only info is OK. + public CelValue fromProtoMessageToCelValue(MessageLite msg) { + checkNotNull(msg); + + 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 super.fromWellKnownProtoToCelValue(msg, wellKnownProto); + } + + @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..9cb3b522d --- /dev/null +++ b/common/src/main/java/dev/cel/common/values/ProtoMessageLiteValue.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.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 CelValue select(StringValue field) { + return find(field) + .orElseGet( + () -> protoLiteCelValueConverter().getDefaultCelValue(celType().name(), field.value())); + } + + @Override + public Optional find(StringValue field) { + Object fieldValue = fieldValues().get(field.value()); + return Optional.ofNullable(fieldValue) + .map(value -> protoLiteCelValueConverter().fromJavaObjectToCelValue(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..fc0e18c2e --- /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.fromProtoMessageToCelValue(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/ProtoMessageValueProvider.java b/common/src/main/java/dev/cel/common/values/ProtoMessageValueProvider.java index 430328596..6dc9dfed2 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; @@ -36,15 +34,23 @@ */ @Immutable @Internal -public class ProtoMessageValueProvider implements CelValueProvider { +public class ProtoMessageValueProvider extends BaseProtoMessageValueProvider { private final ProtoAdapter protoAdapter; private final ProtoMessageFactory protoMessageFactory; private final ProtoCelValueConverter protoCelValueConverter; + @Override + public BaseProtoCelValueConverter protoCelValueConverter() { + 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()); @@ -60,17 +66,6 @@ public Optional newValue(String structType, Map fields } } - 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) { FieldDescriptor fieldDescriptor = descriptor.findFieldByName(fieldName); if (fieldDescriptor != null) { @@ -89,15 +84,14 @@ 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()); + ProtoCelValueConverter.newInstance(protoMessageFactory.getDescriptorPool(), dynamicProto); + this.protoAdapter = new ProtoAdapter(dynamicProto, celOptions); } } diff --git a/common/src/main/java/dev/cel/common/values/UintValue.java b/common/src/main/java/dev/cel/common/values/UintValue.java index 71fcfae83..af931e78b 100644 --- a/common/src/main/java/dev/cel/common/values/UintValue.java +++ b/common/src/main/java/dev/cel/common/values/UintValue.java @@ -27,11 +27,10 @@ @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(); + public abstract UnsignedLong value(); @Override public boolean isZeroValue() { @@ -47,8 +46,7 @@ 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); + public static UintValue create(long value) { + return new AutoValue_UintValue(UnsignedLong.fromLongBits(value)); } } 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 6da50a05b..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 [lazy = true]; - - // 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; -} \ No newline at end of file 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 26df9d899..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 [lazy = true]; - - // 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 6001ed1b4..c2ce1efdb 100644 --- a/common/src/test/java/dev/cel/common/BUILD.bazel +++ b/common/src/test/java/dev/cel/common/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,33 @@ java_library( srcs = glob(["*.java"]), deps = [ "//:java_truth", - "//common", - "//common:features", + "//common:cel_ast", + "//common:cel_descriptors", + "//common:cel_source", + "//common:compiler_common", + "//common:container", "//common:options", + "//common:proto_ast", + "//common:proto_json_adapter", "//common:proto_v1alpha1_ast", + "//common:source_location", "//common/ast", "//common/internal", "//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 03e1a5619..b04f9f406 100644 --- a/common/src/test/java/dev/cel/common/CelAbstractSyntaxTreeTest.java +++ b/common/src/test/java/dev/cel/common/CelAbstractSyntaxTreeTest.java @@ -15,8 +15,6 @@ package dev.cel.common; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth8.assertThat; -import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; import dev.cel.expr.CheckedExpr; import dev.cel.expr.Constant; @@ -27,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; @@ -55,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() @@ -92,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); } @@ -100,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); } @@ -147,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..f116039c8 --- /dev/null +++ b/common/src/test/java/dev/cel/common/CelContainerTest.java @@ -0,0 +1,212 @@ +// 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", + "invalid_qualified_name", + "alias must refer to a valid qualified name: invalid_qualified_name"), + BAD_QUALIFIED_NAME_2( + "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..df4fc35fb 100644 --- a/common/src/test/java/dev/cel/common/CelDescriptorUtilTest.java +++ b/common/src/test/java/dev/cel/common/CelDescriptorUtilTest.java @@ -21,21 +21,30 @@ 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 +56,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 +152,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,7 +181,7 @@ 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( 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 8ab329f6d..585828549 100644 --- a/common/src/test/java/dev/cel/common/CelProtoAbstractSyntaxTreeTest.java +++ b/common/src/test/java/dev/cel/common/CelProtoAbstractSyntaxTreeTest.java @@ -25,7 +25,10 @@ import dev.cel.expr.ParsedExpr; import dev.cel.expr.Reference; import dev.cel.expr.SourceInfo; -import dev.cel.common.types.CelTypes; +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.CelProtoTypes; import java.util.Arrays; import org.junit.Test; import org.junit.runner.RunWith; @@ -57,6 +60,13 @@ public class CelProtoAbstractSyntaxTreeTest { .setLocation("test/location.cel") .putPositions(1L, 0) .addLineOffsets(4) + .addExtensions( + Extension.newBuilder() + .setId("extension_id") + .addAffectedComponents(Component.COMPONENT_PARSER) + .addAffectedComponents(Component.COMPONENT_TYPE_CHECKER) + .addAffectedComponents(Component.COMPONENT_RUNTIME) + .setVersion(Version.newBuilder().setMajor(5).setMinor(3))) .putMacroCalls( 2, Expr.newBuilder() @@ -69,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) @@ -104,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 5fc14fa0e..d8b3701e3 100644 --- a/common/src/test/java/dev/cel/common/CelSourceTest.java +++ b/common/src/test/java/dev/cel/common/CelSourceTest.java @@ -15,10 +15,13 @@ package dev.cel.common; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth8.assertThat; 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; import dev.cel.common.internal.BasicCodePointArray; import dev.cel.common.internal.CodePointStream; import dev.cel.common.internal.Latin1CodePointArray; @@ -157,4 +160,36 @@ public void fromString_handlesMultiLineSupplemental() throws Exception { assertThat(charStream.LA(-1)).isEqualTo(IntStream.EOF); assertThat(source.getLineOffsets()).containsExactly(6, 13).inOrder(); } + + @Test + public void source_withExtension() { + CelSource celSource = + CelSource.newBuilder("") + .addAllExtensions( + Extension.create( + "extension_id", + Version.of(5, 3), + Component.COMPONENT_PARSER, + Component.COMPONENT_TYPE_CHECKER)) + .build(); + + Extension extension = Iterables.getOnlyElement(celSource.getExtensions()); + assertThat(extension.id()).isEqualTo("extension_id"); + assertThat(extension.version().major()).isEqualTo(5L); + assertThat(extension.version().minor()).isEqualTo(3L); + assertThat(extension.affectedComponents()) + .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 new file mode 100644 index 000000000..703531d81 --- /dev/null +++ b/common/src/test/java/dev/cel/common/CelValidationExceptionTest.java @@ -0,0 +1,42 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.common; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableList; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class CelValidationExceptionTest { + + @Test + public void construct_withLargeErrorCount() { + ImmutableList.Builder issueBuilder = ImmutableList.builder(); + for (int i = 0; i < 1500; i++) { + issueBuilder.add(CelIssue.formatError(i + 1, i + 1, "generic error")); + } + + CelValidationException celValidationException = + new CelValidationException(CelSource.newBuilder().build(), issueBuilder.build()); + + assertThat(celValidationException.getErrors()).hasSize(1500); + assertThat(celValidationException) + .hasMessageThat() + .endsWith("...and 1400 more errors (truncated)"); + } +} 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..a2bb62719 100644 --- a/common/src/test/java/dev/cel/common/ast/BUILD.bazel +++ b/common/src/test/java/dev/cel/common/ast/BUILD.bazel @@ -1,3 +1,4 @@ +load("@rules_java//java:defs.bzl", "java_library") load("//:testing.bzl", "junit4_test_suites") package(default_applicable_licenses = [ @@ -11,26 +12,36 @@ java_library( deps = [ "//:auto_value", "//:java_truth", - "//common", + "//common:cel_ast", "//common:compiler_common", + "//common:container", + "//common:mutable_ast", + "//common:mutable_source", + "//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/CelExprFormatterTest.java b/common/src/test/java/dev/cel/common/ast/CelExprFormatterTest.java index b80779bf7..56b386ba3 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; @@ -61,6 +63,13 @@ public void constant(@TestParameter ConstantTestCase constantTestCase) throws Ex assertThat(formattedExpr).isEqualTo(constantTestCase.formatted); } + @Test + public void notSet() { + String formattedExpr = CelExprFormatter.format(CelExpr.ofNotSet(1)); + + assertThat(formattedExpr).isEqualTo("NOT_SET [1] {}"); + } + @Test public void select() throws Exception { CelCompiler celCompiler = @@ -129,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) @@ -141,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" @@ -163,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(); @@ -181,7 +190,7 @@ public void create_struct() throws Exception { assertThat(formattedExpr) .isEqualTo( - "CREATE_STRUCT [1] {\n" + "STRUCT [1] {\n" + " name: TestAllTypes\n" + " entries: {\n" + " ENTRY [2] {\n" @@ -213,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(); @@ -227,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" @@ -265,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(); @@ -276,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" @@ -284,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" @@ -296,7 +306,7 @@ public void comprehension() throws Exception { + " function: !_\n" + " args: {\n" + " IDENT [11] {\n" - + " name: __result__\n" + + " name: @result\n" + " }\n" + " }\n" + " }\n" @@ -308,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" @@ -324,8 +334,41 @@ public void comprehension() throws Exception { + " }\n" + " result: {\n" + " IDENT [16] {\n" - + " name: __result__\n" + + " name: @result\n" + + " }\n" + + " }\n" + + "}"); + } + + @Test + public void ternaryWithPresenceTest() throws Exception { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addMessageTypes(TestAllTypes.getDescriptor()) + .addVar("msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .build(); + CelAbstractSyntaxTree ast = + celCompiler.compile("has(msg.single_any) ? msg.single_any : 10").getAst(); + + String formattedExpr = CelExprFormatter.format(ast.getExpr()); + + assertThat(formattedExpr) + .isEqualTo( + "CALL [5] {\n" + + " function: _?_:_\n" + + " args: {\n" + + " SELECT [4] {\n" + + " IDENT [2] {\n" + + " name: msg\n" + + " }.single_any~presence_test\n" + + " }\n" + + " SELECT [7] {\n" + + " IDENT [6] {\n" + + " name: msg\n" + + " }.single_any\n" + " }\n" + + " CONSTANT [8] { value: 10 }\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 ce8df4d7f..1610acb52 100644 --- a/common/src/test/java/dev/cel/common/ast/CelExprVisitorTest.java +++ b/common/src/test/java/dev/cel/common/ast/CelExprVisitorTest.java @@ -15,26 +15,27 @@ package dev.cel.common.ast; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth8.assertThat; import static org.junit.Assert.assertThrows; 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.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; @@ -54,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(); @@ -74,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); @@ -125,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 @@ -195,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(); @@ -205,17 +206,14 @@ public void visitSelect() throws Exception { assertThat(visited) .isEqualTo( VisitedReference.newBuilder() - .setCreateStruct( - CelCreateStruct.newBuilder().setMessageName("TestAllTypes").build()) + .setStruct(CelStruct.newBuilder().setMessageName("TestAllTypes").build()) .setSelect( CelSelect.newBuilder() .setOperand( CelExpr.newBuilder() .setId(1) - .setCreateStruct( - CelCreateStruct.newBuilder() - .setMessageName("TestAllTypes") - .build()) + .setStruct( + CelStruct.newBuilder().setMessageName("TestAllTypes").build()) .build()) .setField("single_int64") .build()) @@ -238,19 +236,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(); @@ -262,13 +260,13 @@ 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") .build()) @@ -276,7 +274,7 @@ public void visitCreateStruct_fieldkey() throws Exception { } @Test - public void visitCreateMap() throws Exception { + public void visitMap() throws Exception { CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder().build(); CelAbstractSyntaxTree ast = celCompiler.compile("{'a': 'b'}").getAst(); @@ -287,20 +285,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(); @@ -312,8 +310,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()) @@ -324,6 +322,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(); @@ -334,20 +333,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..2307b6219 --- /dev/null +++ b/common/src/test/java/dev/cel/common/ast/CelMutableExprTest.java @@ -0,0 +1,1023 @@ +// 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_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/ast/CelReferenceTest.java b/common/src/test/java/dev/cel/common/ast/CelReferenceTest.java index 0eb73e4c8..dbe6613be 100644 --- a/common/src/test/java/dev/cel/common/ast/CelReferenceTest.java +++ b/common/src/test/java/dev/cel/common/ast/CelReferenceTest.java @@ -15,7 +15,6 @@ package dev.cel.common.ast; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth8.assertThat; import static org.junit.Assert.assertThrows; import com.google.common.collect.ImmutableList; 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..d46607118 100644 --- a/common/src/test/java/dev/cel/common/internal/BUILD.bazel +++ b/common/src/test/java/dev/cel/common/internal/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,8 +11,9 @@ java_library( srcs = glob(["*.java"]), resources = ["//common/src/test/resources"], deps = [ + "//:auto_value", "//:java_truth", - "//common", + "//common:cel_descriptors", "//common:options", "//common/ast", "//common/internal", @@ -19,25 +21,30 @@ 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: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 07f33fc8b..877d6a976 100644 --- a/common/src/test/java/dev/cel/common/internal/CombinedDescriptorPoolTest.java +++ b/common/src/test/java/dev/cel/common/internal/CombinedDescriptorPoolTest.java @@ -15,16 +15,14 @@ package dev.cel.common.internal; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth8.assertThat; import com.google.common.collect.ImmutableList; import com.google.protobuf.Descriptors.FieldDescriptor; 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; @@ -68,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 @@ -88,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 175adb121..d6749a391 100644 --- a/common/src/test/java/dev/cel/common/internal/DefaultInstanceMessageFactoryTest.java +++ b/common/src/test/java/dev/cel/common/internal/DefaultInstanceMessageFactoryTest.java @@ -16,7 +16,6 @@ import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth8.assertThat; import static java.util.Arrays.stream; import com.google.common.collect.ImmutableList; @@ -32,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; @@ -45,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 { @@ -113,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 593817d10..b26197d04 100644 --- a/common/src/test/java/dev/cel/common/internal/DefaultMessageFactoryTest.java +++ b/common/src/test/java/dev/cel/common/internal/DefaultMessageFactoryTest.java @@ -15,7 +15,6 @@ package dev.cel.common.internal; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth8.assertThat; import static java.nio.charset.StandardCharsets.UTF_8; import com.google.common.base.Ascii; @@ -30,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; @@ -60,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/ErrorsTest.java b/common/src/test/java/dev/cel/common/internal/ErrorsTest.java index 2b7690715..ad46f7737 100644 --- a/common/src/test/java/dev/cel/common/internal/ErrorsTest.java +++ b/common/src/test/java/dev/cel/common/internal/ErrorsTest.java @@ -15,7 +15,6 @@ package dev.cel.common.internal; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth8.assertThat; 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 83795b554..3172258f4 100644 --- a/common/src/test/java/dev/cel/common/internal/ProtoAdapterTest.java +++ b/common/src/test/java/dev/cel/common/internal/ProtoAdapterTest.java @@ -15,7 +15,7 @@ package dev.cel.common.internal; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth8.assertThat; +import static org.junit.Assert.assertThrows; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -26,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; @@ -38,6 +40,7 @@ import com.google.protobuf.Value; import com.google.type.Expr; import dev.cel.common.CelOptions; +import dev.cel.common.values.CelByteString; import java.util.Arrays; import java.util.List; import java.util.Optional; @@ -52,10 +55,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); @@ -67,50 +66,41 @@ 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 }, { Duration.newBuilder().setSeconds(123).build(), 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( @@ -118,7 +108,6 @@ public static List data() { .addValues(Value.newBuilder().setStringValue("hello")) .addValues(Value.newBuilder().setStringValue("world")) .build()), - LEGACY }, { ImmutableMap.of("hello", "world"), @@ -126,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", @@ -140,30 +130,31 @@ 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 }, { Timestamp.newBuilder().setSeconds(123).build(), Any.pack(Timestamp.newBuilder().setSeconds(123).build()), - LEGACY }, - // 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.DEFAULT); assertThat(protoAdapter.adaptValueToProto(value, proto.getDescriptorForType().getFullName())) - .hasValue(proto); + .isEqualTo(proto); assertThat(protoAdapter.adaptProtoToValue(proto)).isEqualTo(value); } } @@ -180,94 +171,89 @@ 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); - } - @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/WellKnownProtoTest.java b/common/src/test/java/dev/cel/common/internal/WellKnownProtoTest.java new file mode 100644 index 000000000..bb75a341a --- /dev/null +++ b/common/src/test/java/dev/cel/common/internal/WellKnownProtoTest.java @@ -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. + +package dev.cel.common.internal; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.protobuf.FloatValue; +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(); + } +} 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..172e8f5ea 100644 --- a/common/src/test/java/dev/cel/common/navigation/BUILD.bazel +++ b/common/src/test/java/dev/cel/common/navigation/BUILD.bazel @@ -1,3 +1,4 @@ +load("@rules_java//java:defs.bzl", "java_library") load("//:testing.bzl", "junit4_test_suites") package(default_applicable_licenses = ["//:license"]) @@ -8,17 +9,22 @@ java_library( srcs = glob(["*.java"]), deps = [ "//:java_truth", - "//common", + "//common:cel_ast", "//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/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 e4d293098..6fa4cd203 100644 --- a/common/src/test/java/dev/cel/common/navigation/CelNavigableAstTest.java +++ b/common/src/test/java/dev/cel/common/navigation/CelNavigableAstTest.java @@ -15,7 +15,6 @@ package dev.cel.common.navigation; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth8.assertThat; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.ast.CelConstant; @@ -38,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 303fef42e..69b244708 100644 --- a/common/src/test/java/dev/cel/common/navigation/CelNavigableExprTest.java +++ b/common/src/test/java/dev/cel/common/navigation/CelNavigableExprTest.java @@ -15,7 +15,6 @@ package dev.cel.common.navigation; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth8.assertThat; import dev.cel.common.ast.CelConstant; import dev.cel.common.ast.CelExpr; @@ -28,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(); @@ -39,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 a496a133a..11bf79f27 100644 --- a/common/src/test/java/dev/cel/common/navigation/CelNavigableExprVisitorTest.java +++ b/common/src/test/java/dev/cel/common/navigation/CelNavigableExprVisitorTest.java @@ -16,30 +16,30 @@ import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth8.assertThat; import static dev.cel.common.CelFunctionDecl.newFunctionDeclaration; import static dev.cel.common.CelOverloadDecl.newMemberOverload; import static org.junit.Assert.assertThrows; 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 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.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,210 @@ 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 + public void add_preOrder_heightSet() throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder().addVar("a", SimpleType.INT).build(); + // Tree shape: + // + + // + 2 + // 1 a + CelAbstractSyntaxTree ast = compiler.compile("1 + a + 2").getAst(); + CelNavigableAst navigableAst = CelNavigableAst.fromAst(ast); + + ImmutableList allNodeHeights = + navigableAst + .getRoot() + .allNodes(TraversalOrder.PRE_ORDER) + .map(CelNavigableExpr::height) + .collect(toImmutableList()); + 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 = + CelCompilerFactory.standardCelCompilerBuilder().addVar("a", SimpleType.INT).build(); + // Tree shape: + // + + // + 2 + // 1 a + CelAbstractSyntaxTree ast = compiler.compile("1 + a + 2").getAst(); + CelNavigableAst navigableAst = CelNavigableAst.fromAst(ast); + + ImmutableList allNodeHeights = + navigableAst + .getRoot() + .allNodes(TraversalOrder.POST_ORDER) + .map(CelNavigableExpr::height) + .collect(toImmutableList()); + 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 = + 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.height()); + while (navigableExpr.parent().isPresent()) { + navigableExpr = navigableExpr.parent().get(); + heights.add(navigableExpr.height()); + } + + 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 { + 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::height) + .collect(toImmutableList()); + 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 @@ -127,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 @@ -156,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 @@ -191,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 @@ -244,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 @@ -267,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); @@ -315,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 @@ -339,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); } @@ -369,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); } @@ -392,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 @@ -401,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); @@ -409,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)))); + 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); @@ -434,21 +613,79 @@ 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", 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 + public void messageConstruction_preOrder_heightSet() 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.PRE_ORDER) + .map(CelNavigableExpr::height) + .collect(toImmutableList()); + + assertThat(allNodes).containsExactly(1, 0).inOrder(); + } + + @Test + public void messageConstruction_postOrder_heightSet() 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.POST_ORDER) + .map(CelNavigableExpr::height) + .collect(toImmutableList()); + + 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 @@ -461,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); @@ -483,18 +718,80 @@ 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 + public void mapConstruction_preOrder_heightSet() 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::height) + .collect(toImmutableList()); + + assertThat(allNodes).containsExactly(1, 0, 0).inOrder(); + } + + @Test + public void mapConstruction_postOrder_heightSet() 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::height) + .collect(toImmutableList()); + + 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 @@ -507,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(); @@ -526,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( @@ -576,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(); @@ -588,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( @@ -634,10 +931,87 @@ public void comprehension_postOrder_allNodesReturned() throws Exception { .inOrder(); } + @Test + public void comprehension_preOrder_heightSet() 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::height) + .collect(toImmutableList()); + + assertThat(allNodes).containsExactly(3, 1, 0, 0, 2, 1, 0, 1, 0, 0, 0).inOrder(); + } + + @Test + public void comprehension_postOrder_heightSet() 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::height) + .collect(toImmutableList()); + + 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(); @@ -645,36 +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 @@ -698,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(); @@ -710,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); } @@ -767,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, @@ -804,19 +1176,171 @@ 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(); } + @Test + public void callExpr_preOrder_heightSet() 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::height) + .collect(toImmutableList()); + + assertThat(allNodes).containsExactly(4, 3, 2, 1, 0, 0, 0, 0, 2, 1, 0, 0, 0, 0).inOrder(); + } + + @Test + public void callExpr_postOrder_heightSet() 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::height) + .collect(toImmutableList()); + + assertThat(allNodes).containsExactly(0, 0, 1, 0, 2, 0, 3, 0, 0, 1, 0, 2, 0, 4).inOrder(); + } + + @Test + 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(); + 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::height) + .collect(toImmutableList()); + 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..600a63940 100644 --- a/common/src/test/java/dev/cel/common/types/BUILD.bazel +++ b/common/src/test/java/dev/cel/common/types/BUILD.bazel @@ -1,3 +1,4 @@ +load("@rules_java//java:defs.bzl", "java_library") load("//:testing.bzl", "junit4_test_suites") package(default_applicable_licenses = ["//:license"]) @@ -8,15 +9,16 @@ 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", + "@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/EnumTypeTest.java b/common/src/test/java/dev/cel/common/types/EnumTypeTest.java index 3f59e4011..3fdc47148 100644 --- a/common/src/test/java/dev/cel/common/types/EnumTypeTest.java +++ b/common/src/test/java/dev/cel/common/types/EnumTypeTest.java @@ -14,7 +14,7 @@ package dev.cel.common.types; -import static com.google.common.truth.Truth8.assertThat; +import static com.google.common.truth.Truth.assertThat; import com.google.common.collect.ImmutableMap; import org.junit.Before; 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 7efc23620..d774903e1 100644 --- a/common/src/test/java/dev/cel/common/types/ProtoMessageTypeProviderTest.java +++ b/common/src/test/java/dev/cel/common/types/ProtoMessageTypeProviderTest.java @@ -16,14 +16,12 @@ import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth8.assertThat; 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.expr.conformance.proto2.TestAllTypes; +import dev.cel.expr.conformance.proto2.TestAllTypesExtensions; import java.util.Optional; import org.junit.Test; import org.junit.runner.RunWith; @@ -35,12 +33,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() { @@ -56,21 +57,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"); @@ -80,12 +80,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); @@ -95,11 +94,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(); @@ -109,102 +108,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")) + 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.nested_enum_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(); @@ -214,14 +205,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())) @@ -229,7 +219,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)); 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 5790be414..3feab06fd 100644 --- a/common/src/test/java/dev/cel/common/types/ProtoMessageTypeTest.java +++ b/common/src/test/java/dev/cel/common/types/ProtoMessageTypeTest.java @@ -15,7 +15,6 @@ package dev.cel.common.types; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth8.assertThat; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; 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..9d9132cbb 100644 --- a/common/src/test/java/dev/cel/common/values/BUILD.bazel +++ b/common/src/test/java/dev/cel/common/values/BUILD.bazel @@ -1,3 +1,4 @@ +load("@rules_java//java:defs.bzl", "java_library") load("//:testing.bzl", "junit4_test_suites") package(default_applicable_licenses = ["//:license"]) @@ -9,30 +10,38 @@ java_library( deps = [ "//:java_truth", "//bundle:cel", - "//common", + "//common:cel_ast", + "//common:cel_descriptors", "//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/internal:proto_time_utils", + "//common/internal:well_known_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_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_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/CelValueConverterTest.java b/common/src/test/java/dev/cel/common/values/CelValueConverterTest.java index c373f04fc..528cddee4 100644 --- a/common/src/test/java/dev/cel/common/values/CelValueConverterTest.java +++ b/common/src/test/java/dev/cel/common/values/CelValueConverterTest.java @@ -18,7 +18,6 @@ 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,8 +25,7 @@ @RunWith(JUnit4.class) public class CelValueConverterTest { - private static final CelValueConverter CEL_VALUE_CONVERTER = - new CelValueConverter(CelOptions.DEFAULT) {}; + private static final CelValueConverter CEL_VALUE_CONVERTER = new CelValueConverter() {}; @Test public void fromJavaPrimitiveToCelValue_returnsOpaqueValue() { 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 953513100..6a991a6a9 100644 --- a/common/src/test/java/dev/cel/common/values/OptionalValueTest.java +++ b/common/src/test/java/dev/cel/common/values/OptionalValueTest.java @@ -15,7 +15,6 @@ package dev.cel.common.values; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth8.assertThat; import static org.junit.Assert.assertThrows; import com.google.common.collect.ImmutableMap; 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..b7e72c6b8 100644 --- a/common/src/test/java/dev/cel/common/values/ProtoCelValueConverterTest.java +++ b/common/src/test/java/dev/cel/common/values/ProtoCelValueConverterTest.java @@ -16,15 +16,12 @@ 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 dev.cel.common.internal.ProtoTimeUtils; import java.time.Instant; import org.junit.Test; import org.junit.runner.RunWith; @@ -35,9 +32,7 @@ public class ProtoCelValueConverterTest { private static final ProtoCelValueConverter PROTO_CEL_VALUE_CONVERTER = ProtoCelValueConverter.newInstance( - CelOptions.DEFAULT, - DefaultDescriptorPool.INSTANCE, - DynamicProto.create(DefaultMessageFactory.INSTANCE)); + DefaultDescriptorPool.INSTANCE, DynamicProto.create(DefaultMessageFactory.INSTANCE)); @Test public void fromCelValueToJavaObject_returnsTimestampValue() { @@ -46,7 +41,7 @@ public void fromCelValueToJavaObject_returnsTimestampValue() { PROTO_CEL_VALUE_CONVERTER.fromCelValueToJavaObject( TimestampValue.create(Instant.ofEpochSecond(50))); - assertThat(timestamp).isEqualTo(Timestamps.fromSeconds(50)); + assertThat(timestamp).isEqualTo(ProtoTimeUtils.fromSecondsToTimestamp(50)); } @Test @@ -56,25 +51,29 @@ public void fromCelValueToJavaObject_returnsDurationValue() { PROTO_CEL_VALUE_CONVERTER.fromCelValueToJavaObject( DurationValue.create(java.time.Duration.ofSeconds(10))); - assertThat(duration).isEqualTo(Durations.fromSeconds(10)); + assertThat(duration).isEqualTo(ProtoTimeUtils.fromSecondsToDuration(10)); } @Test - public void fromCelValueToJavaObject_returnsBytesValue() { - ByteString byteString = - (ByteString) + public void fromCelValueToJavaObject_returnsCelBytesValue() { + CelByteString byteString = + (CelByteString) PROTO_CEL_VALUE_CONVERTER.fromCelValueToJavaObject( BytesValue.create(CelByteString.of(new byte[] {0x1, 0x5, 0xc}))); - assertThat(byteString).isEqualTo(ByteString.copyFrom(new byte[] {0x1, 0x5, 0xc})); + // Note: No conversion is attempted. CelByteString is the native Java type equivalent for CEL's + // bytes. + assertThat(byteString).isEqualTo(CelByteString.of(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); + public void fromCelValueToJavaObject_returnsCelNullValue() { + NullValue nullValue = + (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..1e1d5063f --- /dev/null +++ b/common/src/test/java/dev/cel/common/values/ProtoLiteCelValueConverterTest.java @@ -0,0 +1,315 @@ +// 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 static java.nio.charset.StandardCharsets.UTF_8; + +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.protobuf.ByteString; +import com.google.protobuf.Duration; +import com.google.protobuf.ExtensionRegistryLite; +import com.google.protobuf.FloatValue; +import com.google.protobuf.Int32Value; +import com.google.protobuf.Int64Value; +import com.google.protobuf.MessageLite; +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_convertsToProtoMessageLiteValue() { + ProtoMessageLiteValue protoMessageLiteValue = + (ProtoMessageLiteValue) + PROTO_LITE_CEL_VALUE_CONVERTER.fromProtoMessageToCelValue( + TestAllTypes.getDefaultInstance()); + + assertThat(protoMessageLiteValue.value()).isEqualTo(TestAllTypes.getDefaultInstance()); + } + + private enum WellKnownProtoTestCase { + BOOL(com.google.protobuf.BoolValue.of(true), BoolValue.create(true)), + BYTES( + com.google.protobuf.BytesValue.of(ByteString.copyFromUtf8("test")), + BytesValue.create(CelByteString.of("test".getBytes(UTF_8)))), + FLOAT(FloatValue.of(1.0f), DoubleValue.create(1.0f)), + DOUBLE(com.google.protobuf.DoubleValue.of(1.0), DoubleValue.create(1.0)), + INT32(Int32Value.of(1), IntValue.create(1)), + INT64(Int64Value.of(1L), IntValue.create(1L)), + STRING(com.google.protobuf.StringValue.of("test"), StringValue.create("test")), + + DURATION( + Duration.newBuilder().setSeconds(10).setNanos(50).build(), + DurationValue.create(java.time.Duration.ofSeconds(10, 50))), + TIMESTAMP( + Timestamp.newBuilder().setSeconds(1678886400L).setNanos(123000000).build(), + TimestampValue.create(Instant.ofEpochSecond(1678886400L, 123000000))), + UINT32(UInt32Value.of(1), UintValue.create(1)), + UINT64(UInt64Value.of(1L), UintValue.create(1L)), + ; + + private final MessageLite msg; + private final CelValue celValue; + + WellKnownProtoTestCase(MessageLite msg, CelValue celValue) { + this.msg = msg; + this.celValue = celValue; + } + } + + @Test + public void fromProtoMessageToCelValue_withWellKnownProto_convertsToEquivalentCelValue( + @TestParameter WellKnownProtoTestCase testCase) { + CelValue convertedCelValue = + PROTO_LITE_CEL_VALUE_CONVERTER.fromProtoMessageToCelValue(testCase.msg); + + assertThat(convertedCelValue).isEqualTo(testCase.celValue); + } + + /** 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..812da6963 --- /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..03891bb09 --- /dev/null +++ b/common/src/test/java/dev/cel/common/values/ProtoMessageLiteValueTest.java @@ -0,0 +1,274 @@ +// 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(); + } + + private enum SelectFieldTestCase { + BOOL("single_bool", BoolValue.create(true)), + INT32("single_int32", IntValue.create(4L)), + INT64("single_int64", IntValue.create(5L)), + SINT32("single_sint32", IntValue.create(1L)), + SINT64("single_sint64", IntValue.create(2L)), + 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)), + FIXED32("single_fixed32", IntValue.create(20)), + SFIXED32("single_sfixed32", IntValue.create(30)), + FIXED64("single_fixed64", IntValue.create(40)), + SFIXED64("single_sfixed64", IntValue.create(50)), + STRING("single_string", StringValue.create("test")), + BYTES("single_bytes", BytesValue.create(CelByteString.of(new byte[] {0x01}))), + 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), IntValue.create(6L)))), + REPEATED_UINT64( + "repeated_uint64", + ImmutableListValue.create(ImmutableList.of(UintValue.create(7L), UintValue.create(8L)))), + REPEATED_FLOAT( + "repeated_float", + ImmutableListValue.create( + ImmutableList.of(DoubleValue.create(1.5d), DoubleValue.create(2.5d)))), + REPEATED_DOUBLE( + "repeated_double", + ImmutableListValue.create( + ImmutableList.of(DoubleValue.create(3.5d), DoubleValue.create(4.5d)))), + REPEATED_STRING( + "repeated_string", + ImmutableListValue.create( + ImmutableList.of(StringValue.create("foo"), StringValue.create("bar")))), + MAP_INT64_INT64( + "map_int64_int64", + ImmutableMapValue.create( + ImmutableMap.of( + IntValue.create(1L), + IntValue.create(2L), + IntValue.create(3L), + IntValue.create(4L)))), + MAP_UINT32_UINT64( + "map_uint32_uint64", + ImmutableMapValue.create( + ImmutableMap.of( + UintValue.create(5L), + UintValue.create(6L), + UintValue.create(7L), + UintValue.create(8L)))), + MAP_STRING_STRING( + "map_string_string", + ImmutableMapValue.create( + ImmutableMap.of(StringValue.create("a"), StringValue.create("b")))), + NESTED_ENUM("standalone_enum", IntValue.create(1L)); + + private final String fieldName; + private final CelValue celValue; + + SelectFieldTestCase(String fieldName, CelValue celValue) { + this.fieldName = fieldName; + this.celValue = celValue; + } + } + + @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); + + CelValue selectedValue = protoMessageValue.select(StringValue.create(testCase.fieldName)); + + assertThat(selectedValue).isEqualTo(testCase.celValue); + assertThat(selectedValue.isZeroValue()).isFalse(); + } + + private enum DefaultValueTestCase { + BOOL("single_bool", BoolValue.create(false)), + INT32("single_int32", IntValue.create(0L)), + INT64("single_int64", IntValue.create(0L)), + SINT32("single_sint32", IntValue.create(0L)), + SINT64("single_sint64", IntValue.create(0L)), + UINT32("single_uint32", UintValue.create(0L)), + UINT64("single_uint64", UintValue.create(0L)), + FIXED32("single_fixed32", IntValue.create(0)), + SFIXED32("single_sfixed32", IntValue.create(0)), + FIXED64("single_fixed64", IntValue.create(0)), + SFIXED64("single_sfixed64", IntValue.create(0)), + FLOAT("single_float", DoubleValue.create(0d)), + DOUBLE("single_double", DoubleValue.create(0d)), + STRING("single_string", StringValue.create("")), + BYTES("single_bytes", BytesValue.create(CelByteString.EMPTY)), + DURATION("single_duration", DurationValue.create(Duration.ZERO)), + TIMESTAMP("single_timestamp", TimestampValue.create(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", ImmutableListValue.create(ImmutableList.of())), + MAP_INT64_INT64("map_int64_int64", ImmutableMapValue.create(ImmutableMap.of())), + NESTED_ENUM("standalone_enum", IntValue.create(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 CelValue celValue; + + DefaultValueTestCase(String fieldName, CelValue celValue) { + this.fieldName = fieldName; + this.celValue = celValue; + } + } + + @Test + public void selectField_defaultValue(@TestParameter DefaultValueTestCase testCase) { + ProtoMessageLiteValue protoMessageValue = + ProtoMessageLiteValue.create( + TestAllTypes.getDefaultInstance(), + "cel.expr.conformance.proto3.TestAllTypes", + PROTO_LITE_CEL_VALUE_CONVERTER); + + CelValue selectedValue = protoMessageValue.select(StringValue.create(testCase.fieldName)); + + assertThat(selectedValue).isEqualTo(testCase.celValue); + assertThat(selectedValue.isZeroValue()).isTrue(); + } +} 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..891a7d0e2 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,17 @@ 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.common.internal.ProtoTimeUtils; +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 +44,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 +66,7 @@ public void newValue_createEmptyProtoMessage() { @Test public void newValue_createProtoMessage_fieldsPopulated() { ProtoMessageValueProvider protoMessageValueProvider = - ProtoMessageValueProvider.newInstance(DYNAMIC_PROTO, CelOptions.DEFAULT); + ProtoMessageValueProvider.newInstance(CelOptions.DEFAULT, DYNAMIC_PROTO); ProtoMessageValue protoMessageValue = (ProtoMessageValue) @@ -95,9 +89,9 @@ public void newValue_createProtoMessage_fieldsPopulated() { "single_string", "hello", "single_timestamp", - Timestamps.fromSeconds(50), + ProtoTimeUtils.fromSecondsToTimestamp(50), "single_duration", - Durations.fromSeconds(100))) + ProtoTimeUtils.fromSecondsToDuration(100))) .get(); assertThat(protoMessageValue.isZeroValue()).isFalse(); @@ -106,9 +100,9 @@ public void newValue_createProtoMessage_fieldsPopulated() { assertThat(protoMessageValue.select(StringValue.create("single_int64"))) .isEqualTo(IntValue.create(2L)); assertThat(protoMessageValue.select(StringValue.create("single_uint32"))) - .isEqualTo(UintValue.create(3L, false)); + .isEqualTo(UintValue.create(3L)); assertThat(protoMessageValue.select(StringValue.create("single_uint64"))) - .isEqualTo(UintValue.create(4L, false)); + .isEqualTo(UintValue.create(4L)); assertThat(protoMessageValue.select(StringValue.create("single_double"))) .isEqualTo(DoubleValue.create(5.5d)); assertThat(protoMessageValue.select(StringValue.create("single_bool"))) @@ -124,8 +118,7 @@ public void newValue_createProtoMessage_fieldsPopulated() { @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) @@ -146,7 +139,7 @@ public void newValue_createProtoMessage_unsignedLongFieldsPopulated() { @Test public void newValue_createProtoMessage_wrappersPopulated() { ProtoMessageValueProvider protoMessageValueProvider = - ProtoMessageValueProvider.newInstance(DYNAMIC_PROTO, CelOptions.DEFAULT); + ProtoMessageValueProvider.newInstance(CelOptions.DEFAULT, DYNAMIC_PROTO); ProtoMessageValue protoMessageValue = (ProtoMessageValue) @@ -178,9 +171,9 @@ public void newValue_createProtoMessage_wrappersPopulated() { assertThat(protoMessageValue.select(StringValue.create("single_int64_wrapper")).value()) .isEqualTo(2L); assertThat(protoMessageValue.select(StringValue.create("single_uint32_wrapper")).value()) - .isEqualTo(3L); + .isEqualTo(UnsignedLong.valueOf(3L)); assertThat(protoMessageValue.select(StringValue.create("single_uint64_wrapper")).value()) - .isEqualTo(4L); + .isEqualTo(UnsignedLong.valueOf(4L)); assertThat(protoMessageValue.select(StringValue.create("single_double_wrapper")).value()) .isEqualTo(5.5d); assertThat(protoMessageValue.select(StringValue.create("single_bool_wrapper")).value()) @@ -192,43 +185,36 @@ public void newValue_createProtoMessage_wrappersPopulated() { @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")) + .select(StringValue.create("cel.expr.conformance.proto2.int32_ext")) .value()) .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 +227,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) 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 9b0f5b8ef..f2e5aef5a 100644 --- a/common/src/test/java/dev/cel/common/values/ProtoMessageValueTest.java +++ b/common/src/test/java/dev/cel/common/values/ProtoMessageValueTest.java @@ -15,7 +15,6 @@ package dev.cel.common.values; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth8.assertThat; import static org.junit.Assert.assertThrows; import com.google.common.collect.ImmutableList; @@ -36,17 +35,15 @@ import com.google.testing.junit.testparameterinjector.TestParameterInjector; import com.google.testing.junit.testparameterinjector.TestParameters; import dev.cel.common.CelDescriptorUtil; -import dev.cel.common.CelOptions; 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.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; @@ -57,9 +54,7 @@ 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)); + DefaultDescriptorPool.INSTANCE, DynamicProto.create(DefaultMessageFactory.INSTANCE)); @Test public void emptyProtoMessage() { @@ -131,7 +126,7 @@ public void findField_fieldIsUndeclared_throwsException() { .hasMessageThat() .isEqualTo( "field 'bogus' is not declared in message" - + " 'dev.cel.testing.testdata.proto2.TestAllTypes'"); + + " 'cel.expr.conformance.proto2.TestAllTypes'"); } @Test @@ -139,27 +134,25 @@ public void findField_extensionField_success() { CelDescriptorPool descriptorPool = DefaultDescriptorPool.create( CelDescriptorUtil.getAllDescriptorsFromFileDescriptor( - ImmutableList.of(MessagesProto2Extensions.getDescriptor()))); + ImmutableList.of(TestAllTypesExtensions.getDescriptor()))); ProtoCelValueConverter protoCelValueConverter = ProtoCelValueConverter.newInstance( - CelOptions.DEFAULT, DefaultDescriptorPool.INSTANCE, DynamicProto.create(DefaultMessageFactory.create(descriptorPool))); - Proto2Message proto2Message = - Proto2Message.newBuilder().setExtension(MessagesProto2Extensions.int32Ext, 1).build(); + TestAllTypes proto2Message = + TestAllTypes.newBuilder().setExtension(TestAllTypesExtensions.int32Ext, 1).build(); ProtoMessageValue protoMessageValue = ProtoMessageValue.create(proto2Message, descriptorPool, protoCelValueConverter); - assertThat( - protoMessageValue.find(StringValue.create("dev.cel.testing.testdata.proto2.int32_ext"))) + assertThat(protoMessageValue.find(StringValue.create("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( @@ -170,12 +163,12 @@ public void findField_extensionField_throwsWhenDescriptorMissing() { IllegalArgumentException.class, () -> protoMessageValue.select( - StringValue.create("dev.cel.testing.testdata.proto2.int32_ext"))); + StringValue.create("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'"); } private enum SelectFieldTestCase { 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..1ab1137e0 100644 --- a/common/src/test/java/dev/cel/common/values/StructValueTest.java +++ b/common/src/test/java/dev/cel/common/values/StructValueTest.java @@ -26,10 +26,12 @@ 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; import dev.cel.common.types.StructType; +import dev.cel.expr.conformance.proto3.TestAllTypes; import java.util.Map; import java.util.Optional; import org.junit.Test; @@ -176,6 +178,56 @@ public void evaluate_usingCustomClass_selectField() throws Exception { assertThat(result).isEqualTo(5L); } + @Test + public void evaluate_usingMultipleProviders_selectFieldFromCustomClass() throws Exception { + Cel cel = + CelFactory.standardCelBuilder() + .setOptions(CelOptions.current().enableCelValue(true).build()) + .setTypeProvider(CUSTOM_STRUCT_TYPE_PROVIDER) + .setValueProvider( + CombinedCelValueProvider.combine( + ProtoMessageValueProvider.newInstance( + CelOptions.DEFAULT, DynamicProto.create(typeName -> 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); + } + + @Test + public void evaluate_usingMultipleProviders_selectFieldFromProtobufMessage() throws Exception { + Cel cel = + CelFactory.standardCelBuilder() + .setOptions(CelOptions.current().enableCelValue(true).build()) + .addMessageTypes(TestAllTypes.getDescriptor()) + .setTypeProvider(CUSTOM_STRUCT_TYPE_PROVIDER) + .setValueProvider( + CombinedCelValueProvider.combine( + ProtoMessageValueProvider.newInstance( + CelOptions.DEFAULT, + // Note: this is unideal. Future iterations should make DynamicProto + // completely an internal concern, and not expose it at all. + DynamicProto.create( + typeName -> { + if (typeName.equals(TestAllTypes.getDescriptor().getFullName())) { + return Optional.of(TestAllTypes.newBuilder()); + } + return Optional.empty(); + })), + CUSTOM_STRUCT_VALUE_PROVIDER)) + .build(); + CelAbstractSyntaxTree ast = + cel.compile("cel.expr.conformance.proto3.TestAllTypes{single_string: 'foo'}.single_string") + .getAst(); + + String result = (String) cel.createProgram(ast).eval(); + + assertThat(result).isEqualTo("foo"); + } + @SuppressWarnings("Immutable") // Test only private static class CelCustomStructValue extends StructValue { 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..6c2b1a269 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,38 @@ 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 = "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"], +) diff --git a/common/values/BUILD.bazel b/common/values/BUILD.bazel index 931999b4c..f1fa107b6 100644 --- a/common/values/BUILD.bazel +++ b/common/values/BUILD.bazel @@ -1,24 +1,62 @@ +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 = "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 = "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 +64,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 +72,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/CelCompiler.java b/compiler/src/main/java/dev/cel/compiler/CelCompiler.java index dd4d5dd10..602646ab4 100644 --- a/compiler/src/main/java/dev/cel/compiler/CelCompiler.java +++ b/compiler/src/main/java/dev/cel/compiler/CelCompiler.java @@ -57,4 +57,6 @@ default CelValidationResult compile(String expression, String description) { throw new IllegalStateException("this method must only be called when !hasError()", ex); } } + + CelCompilerBuilder toCompilerBuilder(); } 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 5a99054a6..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,34 @@ 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 + public CelCompilerBuilder toCompilerBuilder() { + return newBuilder(toParserBuilder(), toCheckerBuilder()); + } + + @Override + public CelCheckerBuilder toCheckerBuilder() { + return checker.toCheckerBuilder(); + } + + @Override + public CelParserBuilder toParserBuilder() { + return parser.toParserBuilder(); } /** Combines a prebuilt {@link CelParser} and {@link CelChecker} into {@link CelCompilerImpl}. */ @@ -134,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 @@ -200,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 @@ -258,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..aa10b1324 --- /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_descriptors", + "//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..69755324d --- /dev/null +++ b/conformance/src/test/java/dev/cel/conformance/BUILD.bazel @@ -0,0 +1,171 @@ +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", + "//extensions", + "//extensions:optional_library", + "//parser:macro", + "//parser:parser_builder", + "//parser:parser_factory", + "//runtime", + "//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_cel"] + +java_library( + name = "run_maven_jar", + testonly = True, + srcs = glob(["*.java"]), + tags = ["conformance_maven"], + deps = MAVEN_JAR_DEPS + [ + "//:java_truth", + "//parser:parser_factory", # TODO: Remove next OSS release + "//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/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 = [ + # Tests which require spec changes. + # TODO: Deprecate Duration.get_milliseconds + "timestamps/duration_converters/get_milliseconds", + + # Broken test cases which should be supported. + # TODO: Invalid bytes to string conversion should error. + "conversions/string/bytes_invalid", + # TODO: Support setting / getting enum values out of the defined enum value range. + "enums/legacy_proto2/select_big,select_neg", + "enums/legacy_proto2/assign_standalone_int_big,assign_standalone_int_neg", + # TODO: Generate errors on enum value assignment overflows for proto3. + "enums/legacy_proto3/assign_standalone_int_too_big,assign_standalone_int_too_neg", + # TODO: Ensure overflow occurs on conversions of double values which might not work properly on all platforms. + "conversions/int/double_int_min_range", + # TODO: Duration and timestamp operations should error on overflow. + "timestamps/duration_range/from_string_under,from_string_over", + "timestamps/timestamp_range/sub_time_duration_over,sub_time_duration_under", + # TODO: Ensure adding negative duration values is appropriately supported. + "timestamps/timestamp_arithmetic/add_time_to_duration_nanos_negative", + + # Skip until fixed. + "fields/qualified_identifier_resolution/map_value_repeat_key_heterogeneous", + # TODO: Add strings.format and strings.quote. + "string_ext/quote", + "string_ext/format", + "string_ext/format_errors", + + # TODO: Fix null assignment to a field + "proto2/set_null/single_message", + "proto2/set_null/single_duration", + "proto2/set_null/single_timestamp", + "proto3/set_null/single_message", + "proto3/set_null/single_duration", + "proto3/set_null/single_timestamp", + + # Future features for CEL 1.0 + # TODO: Strong typing support for enums, specified but not implemented. + "enums/strong_proto2", + "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", +] + +conformance_test( + name = "conformance", + data = _ALL_TESTS, + skip_tests = _TESTS_TO_SKIP, +) + +conformance_test( + name = "conformance_maven", + data = _ALL_TESTS, + mode = MODE.MAVEN_TEST, + skip_tests = _TESTS_TO_SKIP, +) + +conformance_test( + name = "conformance_dashboard", + data = _ALL_TESTS, + mode = MODE.DASHBOARD, +) 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..c4cf8193d --- /dev/null +++ b/conformance/src/test/java/dev/cel/conformance/ConformanceTest.java @@ -0,0 +1,232 @@ +// 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.DEFAULT_EXTENSION_REGISTRY; +import static dev.cel.testing.utils.ExprValueUtils.DEFAULT_TYPE_REGISTRY; +import static dev.cel.testing.utils.ExprValueUtils.fromValue; +import static dev.cel.testing.utils.ExprValueUtils.toExprValue; + +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 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.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.CelRuntimeFactory; +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() + .evaluateCanonicalTypesToNativeValues(true) + .enableTimestampEpoch(true) + .enableHeterogeneousNumericComparisons(true) + .enableProtoDifferencerEquality(true) + .enableOptionalSyntax(true) + .enableQuotedIdentifierSyntax(true) + .build(); + + private static final CelParser PARSER_WITH_MACROS = + CelParserFactory.standardCelParserBuilder() + .setOptions(OPTIONS) + .addLibraries( + CelExtensions.bindings(), + CelExtensions.encoders(OPTIONS), + CelExtensions.math(OPTIONS), + CelExtensions.protos(), + CelExtensions.sets(OPTIONS), + CelExtensions.strings(), + CelOptionalLibrary.INSTANCE) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .build(); + + private static final CelParser PARSER_WITHOUT_MACROS = + CelParserFactory.standardCelParserBuilder() + .setOptions(OPTIONS) + .addLibraries( + CelExtensions.bindings(), + CelExtensions.encoders(OPTIONS), + CelExtensions.math(OPTIONS), + CelExtensions.protos(), + CelExtensions.sets(OPTIONS), + CelExtensions.strings(), + CelOptionalLibrary.INSTANCE) + .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(), DEFAULT_EXTENSION_REGISTRY)); + } + return CelCompilerFactory.standardCelCheckerBuilder() + .setOptions(OPTIONS) + .setContainer(CelContainer.ofName(test.getContainer())) + .addDeclarations(decls.build()) + .addFileTypes(dev.cel.expr.conformance.proto2.TestAllTypesExtensions.getDescriptor()) + .addLibraries( + CelExtensions.bindings(), + CelExtensions.encoders(OPTIONS), + CelExtensions.math(OPTIONS), + CelExtensions.sets(OPTIONS), + CelExtensions.strings(), + CelOptionalLibrary.INSTANCE) + .addMessageTypes(dev.cel.expr.conformance.proto2.TestAllTypes.getDescriptor()) + .addMessageTypes(dev.cel.expr.conformance.proto3.TestAllTypes.getDescriptor()) + .build(); + } + + private static final CelRuntime RUNTIME = + CelRuntimeFactory.standardCelRuntimeBuilder() + .setOptions(OPTIONS) + .addLibraries( + CelExtensions.encoders(OPTIONS), + CelExtensions.math(OPTIONS), + CelExtensions.sets(OPTIONS), + CelExtensions.strings(), + CelOptionalLibrary.INSTANCE) + .setExtensionRegistry(DEFAULT_EXTENSION_REGISTRY) + .addMessageTypes(dev.cel.expr.conformance.proto2.TestAllTypes.getDescriptor()) + .addMessageTypes(dev.cel.expr.conformance.proto3.TestAllTypes.getDescriptor()) + .addFileTypes(dev.cel.expr.conformance.proto2.TestAllTypesExtensions.getDescriptor()) + .build(); + + private static 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()); + 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; + + public ConformanceTest(String name, SimpleTest test, boolean skip) { + this.name = Preconditions.checkNotNull(name); + this.test = + Preconditions.checkNotNull( + defaultTestMatcherToTrueIfUnset(Preconditions.checkNotNull(test))); + this.skip = skip; + } + + 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(); + 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; + } + + Program program = RUNTIME.createProgram(response.getAst()); + ExprValue result = null; + CelEvaluationException error = null; + try { + 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(DEFAULT_TYPE_REGISTRY, DEFAULT_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(DEFAULT_TYPE_REGISTRY, DEFAULT_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..50fb0e086 --- /dev/null +++ b/conformance/src/test/java/dev/cel/conformance/ConformanceTestRunner.java @@ -0,0 +1,128 @@ +// 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.testing.utils.ExprValueUtils.DEFAULT_EXTENSION_REGISTRY; +import static dev.cel.testing.utils.ExprValueUtils.DEFAULT_TYPE_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 static ImmutableSortedMap loadTestFiles() { + List testPaths = + SPLITTER.splitToList(System.getProperty("dev.cel.conformance.ConformanceTests.tests")); + try { + TextFormat.Parser parser = + TextFormat.Parser.newBuilder().setTypeRegistry(DEFAULT_TYPE_REGISTRY).build(); + ImmutableSortedMap.Builder testFiles = + ImmutableSortedMap.naturalOrder(); + for (String testPath : testPaths) { + SimpleTestFile.Builder fileBuilder = SimpleTestFile.newBuilder(); + try (BufferedReader input = + Files.newBufferedReader(Paths.get(testPath), StandardCharsets.UTF_8)) { + parser.merge(input, DEFAULT_EXTENSION_REGISTRY, fileBuilder); + } + 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"))); + } + + 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, test.getDisableCheck() || shouldSkipTest(name))); + } + } + } + return tests; + } + + @Override + protected Description describeChild(ConformanceTest child) { + return Description.createTestDescription( + ConformanceTest.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..fbae91b00 --- /dev/null +++ b/conformance/src/test/java/dev/cel/conformance/conformance_test.bzl @@ -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. + +""" +This module contains build rules for generating the conformance test targets. +""" + +load("@rules_java//java:defs.bzl", "java_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): + args = [] + args.append("-Ddev.cel.conformance.ConformanceTests.skip_tests={}".format(",".join(_expand_tests_to_skip(skip_tests)))) + args.append("-Ddev.cel.conformance.ConformanceTests.tests={}".format(",".join(["$(location " + test + ")" for test in data]))) + return args + +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 = []): + """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), + data = data, + size = "small", + test_class = "dev.cel.conformance.ConformanceTests", + runtime_deps = ["//conformance/src/test/java/dev/cel/conformance:run"], + tags = [ + "manual", + "notap", + ], + ) + + native.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), + 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), + 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/extensions/BUILD.bazel b/extensions/BUILD.bazel index ef0e9c9fd..c6a029106 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,18 @@ 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"], +) diff --git a/extensions/src/main/java/dev/cel/extensions/BUILD.bazel b/extensions/src/main/java/dev/cel/extensions/BUILD.bazel index bf42c2ae8..c39ff2358 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,52 @@ java_library( ], deps = [ ":bindings", + ":comprehensions", ":encoders", + ":lists", ":math", + ":optional_library", ":protos", + ":regex", + ":sets", + ":sets_function", ":strings", "//common:options", + "//extensions:extension_library", + "@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 +87,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 +104,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,6 +118,7 @@ java_library( tags = [ ], deps = [ + ":extension_library", "//checker:checker_builder", "//common:compiler_common", "//common:options", @@ -72,6 +129,7 @@ java_library( "//parser:macro", "//parser:parser_builder", "//runtime", + "//runtime:function_binding", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", ], @@ -84,6 +142,7 @@ java_library( "//common:compiler_common", "//common/ast", "//compiler:compiler_builder", + "//extensions:extension_library", "//parser:macro", "//parser:parser_builder", "@maven//:com_google_errorprone_error_prone_annotations", @@ -97,10 +156,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 +173,145 @@ java_library( name = "optional_library", srcs = ["CelOptionalLibrary.java"], tags = [ - "alt_dep=//extensions:optional_library", - "avoid_dep", ], deps = [ "//checker:checker_builder", "//common:compiler_common", + "//common:options", "//common/ast", "//common/types", + "//common/values", + "//common/values:cel_byte_string", "//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:options", + "//common/ast", + "//common/internal:comparison_functions", + "//common/types", + "//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", + ], +) + +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:options", + "//common/ast", + "//common/types", + "//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", + ], +) diff --git a/extensions/src/main/java/dev/cel/extensions/CelBindingsExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelBindingsExtensions.java index f05c79c6c..592c198b2 100644 --- a/extensions/src/main/java/dev/cel/extensions/CelBindingsExtensions.java +++ b/extensions/src/main/java/dev/cel/extensions/CelBindingsExtensions.java @@ -18,7 +18,9 @@ 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.ast.CelExpr; import dev.cel.compiler.CelCompilerLibrary; @@ -29,15 +31,47 @@ /** Internal implementation of the CEL local binding extensions. */ @Immutable -final class CelBindingsExtensions implements CelCompilerLibrary { - +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() { + return ImmutableSet.of(); + } + + @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..72f180db5 --- /dev/null +++ b/extensions/src/main/java/dev/cel/extensions/CelComprehensionsExtensions.java @@ -0,0 +1,506 @@ +// 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.ast.CelExpr; +import dev.cel.common.types.MapType; +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.parser.Operator; +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. */ +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) { + for (Function function : functions) { + for (CelOverloadDecl overload : function.functionDecl.overloads()) { + switch (overload.overloadId()) { + case MAP_INSERT_OVERLOAD_MAP_MAP: + runtimeBuilder.addFunctionBindings( + CelFunctionBinding.from( + MAP_INSERT_OVERLOAD_MAP_MAP, + Map.class, + Map.class, + (map1, map2) -> mapInsertMap(map1, map2, runtimeEquality))); + break; + case MAP_INSERT_OVERLOAD_KEY_VALUE: + runtimeBuilder.addFunctionBindings( + CelFunctionBinding.from( + MAP_INSERT_OVERLOAD_KEY_VALUE, + ImmutableList.of(Map.class, Object.class, Object.class), + args -> mapInsertKeyValue(args, runtimeEquality))); + break; + default: + // Nothing to add. + } + } + } + } + + @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( + "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()); + } + + // TODO: Implement a more efficient map insertion based on mutability once mutable + // maps are supported in Java stack. + private static ImmutableMap mapInsertMap( + Map targetMap, Map mapToMerge, RuntimeEquality equality) { + ImmutableMap.Builder resultBuilder = + ImmutableMap.builderWithExpectedSize(targetMap.size() + mapToMerge.size()); + + for (Map.Entry entry : mapToMerge.entrySet()) { + if (equality.findInMap(targetMap, entry.getKey()).isPresent()) { + throw new IllegalArgumentException( + String.format("insert failed: key '%s' already exists", entry.getKey())); + } else { + resultBuilder.put(entry.getKey(), entry.getValue()); + } + } + return resultBuilder.putAll(targetMap).buildOrThrow(); + } + + private static ImmutableMap mapInsertKeyValue( + Object[] args, RuntimeEquality equality) { + Map map = (Map) args[0]; + Object key = args[1]; + Object value = args[2]; + + if (equality.findInMap(map, key).isPresent()) { + throw new IllegalArgumentException( + String.format("insert failed: key '%s' already exists", key)); + } + + ImmutableMap.Builder builder = + ImmutableMap.builderWithExpectedSize(map.size() + 1); + return builder.put(key, value).putAll(map).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 (arg.exprKind().getKind() != CelExpr.ExprKind.Kind.IDENT) { + return reportArgumentError(exprFactory, arg); + } else if (arg.exprKind().ident().name().equals("__result__")) { + return reportAccumulatorOverwriteError(exprFactory, arg); + } else { + return arg; + } + } + + 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..a98f9db41 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,115 @@ /** 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(function.nativeBytesFunctionBinding); + } else { + runtimeBuilder.addFunctionBindings(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..5d70f8dfc 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,7 +14,11 @@ package dev.cel.extensions; +import static com.google.common.collect.ImmutableSet.toImmutableSet; +import static java.util.Arrays.stream; + import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Streams; import dev.cel.common.CelOptions; import java.util.Set; @@ -29,7 +33,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 +120,22 @@ 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. * * @param celOptions CelOptions to configure CelMathExtension with. This should be the same * options object used to configure the compilation/runtime environments. */ public static CelMathExtensions math(CelOptions celOptions) { - return new CelMathExtensions(celOptions); + return CelMathExtensions.library(celOptions).latest(); + } + + /** + * Returns the specified version of the 'math' extension. + * + *

Refer to README.md for functions available in each version. + */ + public static CelMathExtensions math(CelOptions celOptions, int version) { + return CelMathExtensions.library(celOptions).version(version); } /** @@ -160,14 +193,182 @@ 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; + } + + /** + * 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( + stream(CelMathExtensions.Function.values()) + .map(CelMathExtensions.Function::getFunction), + stream(CelStringExtensions.Function.values()) + .map(CelStringExtensions.Function::getFunction), + stream(SetsFunction.values()).map(SetsFunction::getFunction), + stream(CelEncoderExtensions.Function.values()) + .map(CelEncoderExtensions.Function::getFunction), + stream(CelListsExtensions.Function.values()) + .map(CelListsExtensions.Function::getFunction), + stream(CelRegexExtensions.Function.values()) + .map(CelRegexExtensions.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(options); + 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..56e0fb97b --- /dev/null +++ b/extensions/src/main/java/dev/cel/extensions/CelListsExtensions.java @@ -0,0 +1,464 @@ +// 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 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.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.parser.Operator; +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. */ +final class CelListsExtensions + implements CelCompilerLibrary, CelInternalRuntimeLibrary, CelExtensionLibrary.FeatureSet { + + @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"))))), + 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"))))) + ; + + private final CelFunctionDecl functionDecl; + private final ImmutableSet functionBindings; + + String getFunction() { + return functionDecl.name(); + } + + Function(CelFunctionDecl functionDecl, CelFunctionBinding... functionBindings) { + this.functionDecl = functionDecl; + this.functionBindings = ImmutableSet.copyOf(functionBindings); + } + } + + 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) { + for (Function function : functions) { + runtimeBuilder.addFunctionBindings(function.functionBindings); + for (CelOverloadDecl overload : function.functionDecl.overloads()) { + switch (overload.overloadId()) { + case "list_distinct": + runtimeBuilder.addFunctionBindings( + CelFunctionBinding.from( + "list_distinct", Collection.class, (list) -> distinct(list, runtimeEquality))); + break; + case "list_sort": + runtimeBuilder.addFunctionBindings( + CelFunctionBinding.from( + "list_sort", Collection.class, (list) -> sort(list, celOptions))); + break; + case "list_sortByAssociatedKeys": + runtimeBuilder.addFunctionBindings( + CelFunctionBinding.from( + "list_sortByAssociatedKeys", + Collection.class, + (list) -> sortByAssociatedKeys(list, celOptions))); + break; + default: + // Nothing to add + } + } + } + } + + 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 ImmutableList distinct( + Collection list, RuntimeEquality runtimeEquality) { + // TODO Optimize this method, which currently has the O(N^2) complexity. + int size = list.size(); + ImmutableList.Builder builder = ImmutableList.builderWithExpectedSize(size); + List theList; + if (list instanceof List) { + theList = (List) list; + } else { + theList = ImmutableList.copyOf(list); + } + for (int i = 0; i < size; i++) { + Object element = theList.get(i); + boolean found = false; + for (int j = 0; j < i; j++) { + if (runtimeEquality.objectEquals(element, theList.get(j))) { + found = true; + break; + } + } + if (!found) { + 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, CelOptions options) { + return ImmutableList.sortedCopyOf( + new CelObjectComparator(options.enableHeterogeneousNumericComparisons()), objects); + } + + private static class CelObjectComparator implements Comparator { + private final boolean enableHeterogeneousNumericComparisons; + + CelObjectComparator(boolean enableHeterogeneousNumericComparisons) { + this.enableHeterogeneousNumericComparisons = enableHeterogeneousNumericComparisons; + } + + @SuppressWarnings({"unchecked"}) + @Override + public int compare(Object o1, Object o2) { + if (o1 instanceof Number && o2 instanceof Number && enableHeterogeneousNumericComparisons) { + 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, CelOptions options) { + List[] array = keyValuePairs.toArray(new List[0]); + Arrays.sort( + array, + new CelObjectByKeyComparator( + new CelObjectComparator(options.enableHeterogeneousNumericComparisons()))); + ImmutableList.Builder builder = ImmutableList.builderWithExpectedSize(array.length); + for (List pair : array) { + builder.add(pair.get(1)); + } + 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..1e318aedc 100644 --- a/extensions/src/main/java/dev/cel/extensions/CelMathExtensions.java +++ b/extensions/src/main/java/dev/cel/extensions/CelMathExtensions.java @@ -16,10 +16,12 @@ 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; @@ -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 { +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,7 +135,7 @@ final class CelMathExtensions implements CelCompilerLibrary, CelRuntimeLibrary { return builder.buildOrThrow(); } - public enum Function { + enum Function { MAX( CelFunctionDecl.newFunctionDeclaration( MATH_MAX_FUNCTION, @@ -175,50 +206,50 @@ public enum Function { 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( + CelFunctionBinding.from("math_@max_double", Double.class, x -> x), + CelFunctionBinding.from("math_@max_int", Long.class, x -> x), + CelFunctionBinding.from( "math_@max_double_double", Double.class, Double.class, CelMathExtensions::maxPair), - CelRuntime.CelFunctionBinding.from( + CelFunctionBinding.from( "math_@max_int_int", Long.class, Long.class, CelMathExtensions::maxPair), - CelRuntime.CelFunctionBinding.from( + CelFunctionBinding.from( "math_@max_int_double", Long.class, Double.class, CelMathExtensions::maxPair), - CelRuntime.CelFunctionBinding.from( + CelFunctionBinding.from( "math_@max_double_int", Double.class, Long.class, CelMathExtensions::maxPair), - CelRuntime.CelFunctionBinding.from( + 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( + CelFunctionBinding.from("math_@max_uint", Long.class, x -> x), + CelFunctionBinding.from( "math_@max_uint_uint", Long.class, Long.class, CelMathExtensions::maxPair), - CelRuntime.CelFunctionBinding.from( + CelFunctionBinding.from( "math_@max_double_uint", Double.class, Long.class, CelMathExtensions::maxPair), - CelRuntime.CelFunctionBinding.from( + CelFunctionBinding.from( "math_@max_uint_int", Long.class, Long.class, CelMathExtensions::maxPair), - CelRuntime.CelFunctionBinding.from( + CelFunctionBinding.from( "math_@max_uint_double", Long.class, Double.class, CelMathExtensions::maxPair), - CelRuntime.CelFunctionBinding.from( + 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( + CelFunctionBinding.from("math_@max_uint", UnsignedLong.class, x -> x), + CelFunctionBinding.from( "math_@max_uint_uint", UnsignedLong.class, UnsignedLong.class, CelMathExtensions::maxPair), - CelRuntime.CelFunctionBinding.from( + CelFunctionBinding.from( "math_@max_double_uint", Double.class, UnsignedLong.class, CelMathExtensions::maxPair), - CelRuntime.CelFunctionBinding.from( + CelFunctionBinding.from( "math_@max_uint_int", UnsignedLong.class, Long.class, CelMathExtensions::maxPair), - CelRuntime.CelFunctionBinding.from( + CelFunctionBinding.from( "math_@max_uint_double", UnsignedLong.class, Double.class, CelMathExtensions::maxPair), - CelRuntime.CelFunctionBinding.from( + CelFunctionBinding.from( "math_@max_int_uint", Long.class, UnsignedLong.class, CelMathExtensions::maxPair))), MIN( CelFunctionDecl.newFunctionDeclaration( @@ -290,62 +321,348 @@ public enum Function { SimpleType.DYN, ListType.create(SimpleType.DYN))), 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( + CelFunctionBinding.from("math_@min_double", Double.class, x -> x), + CelFunctionBinding.from("math_@min_int", Long.class, x -> x), + CelFunctionBinding.from( "math_@min_double_double", Double.class, Double.class, CelMathExtensions::minPair), - CelRuntime.CelFunctionBinding.from( + CelFunctionBinding.from( "math_@min_int_int", Long.class, Long.class, CelMathExtensions::minPair), - CelRuntime.CelFunctionBinding.from( + CelFunctionBinding.from( "math_@min_int_double", Long.class, Double.class, CelMathExtensions::minPair), - CelRuntime.CelFunctionBinding.from( + 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_@min_list_dyn", List.class, CelMathExtensions::minList)), ImmutableSet.of( - CelRuntime.CelFunctionBinding.from("math_@min_uint", Long.class, x -> x), - CelRuntime.CelFunctionBinding.from( + CelFunctionBinding.from("math_@min_uint", Long.class, x -> x), + CelFunctionBinding.from( "math_@min_uint_uint", Long.class, Long.class, CelMathExtensions::minPair), - CelRuntime.CelFunctionBinding.from( + CelFunctionBinding.from( "math_@min_double_uint", Double.class, Long.class, CelMathExtensions::minPair), - CelRuntime.CelFunctionBinding.from( + CelFunctionBinding.from( "math_@min_uint_int", Long.class, Long.class, CelMathExtensions::minPair), - CelRuntime.CelFunctionBinding.from( + CelFunctionBinding.from( "math_@min_uint_double", Long.class, Double.class, CelMathExtensions::minPair), - CelRuntime.CelFunctionBinding.from( + CelFunctionBinding.from( "math_@min_int_uint", Long.class, Long.class, CelMathExtensions::minPair)), ImmutableSet.of( - CelRuntime.CelFunctionBinding.from("math_@min_uint", UnsignedLong.class, x -> x), - CelRuntime.CelFunctionBinding.from( + CelFunctionBinding.from("math_@min_uint", UnsignedLong.class, x -> x), + CelFunctionBinding.from( "math_@min_uint_uint", UnsignedLong.class, UnsignedLong.class, CelMathExtensions::minPair), - CelRuntime.CelFunctionBinding.from( + CelFunctionBinding.from( "math_@min_double_uint", Double.class, UnsignedLong.class, CelMathExtensions::minPair), - CelRuntime.CelFunctionBinding.from( + CelFunctionBinding.from( "math_@min_uint_int", UnsignedLong.class, Long.class, CelMathExtensions::minPair), - CelRuntime.CelFunctionBinding.from( + 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))); + CelFunctionBinding.from( + "math_@min_int_uint", Long.class, UnsignedLong.class, CelMathExtensions::minPair))), + 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( + 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( + 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( + 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::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, + UnsignedLong.class, + 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; + private final ImmutableSet functionBindings; + private final ImmutableSet functionBindingsULongSigned; + private final ImmutableSet functionBindingsULongUnsigned; + + String getFunction() { + return functionDecl.name(); + } + + Function(CelFunctionDecl functionDecl, ImmutableSet bindings) { + this(functionDecl, bindings, ImmutableSet.of(), ImmutableSet.of()); + } Function( CelFunctionDecl functionDecl, - ImmutableSet functionBindings, - ImmutableSet functionBindingsULongSigned, - ImmutableSet functionBindingsULongUnsigned) { + ImmutableSet functionBindings, + ImmutableSet functionBindingsULongSigned, + ImmutableSet functionBindingsULongUnsigned) { this.functionDecl = functionDecl; this.functionBindings = functionBindings; this.functionBindingsULongSigned = functionBindingsULongSigned; @@ -353,25 +670,106 @@ public enum Function { } } + private static final class Library implements CelExtensionLibrary { + private final CelMathExtensions version0; + private final CelMathExtensions version1; + private final CelMathExtensions version2; + + Library(boolean enableUnsignedLongs) { + version0 = + new CelMathExtensions( + 0, ImmutableSet.of(Function.MIN, Function.MAX), enableUnsignedLongs); + + 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(), + enableUnsignedLongs); + + version2 = + new CelMathExtensions( + 2, + ImmutableSet.builder() + .addAll(version1.functions) + .add(Function.SQRT) + .build(), + enableUnsignedLongs); + } + + @Override + public String name() { + return "math"; + } + + @Override + public ImmutableSet versions() { + return ImmutableSet.of(version0, version1, version2); + } + } + + private static final Library LIBRARY_UNSIGNED_LONGS_ENABLED = new Library(true); + private static final Library LIBRARY_UNSIGNED_LONGS_DISABLED = new Library(false); + + static CelExtensionLibrary library(CelOptions celOptions) { + return celOptions.enableUnsignedLongs() + ? LIBRARY_UNSIGNED_LONGS_ENABLED + : LIBRARY_UNSIGNED_LONGS_DISABLED; + } + private final boolean enableUnsignedLongs; private final ImmutableSet functions; + private final int version; - CelMathExtensions(CelOptions celOptions) { - this(celOptions, ImmutableSet.copyOf(Function.values())); + CelMathExtensions(CelOptions celOptions, Set functions) { + this(-1, functions, celOptions.enableUnsignedLongs()); } - CelMathExtensions(CelOptions celOptions, Set functions) { - this.enableUnsignedLongs = celOptions.enableUnsignedLongs(); + private CelMathExtensions(int version, Set functions, boolean enableUnsignedLongs) { + this.enableUnsignedLongs = enableUnsignedLongs; + 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)); @@ -455,6 +853,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 ArithmeticException("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_EVEN); + } + + 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 +1073,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 +1095,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/CelOptionalLibrary.java b/extensions/src/main/java/dev/cel/extensions/CelOptionalLibrary.java index 96796cde3..a384b09e2 100644 --- a/extensions/src/main/java/dev/cel/extensions/CelOptionalLibrary.java +++ b/extensions/src/main/java/dev/cel/extensions/CelOptionalLibrary.java @@ -16,47 +16,257 @@ 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.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.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.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.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(); - - private static final String VALUE_FUNCTION = "value"; - private static final String HAS_VALUE_FUNCTION = "hasValue"; - private static final String OPTIONAL_NONE_FUNCTION = "optional.none"; - private static final String OPTIONAL_OF_FUNCTION = "optional.of"; - private static final String OPTIONAL_OF_NON_ZERO_VALUE_FUNCTION = "optional.ofNonZeroValue"; +public final class CelOptionalLibrary + implements CelCompilerLibrary, CelInternalRuntimeLibrary, CelExtensionLibrary.FeatureSet { + + /** Enumerations of function names used for supporting optionals. */ + public enum Function { + VALUE("value"), + 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"), + FIRST("first"), + LAST("last"); + + private final String functionName; + + public String getFunction() { + return functionName; + } + + Function(String functionName) { + this.functionName = functionName; + } + } + + 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( + 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)), + CelFunctionDecl.newFunctionDeclaration( + Function.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( + Function.FIRST.functionName, + CelOverloadDecl.newMemberOverload( + "optional_list_first", + "Return the first value in a list if present, otherwise" + + " optional.none()", + optionalTypeV, + listTypeV)), + CelFunctionDecl.newFunctionDeclaration( + Function.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()) { @@ -65,88 +275,27 @@ 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( - OPTIONAL_OF_FUNCTION, - CelOverloadDecl.newGlobalOverload("optional_of", optionalTypeV, paramTypeV)), - CelFunctionDecl.newFunctionDeclaration( - OPTIONAL_OF_NON_ZERO_VALUE_FUNCTION, - CelOverloadDecl.newGlobalOverload( - "optional_ofNonZeroValue", optionalTypeV, paramTypeV)), - CelFunctionDecl.newFunctionDeclaration( - OPTIONAL_NONE_FUNCTION, - CelOverloadDecl.newGlobalOverload("optional_none", optionalTypeV)), - CelFunctionDecl.newFunctionDeclaration( - VALUE_FUNCTION, - CelOverloadDecl.newMemberOverload("optional_value", paramTypeV, optionalTypeV)), - CelFunctionDecl.newFunctionDeclaration( - HAS_VALUE_FUNCTION, - 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( - "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))); + 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( - CelRuntime.CelFunctionBinding.from("optional_of", Object.class, Optional::of), - CelRuntime.CelFunctionBinding.from( + CelFunctionBinding.from("optional_of", Object.class, Optional::of), + CelFunctionBinding.from( "optional_ofNonZeroValue", Object.class, val -> { @@ -155,12 +304,65 @@ public void setRuntimeOptions(CelRuntimeBuilder runtimeBuilder) { } 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())); + CelFunctionBinding.from( + "optional_unwrap_list", Collection.class, CelOptionalLibrary::elideOptionalCollection), + CelFunctionBinding.from("optional_none", ImmutableList.of(), val -> Optional.empty()), + CelFunctionBinding.from("optional_value", Object.class, val -> ((Optional) val).get()), + CelFunctionBinding.from( + "optional_hasValue", Object.class, val -> ((Optional) val).isPresent()), + CelFunctionBinding.from( + "select_optional_field", // This only handles map selection. Proto selection is + // special cased inside the interpreter. + Map.class, + String.class, + runtimeEquality::findInMap), + CelFunctionBinding.from( + "map_optindex_optional_value", Map.class, Object.class, runtimeEquality::findInMap), + CelFunctionBinding.from( + "optional_map_optindex_optional_value", + Optional.class, + Object.class, + (Optional optionalMap, Object key) -> + indexOptionalMap(optionalMap, key, runtimeEquality)), + CelFunctionBinding.from( + "optional_map_index_value", + Optional.class, + Object.class, + (Optional optionalMap, Object key) -> + indexOptionalMap(optionalMap, key, runtimeEquality)), + CelFunctionBinding.from( + "optional_list_index_int", + Optional.class, + Long.class, + CelOptionalLibrary::indexOptionalList), + CelFunctionBinding.from( + "list_optindex_optional_int", + List.class, + Long.class, + (List list, Long index) -> { + int castIndex = Ints.checkedCast(index); + if (castIndex < 0 || castIndex >= list.size()) { + return Optional.empty(); + } + return Optional.of(list.get(castIndex)); + }), + CelFunctionBinding.from( + "optional_list_optindex_optional_int", + Optional.class, + Long.class, + CelOptionalLibrary::indexOptionalList)); + + if (version >= 2) { + runtimeBuilder.addFunctionBindings( + CelFunctionBinding.from( + "optional_list_first", Collection.class, CelOptionalLibrary::listOptionalFirst), + CelFunctionBinding.from( + "optional_list_last", Collection.class, CelOptionalLibrary::listOptionalLast)); + } + } + + 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, @@ -181,8 +383,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) { @@ -218,18 +420,19 @@ private static Optional expandOptMap( return Optional.of( exprFactory.newGlobalCall( Operator.CONDITIONAL.getFunction(), - exprFactory.newReceiverCall(HAS_VALUE_FUNCTION, target), + exprFactory.newReceiverCall(Function.HAS_VALUE.getFunction(), target), exprFactory.newGlobalCall( - OPTIONAL_OF_FUNCTION, + Function.OPTIONAL_OF.getFunction(), exprFactory.fold( UNUSED_ITER_VAR, exprFactory.newList(), varName, - exprFactory.newReceiverCall(VALUE_FUNCTION, target), + exprFactory.newReceiverCall( + Function.VALUE.getFunction(), exprFactory.copy(target)), exprFactory.newBoolLiteral(true), exprFactory.newIdentifier(varName), mapExpr)), - exprFactory.newGlobalCall(OPTIONAL_NONE_FUNCTION))); + exprFactory.newGlobalCall(Function.OPTIONAL_NONE.getFunction()))); } private static Optional expandOptFlatMap( @@ -252,17 +455,53 @@ private static Optional expandOptFlatMap( return Optional.of( exprFactory.newGlobalCall( Operator.CONDITIONAL.getFunction(), - exprFactory.newReceiverCall(HAS_VALUE_FUNCTION, target), + exprFactory.newReceiverCall(Function.HAS_VALUE.getFunction(), target), exprFactory.fold( UNUSED_ITER_VAR, exprFactory.newList(), varName, - exprFactory.newReceiverCall(VALUE_FUNCTION, target), + exprFactory.newReceiverCall(Function.VALUE.getFunction(), exprFactory.copy(target)), exprFactory.newBoolLiteral(true), exprFactory.newIdentifier(varName), mapExpr), - exprFactory.newGlobalCall(OPTIONAL_NONE_FUNCTION))); + exprFactory.newGlobalCall(Function.OPTIONAL_NONE.getFunction()))); } - private CelOptionalLibrary() {} + 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 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..f92265782 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,47 @@ /** Internal implementation of CEL proto extensions. */ @Immutable -final class CelProtoExtensions implements CelCompilerLibrary { +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); 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..368301ee8 --- /dev/null +++ b/extensions/src/main/java/dev/cel/extensions/CelRegexExtensions.java @@ -0,0 +1,309 @@ +// 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 +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 = 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..35739ddc9 --- /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 +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..37b2a368b 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; @@ -32,7 +33,8 @@ 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; @@ -41,7 +43,8 @@ /** 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 +58,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 +77,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 +97,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 +117,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 +136,7 @@ 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)), REPLACE( CelFunctionDecl.newFunctionDeclaration( "replace", @@ -153,11 +156,11 @@ 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)), @@ -175,9 +178,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 +200,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 +216,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,12 +227,16 @@ 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); } @@ -245,6 +252,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 +296,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 +307,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 +331,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 +349,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 +391,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 +416,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()) { @@ -412,9 +458,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); @@ -469,9 +516,10 @@ private static List split(Object[] args) throws CelEvaluationException { 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); @@ -532,18 +580,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 +615,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 +631,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..a5b65168c 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 @@ -304,8 +559,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 +593,452 @@ 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 +``` +## TwoVarComprehensions + +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 \ No newline at end of file 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..a42fba189 --- /dev/null +++ b/extensions/src/main/java/dev/cel/extensions/SetsExtensionsRuntimeImpl.java @@ -0,0 +1,141 @@ +// 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.add( + CelFunctionBinding.from( + "list_sets_contains_list", + Collection.class, + Collection.class, + this::containsAll)); + break; + case EQUIVALENT: + bindingBuilder.add( + CelFunctionBinding.from( + "list_sets_equivalent_list", + Collection.class, + Collection.class, + (listA, listB) -> containsAll(listA, listB) && containsAll(listB, listA))); + break; + case INTERSECTS: + bindingBuilder.add( + 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 b5b40d072..45d48aeca 100644 --- a/extensions/src/test/java/dev/cel/extensions/BUILD.bazel +++ b/extensions/src/test/java/dev/cel/extensions/BUILD.bazel @@ -1,3 +1,4 @@ +load("@rules_java//java:defs.bzl", "java_library") load("//:testing.bzl", "junit4_test_suites") package(default_applicable_licenses = ["//:license"]) @@ -9,25 +10,37 @@ java_library( deps = [ "//:java_truth", "//bundle:cel", - "//common", + "//common:cel_ast", "//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/internal:proto_time_utils", "//common/types", "//common/types:type_providers", + "//common/values", + "//common/values:cel_byte_string", "//compiler", "//compiler:compiler_builder", "//extensions", + "//extensions:extension_library", + "//extensions:lite_extensions", "//extensions:math", "//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", + "@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 43e64bb8b..33dd9db39 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelBindingsExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelBindingsExtensionsTest.java @@ -17,20 +17,28 @@ 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.common.CelAbstractSyntaxTree; import dev.cel.common.CelFunctionDecl; +import dev.cel.common.CelOptions; import dev.cel.common.CelOverloadDecl; import dev.cel.common.CelValidationException; import dev.cel.common.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.CelFunctionBinding; import dev.cel.runtime.CelRuntime; -import dev.cel.runtime.CelRuntime.CelFunctionBinding; import dev.cel.runtime.CelRuntimeFactory; import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.Test; import org.junit.runner.RunWith; @@ -39,31 +47,55 @@ public final class CelBindingsExtensionsTest { private static final CelCompiler COMPILER = CelCompilerFactory.standardCelCompilerBuilder() - .addMacros(CelMacro.STANDARD_MACROS) - .addLibraries(CelExtensions.bindings()) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addLibraries(CelOptionalLibrary.INSTANCE, CelExtensions.bindings()) .build(); - private static final CelRuntime RUNTIME = CelRuntimeFactory.standardCelRuntimeBuilder().build(); + private static final CelRuntime RUNTIME = + CelRuntimeFactory.standardCelRuntimeBuilder() + .addLibraries(CelOptionalLibrary.INSTANCE) + .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()).isEmpty(); + assertThat(library.version(0).macros().stream().map(CelMacro::getFunction)) + .containsExactly("bind"); + } + + private enum BindingTestCase { + BOOL_LITERAL("cel.bind(t, true, t)"), + STRING_CONCAT("cel.bind(msg, \"hello\", msg + msg + msg) == \"hellohellohello\""), + NESTED_BINDS("cel.bind(t1, true, cel.bind(t2, true, t1 && t2))"), + NESTED_BINDS_SPECIFIER_ONLY( + "cel.bind(x, cel.bind(x, \"a\", x + x), x + \":\" + x) == \"aa:aa\""), + NESTED_BINDS_SPECIFIER_AND_VALUE( + "cel.bind(x, cel.bind(x, \"a\", x + x), cel.bind(y, x + x, y + \":\" + y)) ==" + + " \"aaaa:aaaa\""), + 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_OPTIONAL_LIST("cel.bind(r0, optional.none(), [?r0, ?r0]) == []"); + + private final String source; + + BindingTestCase(String source) { + this.source = source; + } + } @Test - @TestParameters("{expr: 'cel.bind(t, true, t)', expectedResult: true}") - @TestParameters( - "{expr: 'cel.bind(msg, \"hello\", msg + msg + msg) == \"hellohellohello\"'," - + " expectedResult: true}") - @TestParameters( - "{expr: 'cel.bind(t1, true, cel.bind(t2, true, t1 && t2))', expectedResult: true}") - @TestParameters( - "{expr: 'cel.bind(valid_elems, [1, 2, 3], [3, 4, 5]" - + ".exists(e, e in valid_elems))', expectedResult: true}") - @TestParameters( - "{expr: 'cel.bind(valid_elems, [1, 2, 3], ![4, 5].exists(e, e in valid_elems))'," - + " expectedResult: true}") - public void binding_success(String expr, boolean expectedResult) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile(expr).getAst(); + public void binding_success(@TestParameter BindingTestCase testCase) throws Exception { + CelAbstractSyntaxTree ast = COMPILER.compile(testCase.source).getAst(); CelRuntime.Program program = RUNTIME.createProgram(ast); - Object evaluatedResult = program.eval(); + boolean evaluatedResult = (boolean) program.eval(); - assertThat(evaluatedResult).isEqualTo(expectedResult); + assertThat(evaluatedResult).isTrue(); } @Test @@ -105,4 +137,110 @@ public void binding_throwsCompilationException(String expr) throws Exception { assertThat(e).hasMessageThat().contains("cel.bind() variable name must be a simple identifier"); } + + @Test + @SuppressWarnings("Immutable") // Test only + public void lazyBinding_bindingVarNeverReferenced() throws Exception { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .setStandardMacros(CelStandardMacro.HAS) + .addMessageTypes(TestAllTypes.getDescriptor()) + .addVar("msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())) + .addLibraries(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; + })) + .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())); + + assertThat(result).isFalse(); + assertThat(invocation.get()).isEqualTo(0); + } + + @Test + @SuppressWarnings("Immutable") // Test only + public void lazyBinding_accuInitEvaluatedOnce() throws Exception { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addLibraries(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; + })) + .build(); + CelAbstractSyntaxTree ast = + celCompiler.compile("cel.bind(t, get_true(), t && t && t && t)").getAst(); + + boolean result = (boolean) celRuntime.createProgram(ast).eval(); + + assertThat(result).isTrue(); + assertThat(invocation.get()).isEqualTo(1); + } + + @Test + @SuppressWarnings("Immutable") // Test only + public void lazyBinding_withNestedBinds() throws Exception { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addLibraries(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; + })) + .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(); + + assertThat(result).isTrue(); + assertThat(invocation.get()).isEqualTo(2); + } } 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..5318234b9 --- /dev/null +++ b/extensions/src/test/java/dev/cel/extensions/CelComprehensionsExtensionsTest.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.extensions; + +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 com.google.testing.junit.testparameterinjector.TestParameters; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelFunctionDecl; +import dev.cel.common.CelOptions; +import dev.cel.common.CelValidationException; +import dev.cel.common.types.SimpleType; +import dev.cel.common.types.TypeParamType; +import dev.cel.compiler.CelCompiler; +import dev.cel.compiler.CelCompilerFactory; +import dev.cel.parser.CelMacro; +import dev.cel.parser.CelStandardMacro; +import dev.cel.parser.CelUnparser; +import dev.cel.parser.CelUnparserFactory; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.CelRuntime; +import dev.cel.runtime.CelRuntimeFactory; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Test for {@link CelExtensions#comprehensions()} */ +@RunWith(TestParameterInjector.class) +public class CelComprehensionsExtensionsTest { + + private static final CelOptions CEL_OPTIONS = + CelOptions.current() + // Enable macro call population for unparsing + .populateMacroCalls(true) + .build(); + + private static final CelCompiler CEL_COMPILER = + CelCompilerFactory.standardCelCompilerBuilder() + .setOptions(CEL_OPTIONS) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addLibraries(CelExtensions.comprehensions()) + .addLibraries(CelExtensions.lists()) + .addLibraries(CelExtensions.strings()) + .addLibraries(CelOptionalLibrary.INSTANCE, CelExtensions.bindings()) + .build(); + private static final CelRuntime CEL_RUNTIME = + CelRuntimeFactory.standardCelRuntimeBuilder() + .addLibraries(CelOptionalLibrary.INSTANCE) + .addLibraries(CelExtensions.lists()) + .addLibraries(CelExtensions.strings()) + .addLibraries(CelExtensions.comprehensions()) + .build(); + + 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 { + CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); + + Object result = CEL_RUNTIME.createProgram(ast).eval(); + + assertThat(result).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 { + CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); + + Object result = CEL_RUNTIME.createProgram(ast).eval(); + + assertThat(result).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 { + CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); + + Object result = CEL_RUNTIME.createProgram(ast).eval(); + + assertThat(result).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 { + CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); + + Object result = CEL_RUNTIME.createProgram(ast).eval(); + + assertThat(result).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 { + CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); + + Object result = CEL_RUNTIME.createProgram(ast).eval(); + + assertThat(result).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 + '_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 { + CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); + + Object result = CEL_RUNTIME.createProgram(ast).eval(); + + assertThat(result).isEqualTo(true); + } + + @Test + public void comprehension_onTypeParam_success() throws Exception { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .setOptions(CEL_OPTIONS) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addLibraries(CelExtensions.comprehensions()) + .addVar("items", TypeParamType.create("T")) + .build(); + + CelAbstractSyntaxTree ast = celCompiler.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 = CEL_COMPILER.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 { + CelValidationException e = + assertThrows(CelValidationException.class, () -> CEL_COMPILER.compile(expr).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 { + CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); + + CelEvaluationException e = + assertThrows(CelEvaluationException.class, () -> CEL_RUNTIME.createProgram(ast).eval()); + + assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class); + assertThat(e).hasCauseThat().hasMessageThat().contains(err); + } + + @Test + public void twoVarComprehension_arithematicException_runtimeError() throws Exception { + CelAbstractSyntaxTree ast = CEL_COMPILER.compile("[0].all(i, k, i/k < k)").getAst(); + + CelEvaluationException e = + assertThrows(CelEvaluationException.class, () -> CEL_RUNTIME.createProgram(ast).eval()); + + assertThat(e).hasCauseThat().isInstanceOf(ArithmeticException.class); + assertThat(e).hasCauseThat().hasMessageThat().contains("/ by zero"); + } + + @Test + public void twoVarComprehension_outOfBounds_runtimeError() throws Exception { + CelAbstractSyntaxTree ast = CEL_COMPILER.compile("[1, 2].exists(i, v, [0][v] > 0)").getAst(); + + CelEvaluationException e = + assertThrows(CelEvaluationException.class, () -> CEL_RUNTIME.createProgram(ast).eval()); + + assertThat(e).hasCauseThat().isInstanceOf(IndexOutOfBoundsException.class); + assertThat(e).hasCauseThat().hasMessageThat().contains("Index out of bounds: 1"); + } +} diff --git a/extensions/src/test/java/dev/cel/extensions/CelEncoderExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelEncoderExtensionsTest.java index deee407aa..ece060011 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelEncoderExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelEncoderExtensionsTest.java @@ -19,11 +19,13 @@ 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.common.CelFunctionDecl; +import dev.cel.common.CelOptions; import dev.cel.common.CelValidationException; import dev.cel.common.types.SimpleType; +import dev.cel.common.values.CelByteString; import dev.cel.compiler.CelCompiler; import dev.cel.compiler.CelCompilerFactory; import dev.cel.runtime.CelEvaluationException; @@ -34,14 +36,30 @@ @RunWith(TestParameterInjector.class) public class CelEncoderExtensionsTest { + private static final CelOptions CEL_OPTIONS = + CelOptions.current().evaluateCanonicalTypesToNativeValues(true).build(); private static final CelCompiler CEL_COMPILER = CelCompilerFactory.standardCelCompilerBuilder() .addVar("stringVar", SimpleType.STRING) - .addLibraries(CelExtensions.encoders()) + .addLibraries(CelExtensions.encoders(CEL_OPTIONS)) .build(); private static final CelRuntime CEL_RUNTIME = - CelRuntimeFactory.standardCelRuntimeBuilder().addLibraries(CelExtensions.encoders()).build(); + CelRuntimeFactory.standardCelRuntimeBuilder() + .setOptions(CEL_OPTIONS) + .addLibraries(CelExtensions.encoders(CEL_OPTIONS)) + .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 { @@ -56,27 +74,27 @@ public void encode_success() throws Exception { @Test public void decode_success() throws Exception { - ByteString decodedBytes = - (ByteString) + CelByteString decodedBytes = + (CelByteString) CEL_RUNTIME .createProgram(CEL_COMPILER.compile("base64.decode('aGVsbG8=')").getAst()) .eval(); 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) + CelByteString decodedBytes = + (CelByteString) CEL_RUNTIME // RFC2045 6.8, padding can be ignored. .createProgram(CEL_COMPILER.compile("base64.decode('aGVsbG8')").getAst()) .eval(); assertThat(decodedBytes.size()).isEqualTo(5); - assertThat(decodedBytes.toString(ISO_8859_1)).isEqualTo("hello"); + assertThat(new String(decodedBytes.toByteArray(), ISO_8859_1)).isEqualTo("hello"); } @Test @@ -86,13 +104,13 @@ public void roundTrip_success() throws Exception { CEL_RUNTIME .createProgram(CEL_COMPILER.compile("base64.encode(b'Hello World!')").getAst()) .eval(); - ByteString decodedBytes = - (ByteString) + CelByteString decodedBytes = + (CelByteString) CEL_RUNTIME .createProgram(CEL_COMPILER.compile("base64.decode(stringVar)").getAst()) .eval(ImmutableMap.of("stringVar", encodedString)); - assertThat(decodedBytes.toString(ISO_8859_1)).isEqualTo("Hello World!"); + assertThat(new String(decodedBytes.toByteArray(), ISO_8859_1)).isEqualTo("Hello World!"); } @Test @@ -128,4 +146,3 @@ public void decode_malformedBase64Char_throwsEvaluationException() throws Except assertThat(e).hasCauseThat().hasMessageThat().contains("Illegal base64 character"); } } - diff --git a/extensions/src/test/java/dev/cel/extensions/CelExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelExtensionsTest.java index 0b2e7b2cd..db126f6ee 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,53 @@ 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", + "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"); + } +} 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..c4739b18b --- /dev/null +++ b/extensions/src/test/java/dev/cel/extensions/CelListsExtensionsTest.java @@ -0,0 +1,343 @@ +// 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.bundle.CelFactory; +import dev.cel.common.CelContainer; +import dev.cel.common.CelOptions; +import dev.cel.common.CelValidationException; +import dev.cel.common.types.SimpleType; +import dev.cel.expr.conformance.test.SimpleTest; +import dev.cel.parser.CelStandardMacro; +import dev.cel.runtime.CelEvaluationException; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public class CelListsExtensionsTest { + private static final Cel CEL = + CelFactory.standardCelBuilder() + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addCompilerLibraries(CelExtensions.lists()) + .addRuntimeLibraries(CelExtensions.lists()) + .setContainer(CelContainer.ofName("cel.expr.conformance.test")) + .addMessageTypes(SimpleTest.getDescriptor()) + .addVar("non_list", SimpleType.DYN) + .build(); + + private static final Cel CEL_WITH_HETEROGENEOUS_NUMERIC_COMPARISONS = + CelFactory.standardCelBuilder() + .setOptions(CelOptions.current().enableHeterogeneousNumericComparisons(true).build()) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addCompilerLibraries(CelExtensions.lists()) + .addRuntimeLibraries(CelExtensions.lists()) + .setContainer(CelContainer.ofName("cel.expr.conformance.test")) + .addMessageTypes(SimpleTest.getDescriptor()) + .build(); + + @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 = + CEL.createProgram(CEL.compile(expression).getAst()) + .eval(ImmutableMap.of("non_list", ImmutableSortedSet.of(4L, 1L, 3L, 2L))); + + assertThat(result).isEqualTo(expectedResult(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, + () -> CEL.createProgram(CEL.compile(expression).getAst()).eval())) + .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) CEL.createProgram(CEL.compile(expression).getAst()).eval(); + + 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) CEL.createProgram(CEL.compile(expression).getAst()).eval(); + + assertThat(result).isTrue(); + } + + @Test + public void flatten_negativeDepth_throws() { + CelEvaluationException e = + assertThrows( + CelEvaluationException.class, + () -> CEL.createProgram(CEL.compile("[1,2,3,4].flatten(-1)").getAst()).eval()); + + assertThat(e) + .hasMessageThat() + .contains("evaluation error at :17: Function 'list_flatten_list_int' failed"); + 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) { + // 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) CEL.createProgram(CEL.compile(expression).getAst()).eval(); + + 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 = + CEL.createProgram(CEL.compile(expression).getAst()) + .eval( + ImmutableMap.of( + "non_list", ImmutableSortedMultiset.of(1L, 2L, 3L, 4L, 4L, 1L, 3L, 2L))); + + assertThat(result).isEqualTo(expectedResult(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 = + CEL.createProgram(CEL.compile(expression).getAst()) + .eval(ImmutableMap.of("non_list", ImmutableSortedSet.of(4L, 1L, 3L, 2L))); + + assertThat(result).isEqualTo(expectedResult(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 = CEL.createProgram(CEL.compile(expression).getAst()).eval(); + + assertThat(result).isEqualTo(expectedResult(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 = + CEL_WITH_HETEROGENEOUS_NUMERIC_COMPARISONS + .createProgram(CEL_WITH_HETEROGENEOUS_NUMERIC_COMPARISONS.compile(expression).getAst()) + .eval(); + + assertThat(result).isEqualTo(expectedResult(expected)); + } + + @Test + @TestParameters( + "{expression: '[\"d\", 3, 2, \"c\"].sort()', " + + "expectedError: 'List elements must have the same type'}") + @TestParameters( + "{expression: '[3.0, 2, 1u].sort()', " + + "expectedError: 'List elements must have the same type'}") + @TestParameters( + "{expression: '[SimpleTest{name: \"a\"}, SimpleTest{name: \"b\"}].sort()', " + + "expectedError: 'List elements must be comparable'}") + public void sort_throws(String expression, String expectedError) throws Exception { + assertThat( + assertThrows( + CelEvaluationException.class, + () -> CEL.createProgram(CEL.compile(expression).getAst()).eval())) + .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 = CEL.createProgram(CEL.compile(expression).getAst()).eval(); + + assertThat(result).isEqualTo(expectedResult(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 { + assertThat( + assertThrows( + CelValidationException.class, + () -> CEL.createProgram(CEL.compile(expression).getAst()).eval())) + .hasMessageThat() + .contains(expectedError); + } + + @Test + @TestParameters( + "{expression: '[[1, 2], [\"a\", \"b\"]].sortBy(e, e[0])', " + + "expectedError: 'List elements must have the same type'}") + @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, + () -> CEL.createProgram(CEL.compile(expression).getAst()).eval())) + .hasCauseThat() + .hasMessageThat() + .contains(expectedError); + } + + private static Object expectedResult(String expression) + throws CelEvaluationException, CelValidationException { + return CEL.createProgram(CEL.compile(expression).getAst()).eval(); + } +} 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..bcdfb0a21 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelMathExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelMathExtensionsTest.java @@ -32,8 +32,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 org.junit.Test; import org.junit.runner.RunWith; @@ -44,12 +44,25 @@ public class CelMathExtensionsTest { CelOptions.current().enableUnsignedLongs(false).build(); private static final CelCompiler CEL_COMPILER = CelCompilerFactory.standardCelCompilerBuilder() + .setOptions(CEL_OPTIONS) .addLibraries(CelExtensions.math(CEL_OPTIONS)) .build(); private static final CelRuntime CEL_RUNTIME = CelRuntimeFactory.standardCelRuntimeBuilder() + .setOptions(CEL_OPTIONS) .addLibraries(CelExtensions.math(CEL_OPTIONS)) .build(); + private static final CelOptions CEL_UNSIGNED_OPTIONS = CelOptions.current().build(); + private static final CelCompiler CEL_UNSIGNED_COMPILER = + CelCompilerFactory.standardCelCompilerBuilder() + .setOptions(CEL_UNSIGNED_OPTIONS) + .addLibraries(CelExtensions.math(CEL_UNSIGNED_OPTIONS)) + .build(); + private static final CelRuntime CEL_UNSIGNED_RUNTIME = + CelRuntimeFactory.standardCelRuntimeBuilder() + .setOptions(CEL_UNSIGNED_OPTIONS) + .addLibraries(CelExtensions.math(CEL_UNSIGNED_OPTIONS)) + .build(); @Test @TestParameters("{expr: 'math.greatest(-5)', expectedResult: -5}") @@ -536,7 +549,7 @@ 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) @@ -653,4 +666,518 @@ public void least_nonProtoNamespace_success(String expr) throws Exception { 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_COMPILER.compile(expr).getAst(); + + Object result = CEL_RUNTIME.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_COMPILER.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_COMPILER.compile(expr).getAst(); + + Object result = CEL_RUNTIME.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_COMPILER.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_COMPILER.compile(expr).getAst(); + + Object result = CEL_RUNTIME.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_COMPILER.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_COMPILER.compile(expr).getAst(); + + Object result = CEL_RUNTIME.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_COMPILER.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_COMPILER.compile(expr).getAst(); + + Object result = CEL_RUNTIME.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_COMPILER.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}") + @TestParameters("{expr: 'math.round(0.0/0.0)' , expectedResult: NaN}") + @TestParameters("{expr: 'math.round(1.0/0.0)' , expectedResult: Infinity}") + @TestParameters("{expr: 'math.round(-1.0/0.0)' , expectedResult: -Infinity}") + public void round_success(String expr, double expectedResult) throws Exception { + CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); + + Object result = CEL_RUNTIME.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_COMPILER.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_COMPILER.compile(expr).getAst(); + + Object result = CEL_RUNTIME.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_COMPILER.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_COMPILER.compile(expr).getAst(); + + Object result = CEL_RUNTIME.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_COMPILER.compile(expr).getAst(); + + Object result = CEL_RUNTIME.createProgram(ast).eval(); + + assertThat(result).isEqualTo(expectedResult); + } + + @Test + public void abs_overflow_throwsException() { + CelValidationException e = + assertThrows( + CelValidationException.class, + () -> CEL_COMPILER.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_COMPILER.compile(expr).getAst(); + + Object result = CEL_RUNTIME.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_COMPILER.compile(expr).getAst(); + + Object result = CEL_RUNTIME.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_COMPILER.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_COMPILER.compile(expr).getAst(); + + Object result = CEL_RUNTIME.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_UNSIGNED_COMPILER.compile(expr).getAst(); + + Object result = CEL_UNSIGNED_RUNTIME.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_COMPILER.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_COMPILER + .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_COMPILER.compile(expr).getAst(); + + Object result = CEL_RUNTIME.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_UNSIGNED_COMPILER.compile(expr).getAst(); + + Object result = CEL_UNSIGNED_RUNTIME.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_COMPILER.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_COMPILER.compile(expr).getAst(); + + Object result = CEL_RUNTIME.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_UNSIGNED_COMPILER.compile(expr).getAst(); + + Object result = CEL_UNSIGNED_RUNTIME.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_COMPILER.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_COMPILER.compile(expr).getAst(); + + Object result = CEL_RUNTIME.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_UNSIGNED_COMPILER.compile(expr).getAst(); + + Object result = CEL_UNSIGNED_RUNTIME.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_COMPILER.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_COMPILER.compile(expr).getAst(); + + Object result = CEL_RUNTIME.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_UNSIGNED_COMPILER.compile(expr).getAst(); + + Object result = CEL_UNSIGNED_RUNTIME.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_COMPILER.compile(expr).getAst(); + + CelEvaluationException e = + assertThrows( + CelEvaluationException.class, () -> CEL_UNSIGNED_RUNTIME.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_COMPILER.compile(expr).getAst(); + + Object result = CEL_RUNTIME.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 { + CelAbstractSyntaxTree ast = CEL_UNSIGNED_COMPILER.compile(expr).getAst(); + + Object result = CEL_UNSIGNED_RUNTIME.createProgram(ast).eval(); + + 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 { + CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); + + CelEvaluationException e = + assertThrows( + CelEvaluationException.class, () -> CEL_UNSIGNED_RUNTIME.createProgram(ast).eval()); + + 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 { + CelAbstractSyntaxTree ast = CEL_UNSIGNED_COMPILER.compile(expr).getAst(); + + Object result = CEL_UNSIGNED_RUNTIME.createProgram(ast).eval(); + + assertThat(result).isEqualTo(expectedResult); + } } diff --git a/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java b/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java index 1b367dbda..a806037da 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java @@ -15,17 +15,12 @@ package dev.cel.extensions; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth8.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.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; @@ -33,22 +28,30 @@ 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.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.StructTypeReference; +import dev.cel.common.types.TypeType; +import dev.cel.common.values.CelByteString; +import dev.cel.common.values.NullValue; +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.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 java.util.List; import java.util.Map; import java.util.Optional; @@ -56,7 +59,7 @@ import org.junit.runner.RunWith; @RunWith(TestParameterInjector.class) -@SuppressWarnings("unchecked") +@SuppressWarnings({"unchecked", "SingleTestParameter"}) public class CelOptionalLibraryTest { @SuppressWarnings("ImmutableEnumChecker") // Test only @@ -66,13 +69,17 @@ 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, + ProtoTimeUtils.fromSecondsToDuration(180)), TIMESTAMP( "timestamp(1685552643)", "timestamp(0)", SimpleType.TIMESTAMP, - Timestamps.fromSeconds(1685552643)); + ProtoTimeUtils.fromSecondsToTimestamp(1685552643)); private final String sourceWithNonZeroValue; private final String sourceWithZeroValue; @@ -89,14 +96,88 @@ private enum ConstantTestCases { } private static CelBuilder newCelBuilder() { + return newCelBuilder(Integer.MAX_VALUE); + } + + private static CelBuilder newCelBuilder(int version) { return CelFactory.standardCelBuilder() .setOptions( - CelOptions.current().enableUnsignedLongs(true).enableTimestampEpoch(true).build()) + CelOptions.current() + .evaluateCanonicalTypesToNativeValues(true) + .enableTimestampEpoch(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 @@ -540,6 +621,31 @@ public void optionalFieldSelection_onProtoMessage_returnsOptionalValue() throws assertThat(result).hasValue(5L); } + @Test + public void optionalFieldSelection_onProtoMessage_listValue() throws Exception { + Cel cel = newCelBuilder().build(); + CelAbstractSyntaxTree ast = + cel.compile("optional.of(TestAllTypes{repeated_string: ['foo']}).?repeated_string.value()") + .getAst(); + + 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 = + cel.compile( + "optional.of(TestAllTypes{repeated_string: ['foo']}).?repeated_string[0].value()") + .getAst(); + + String result = (String) cel.createProgram(ast).eval(); + + assertThat(result).isEqualTo("foo"); + } + @Test public void optionalFieldSelection_onProtoMessage_chainedSuccess() throws Exception { Cel cel = @@ -767,6 +873,38 @@ 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 = cel.compile(source).getAst(); + + Object result = cel.createProgram(ast).eval(); + + 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 = cel.compile("{?'key': optional.of('test')}.?key").getAst(); + + Object result = cel.createProgram(ast).eval(); + + assertThat(result).isEqualTo(Optional.of("test")); + } + @Test public void optionalIndex_onList_returnsOptionalEmpty() throws Exception { Cel cel = @@ -825,6 +963,20 @@ 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 = cel.compile("[?x]").getAst(); + + Object result = cel.createProgram(ast).eval(); + + assertThat(InterpreterUtil.isUnknown(result)).isTrue(); + } + @Test public void traditionalIndex_onOptionalList_returnsOptionalEmpty() throws Exception { Cel cel = @@ -839,6 +991,45 @@ public void traditionalIndex_onOptionalList_returnsOptionalEmpty() throws Except assertThat(result).isEqualTo(Optional.empty()); } + @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 = cel.compile(expression).getAst(); + + Object result = cel.createProgram(ast).eval(); + + 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 = cel.compile(expression).getAst(); + + assertThrows(CelEvaluationException.class, () -> cel.createProgram(ast).eval()); + } + @Test public void traditionalIndex_onOptionalList_returnsOptionalValue() throws Exception { Cel cel = @@ -1363,4 +1554,60 @@ public void optionalFlatMapMacro_withNonIdent_throws() { .hasMessageThat() .contains("optFlatMap() variable name must be a simple identifier"); } + + @Test + public void optionalType_typeResolution() throws Exception { + Cel cel = newCelBuilder().build(); + CelAbstractSyntaxTree ast = cel.compile("optional_type").getAst(); + + 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 = cel.compile("type(optional.none()) == optional_type").getAst(); + + 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(cel.compile(expression).getAst()).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(cel.compile(expression).getAst()).eval(); + assertThat(result).isTrue(); + } + + @Test + @TestParameters("{expression: '[1].first()', expectedError: 'undeclared reference to ''first'''}") + @TestParameters("{expression: '[2].last()', expectedError: 'undeclared reference to ''last'''}") + public void listFirstAndLast_throws_earlyVersion(String expression, String expectedError) + throws Exception { + // 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(cel.compile(expression).getAst()).eval(); + })) + .hasMessageThat() + .contains(expectedError); + } } diff --git a/extensions/src/test/java/dev/cel/extensions/CelProtoExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelProtoExtensionsTest.java index d7b66cb1d..f9ec584aa 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelProtoExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelProtoExtensionsTest.java @@ -28,23 +28,24 @@ 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.CelFunctionBinding; 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 org.junit.Test; import org.junit.runner.RunWith; @@ -55,47 +56,55 @@ public final class CelProtoExtensionsTest { 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") + .addFileTypes(TestAllTypesExtensions.getDescriptor()) + .addVar("msg", StructTypeReference.create("cel.expr.conformance.proto2.TestAllTypes")) + .setContainer(CelContainer.ofName("cel.expr.conformance.proto2")) .build(); private static final CelRuntime CEL_RUNTIME = CelRuntimeFactory.standardCelRuntimeBuilder() - .addFileTypes(MessagesProto2Extensions.getDescriptor()) + .addFileTypes(TestAllTypesExtensions.getDescriptor()) .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 = @@ -108,16 +117,16 @@ 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 = @@ -128,11 +137,11 @@ public void hasExt_messageScoped_success(String expr) throws Exception { } @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"); + StructTypeReference.create("cel.expr.conformance.proto2.TestAllTypes"); CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder() .addLibraries(CelExtensions.protos()) @@ -151,9 +160,9 @@ public void hasExt_nonProtoNamespace_success(String expr) throws Exception { .addFunctionBindings( CelFunctionBinding.from( "msg_hasExt", - ImmutableList.of(Proto2Message.class, String.class, Long.class), + ImmutableList.of(TestAllTypes.class, String.class, Long.class), (arg) -> { - Proto2Message msg = (Proto2Message) arg[0]; + TestAllTypes msg = (TestAllTypes) arg[0]; String extensionField = (String) arg[1]; return msg.getAllFields().keySet().stream() .anyMatch(fd -> fd.getFullName().equals(extensionField)); @@ -175,25 +184,25 @@ public void hasExt_undefinedField_throwsException() { CelValidationException.class, () -> CEL_COMPILER - .compile("!proto.hasExt(msg, dev.cel.testing.testdata.proto2.undefined_field)") + .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 = @@ -206,11 +215,11 @@ 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 = @@ -227,21 +236,20 @@ public void getExt_undefinedField_throwsException() { CelValidationException.class, () -> CEL_COMPILER - .compile("!proto.getExt(msg, dev.cel.testing.testdata.proto2.undefined_field)") + .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"); + StructTypeReference.create("cel.expr.conformance.proto2.TestAllTypes"); CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder() .addLibraries(CelExtensions.protos()) @@ -260,9 +268,9 @@ public void getExt_nonProtoNamespace_success(String expr) throws Exception { .addFunctionBindings( CelFunctionBinding.from( "msg_getExt", - ImmutableList.of(Proto2Message.class, String.class, Long.class), + ImmutableList.of(TestAllTypes.class, String.class, Long.class), (arg) -> { - Proto2Message msg = (Proto2Message) arg[0]; + TestAllTypes msg = (TestAllTypes) arg[0]; String extensionField = (String) arg[1]; FieldDescriptor extensionDescriptor = msg.getAllFields().keySet().stream() @@ -284,20 +292,19 @@ public void getExt_nonProtoNamespace_success(String expr) throws Exception { @Test public void getExt_onAnyPackedExtensionField_success() throws Exception { ExtensionRegistry extensionRegistry = ExtensionRegistry.newInstance(); - MessagesProto2Extensions.registerAllExtensions(extensionRegistry); + TestAllTypesExtensions.registerAllExtensions(extensionRegistry); Cel cel = CelFactory.standardCelBuilder() .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(); + cel.compile("proto.getExt(msg, cel.expr.conformance.proto2.int32_ext)").getAst(); Any anyMsg = Any.pack( - Proto2Message.newBuilder().setExtension(MessagesProto2Extensions.int32Ext, 1).build()); + TestAllTypes.newBuilder().setExtension(TestAllTypesExtensions.int32Ext, 1).build()); Long result = (Long) cel.createProgram(ast).eval(ImmutableMap.of("msg", anyMsg)); @@ -317,10 +324,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; 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..8a1bef014 --- /dev/null +++ b/extensions/src/test/java/dev/cel/extensions/CelRegexExtensionsTest.java @@ -0,0 +1,295 @@ +// 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.common.CelAbstractSyntaxTree; +import dev.cel.common.CelFunctionDecl; +import dev.cel.common.CelOptions; +import dev.cel.compiler.CelCompiler; +import dev.cel.compiler.CelCompilerFactory; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.CelRuntime; +import dev.cel.runtime.CelRuntimeFactory; +import java.util.Optional; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public final class CelRegexExtensionsTest { + + private static final CelCompiler COMPILER = + CelCompilerFactory.standardCelCompilerBuilder().addLibraries(CelExtensions.regex()).build(); + private static final CelRuntime RUNTIME = + CelRuntimeFactory.standardCelRuntimeBuilder().addLibraries(CelExtensions.regex()).build(); + + @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); + CelRuntime.Program program = RUNTIME.createProgram(COMPILER.compile(expr).getAst()); + + Object result = program.eval(); + + assertThat(result).isEqualTo(res); + } + + @Test + public void replace_nested_success() throws Exception { + String expr = + "regex.replace(" + + " regex.replace('%(foo) %(bar) %2','%\\\\((\\\\w+)\\\\)','${\\\\1}')," + + " '%(\\\\d+)', '$\\\\1')"; + CelRuntime.Program program = RUNTIME.createProgram(COMPILER.compile(expr).getAst()); + + Object result = program.eval(); + + assertThat(result).isEqualTo("${foo} ${bar} $2"); + } + + @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); + CelRuntime.Program program = RUNTIME.createProgram(COMPILER.compile(expr).getAst()); + + Object result = program.eval(); + + assertThat(result).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); + CelAbstractSyntaxTree ast = COMPILER.compile(expr).getAst(); + + CelEvaluationException e = + assertThrows(CelEvaluationException.class, () -> RUNTIME.createProgram(ast).eval()); + + 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')"; + CelAbstractSyntaxTree ast = COMPILER.compile(expr).getAst(); + + CelEvaluationException e = + assertThrows(CelEvaluationException.class, () -> RUNTIME.createProgram(ast).eval()); + + 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+)', '\\\\')"; + CelAbstractSyntaxTree ast = COMPILER.compile(expr).getAst(); + + CelEvaluationException e = + assertThrows(CelEvaluationException.class, () -> RUNTIME.createProgram(ast).eval()); + + 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')"; + CelAbstractSyntaxTree ast = COMPILER.compile(expr).getAst(); + + CelEvaluationException e = + assertThrows(CelEvaluationException.class, () -> RUNTIME.createProgram(ast).eval()); + + 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); + CelRuntime.Program program = RUNTIME.createProgram(COMPILER.compile(expr).getAst()); + + Object result = program.eval(); + + 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); + CelRuntime.Program program = RUNTIME.createProgram(COMPILER.compile(expr).getAst()); + + Object result = program.eval(); + + 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); + CelAbstractSyntaxTree ast = COMPILER.compile(expr).getAst(); + + CelEvaluationException e = + assertThrows(CelEvaluationException.class, () -> RUNTIME.createProgram(ast).eval()); + + 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 { + CelAbstractSyntaxTree ast = COMPILER.compile(testCase.expr).getAst(); + + Object result = RUNTIME.createProgram(ast).eval(); + + 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); + CelAbstractSyntaxTree ast = COMPILER.compile(expr).getAst(); + + CelEvaluationException e = + assertThrows(CelEvaluationException.class, () -> RUNTIME.createProgram(ast).eval()); + + 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..1aac5a023 --- /dev/null +++ b/extensions/src/test/java/dev/cel/extensions/CelSetsExtensionsTest.java @@ -0,0 +1,531 @@ +// 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.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.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.compiler.CelCompiler; +import dev.cel.compiler.CelCompilerFactory; +import dev.cel.expr.conformance.proto3.TestAllTypes; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.CelRuntime; +import dev.cel.runtime.CelRuntimeFactory; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public final class CelSetsExtensionsTest { + private static final CelOptions CEL_OPTIONS = CelOptions.current().build(); + private static final CelCompiler COMPILER = + CelCompilerFactory.standardCelCompilerBuilder() + .addMessageTypes(TestAllTypes.getDescriptor()) + .setOptions(CEL_OPTIONS) + .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) + .addLibraries(CelExtensions.sets(CEL_OPTIONS)) + .addVar("list", ListType.create(SimpleType.INT)) + .addVar("subList", ListType.create(SimpleType.INT)) + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "new_int", + CelOverloadDecl.newGlobalOverload( + "new_int_int64", SimpleType.INT, SimpleType.INT))) + .build(); + + private static final CelRuntime RUNTIME = + CelRuntimeFactory.standardCelRuntimeBuilder() + .addMessageTypes(TestAllTypes.getDescriptor()) + .addLibraries(CelExtensions.sets(CEL_OPTIONS)) + .setOptions(CEL_OPTIONS) + .addFunctionBindings( + CelFunctionBinding.from( + "new_int_int64", + Long.class, + // Intentionally return java.lang.Integer to test primitive type adaptation + Math::toIntExact)) + .build(); + + @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); + CelAbstractSyntaxTree ast = COMPILER.compile("sets.contains(list, subList)").getAst(); + CelRuntime.Program program = RUNTIME.createProgram(ast); + + Object result = program.eval(ImmutableMap.of("list", list, "subList", subList)); + + assertThat(result).isEqualTo(true); + } + + @Test + public void contains_integerListAsExpression_succeeds() throws Exception { + CelAbstractSyntaxTree ast = COMPILER.compile("sets.contains([1, 1], [1])").getAst(); + CelRuntime.Program program = RUNTIME.createProgram(ast); + + Object result = program.eval(); + + assertThat(result).isEqualTo(true); + } + + @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 { + CelAbstractSyntaxTree ast = COMPILER.compile(expression).getAst(); + CelRuntime.Program program = RUNTIME.createProgram(ast); + + boolean result = (boolean) program.eval(); + + assertThat(result).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 { + CelAbstractSyntaxTree ast = COMPILER.compile(expression).getAst(); + CelRuntime.Program program = RUNTIME.createProgram(ast); + + boolean result = (boolean) program.eval(); + + assertThat(result).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 { + CelAbstractSyntaxTree ast = COMPILER.compile("sets.contains(list, subList)").getAst(); + CelRuntime.Program program = RUNTIME.createProgram(ast); + + Object result = program.eval(ImmutableMap.of("list", list, "subList", subList)); + + assertThat(result).isEqualTo(expected); + } + + @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 { + CelAbstractSyntaxTree ast = COMPILER.compile("sets.contains(list, subList)").getAst(); + CelRuntime.Program program = RUNTIME.createProgram(ast); + + Object result = program.eval(ImmutableMap.of("list", list, "subList", subList)); + + assertThat(result).isEqualTo(expected); + } + + @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 { + CelAbstractSyntaxTree ast = COMPILER.compile(expression).getAst(); + CelRuntime.Program program = RUNTIME.createProgram(ast); + + Object result = program.eval(); + + assertThat(result).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 { + CelAbstractSyntaxTree ast = COMPILER.compile(expression).getAst(); + CelRuntime.Program program = RUNTIME.createProgram(ast); + + Object result = program.eval(); + + assertThat(result).isEqualTo(expected); + } + + @Test + @TestParameters("{expression: 'sets.contains([1], [\"1\"])'}") + @TestParameters("{expression: 'sets.contains([\"1\"], [1])'}") + public void contains_withMixingIntAndString_throwsException(String expression) throws Exception { + CelValidationResult invalidData = COMPILER.compile(expression); + + 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 { + CelAbstractSyntaxTree ast = COMPILER.compile("sets.contains([1, 2], [2u, 2.0])").getAst(); + CelRuntime.Program program = RUNTIME.createProgram(ast); + + Object result = program.eval(); + + assertThat(result).isEqualTo(true); + } + + @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 { + CelAbstractSyntaxTree ast = COMPILER.compile(expression).getAst(); + CelRuntime.Program program = RUNTIME.createProgram(ast); + + Object result = program.eval(); + + assertThat(result).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 { + CelAbstractSyntaxTree ast = COMPILER.compile(expression).getAst(); + CelRuntime.Program program = RUNTIME.createProgram(ast); + + boolean result = (boolean) program.eval(); + + assertThat(result).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 { + CelAbstractSyntaxTree ast = COMPILER.compile(expression).getAst(); + CelRuntime.Program program = RUNTIME.createProgram(ast); + + Object result = program.eval(); + + assertThat(result).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 { + CelAbstractSyntaxTree ast = COMPILER.compile(expression).getAst(); + CelRuntime.Program program = RUNTIME.createProgram(ast); + + Object result = program.eval(); + + assertThat(result).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 { + CelAbstractSyntaxTree ast = COMPILER.compile(expression).getAst(); + CelRuntime.Program program = RUNTIME.createProgram(ast); + + boolean result = (boolean) program.eval(); + + assertThat(result).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 { + CelAbstractSyntaxTree ast = COMPILER.compile(expression).getAst(); + CelRuntime.Program program = RUNTIME.createProgram(ast); + + boolean result = (boolean) program.eval(); + + assertThat(result).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 { + CelAbstractSyntaxTree ast = COMPILER.compile(expression).getAst(); + CelRuntime.Program program = RUNTIME.createProgram(ast); + + Object result = program.eval(); + + assertThat(result).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 { + CelAbstractSyntaxTree ast = COMPILER.compile(expression).getAst(); + CelRuntime.Program program = RUNTIME.createProgram(ast); + + boolean result = (boolean) program.eval(); + + assertThat(result).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 { + CelAbstractSyntaxTree ast = COMPILER.compile(expression).getAst(); + CelRuntime.Program program = RUNTIME.createProgram(ast); + + boolean result = (boolean) program.eval(); + + assertThat(result).isEqualTo(expected); + } + + @Test + public void setsExtension_containsFunctionSubset_succeeds() throws Exception { + CelSetsExtensions setsExtensions = + CelExtensions.sets(CelOptions.DEFAULT, SetsFunction.CONTAINS); + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder().addLibraries(setsExtensions).build(); + CelRuntime celRuntime = + CelRuntimeFactory.standardCelRuntimeBuilder().addLibraries(setsExtensions).build(); + + Object evaluatedResult = + celRuntime.createProgram(celCompiler.compile("sets.contains([1, 2], [2])").getAst()).eval(); + + assertThat(evaluatedResult).isEqualTo(true); + } + + @Test + public void setsExtension_equivalentFunctionSubset_succeeds() throws Exception { + CelSetsExtensions setsExtensions = + CelExtensions.sets(CelOptions.DEFAULT, SetsFunction.EQUIVALENT); + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder().addLibraries(setsExtensions).build(); + CelRuntime celRuntime = + CelRuntimeFactory.standardCelRuntimeBuilder().addLibraries(setsExtensions).build(); + + Object evaluatedResult = + celRuntime + .createProgram(celCompiler.compile("sets.equivalent([1, 1], [1])").getAst()) + .eval(); + + assertThat(evaluatedResult).isEqualTo(true); + } + + @Test + public void setsExtension_intersectsFunctionSubset_succeeds() throws Exception { + CelSetsExtensions setsExtensions = + CelExtensions.sets(CelOptions.DEFAULT, SetsFunction.INTERSECTS); + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder().addLibraries(setsExtensions).build(); + CelRuntime celRuntime = + CelRuntimeFactory.standardCelRuntimeBuilder().addLibraries(setsExtensions).build(); + + Object evaluatedResult = + celRuntime + .createProgram(celCompiler.compile("sets.intersects([1, 1], [1])").getAst()) + .eval(); + + assertThat(evaluatedResult).isEqualTo(true); + } + + @Test + public void setsExtension_compileUnallowedFunction_throws() { + CelSetsExtensions setsExtensions = + CelExtensions.sets(CelOptions.DEFAULT, SetsFunction.EQUIVALENT); + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder().addLibraries(setsExtensions).build(); + + assertThrows( + CelValidationException.class, + () -> celCompiler.compile("sets.contains([1, 2], [2])").getAst()); + } + + @Test + public void setsExtension_evaluateUnallowedFunction_throws() throws Exception { + CelSetsExtensions setsExtensions = + CelExtensions.sets(CelOptions.DEFAULT, SetsFunction.CONTAINS, SetsFunction.EQUIVALENT); + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder().addLibraries(setsExtensions).build(); + CelRuntime celRuntime = + CelRuntimeFactory.standardCelRuntimeBuilder() + .addLibraries(CelExtensions.sets(CelOptions.DEFAULT, SetsFunction.EQUIVALENT)) + .build(); + + CelAbstractSyntaxTree ast = celCompiler.compile("sets.contains([1, 2], [2])").getAst(); + + assertThrows(CelEvaluationException.class, () -> celRuntime.createProgram(ast).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..6ea9b702c 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelStringExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelStringExtensionsTest.java @@ -23,6 +23,8 @@ import com.google.testing.junit.testparameterinjector.TestParameterInjector; import com.google.testing.junit.testparameterinjector.TestParameters; import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelFunctionDecl; +import dev.cel.common.CelOptions; import dev.cel.common.CelValidationException; import dev.cel.common.types.SimpleType; import dev.cel.compiler.CelCompiler; @@ -54,6 +56,27 @@ public final class CelStringExtensionsTest { private static final CelRuntime RUNTIME = CelRuntimeFactory.standardCelRuntimeBuilder().addLibraries(CelExtensions.strings()).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", + "split", + "substring", + "trim", + "upperAscii"); + assertThat(library.version(0).macros()).isEmpty(); + } + @Test @TestParameters("{string: 'abcd', beginIndex: 0, expectedResult: 'abcd'}") @TestParameters("{string: 'abcd', beginIndex: 1, expectedResult: 'bcd'}") @@ -887,6 +910,7 @@ public void join_separatorIsNonString_throwsException() { } @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}") @@ -953,21 +977,20 @@ public void lastIndexOf_unicode_success(String string, String lastIndexOf, int e } @Test + @TestParameters("{lastIndexOf: '@@'}") @TestParameters("{lastIndexOf: ' '}") @TestParameters("{lastIndexOf: 'a'}") @TestParameters("{lastIndexOf: 'abc'}") @TestParameters("{lastIndexOf: '나'}") @TestParameters("{lastIndexOf: '😁'}") - public void lastIndexOf_onEmptyString_throwsException(String lastIndexOf) throws Exception { + public void lastIndexOf_strLengthLessThanSubstrLength_returnsMinusOne(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))); + Object evaluatedResult = program.eval(ImmutableMap.of("s", "", "indexOfParam", lastIndexOf)); - assertThat(exception).hasMessageThat().contains("lastIndexOf failure: Offset out of range"); + assertThat(evaluatedResult).isEqualTo(-1); } @Test 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..bd31f053d --- /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("@rules_proto//proto:defs.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 f0bc0bae7..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"], @@ -25,8 +27,6 @@ java_library( java_library( name = "mutable_ast", - testonly = 1, - visibility = ["//optimizer/src/test/java/dev/cel/optimizer:__pkg__"], exports = ["//optimizer/src/main/java/dev/cel/optimizer:mutable_ast"], ) diff --git a/optimizer/optimizers/BUILD.bazel b/optimizer/optimizers/BUILD.bazel index c6cdf913a..21241241f 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"], @@ -7,3 +9,8 @@ java_library( name = "constant_folding", exports = ["//optimizer/src/main/java/dev/cel/optimizer/optimizers:constant_folding"], ) + +java_library( + name = "common_subexpression_elimination", + exports = ["//optimizer/src/main/java/dev/cel/optimizer/optimizers:common_subexpression_elimination"], +) 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..00c5c2e3a --- /dev/null +++ b/optimizer/src/main/java/dev/cel/optimizer/AstMutator.java @@ -0,0 +1,926 @@ +// 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 a 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: + * + *

+ * + * @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. + * @param incrementSerially If true, indices for the mangled variables are incremented serially + * per occurrence regardless of their nesting level or its types. + */ + public MangledComprehensionAst mangleComprehensionIdentifierNames( + CelMutableAst ast, + String newIterVarPrefix, + String newAccuVarPrefix, + boolean incrementSerially) { + 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)); + + 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 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(result)); + }) + .collect( + Collectors.toMap( + k -> k, + v -> { + CelMutableComprehension 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 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 iterVarType = + iterVarId.map( + id -> + navigableMutableAst + .getType(id) + .orElseThrow( + () -> + new NoSuchElementException( + "Checked type not present for iteration" + + " variable: " + + iterVarId))); + 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, 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; + if (incrementSerially) { + // In case of applying CSE via cascaded cel.binds, not only is mangling based on level/types + // meaningless (because all comprehensions are nested anyways, thus all indices would be + // uinque), + // it can lead to an erroneous result due to extracting a common subexpr with accu_var at + // the wrong scope. + // Example: "[1].exists(k, k > 1) && [2].exists(l, l > 1). The loop step for both branches + // are identical, but shouldn't be extracted. + String mangledIterVarName = newIterVarPrefix + ":" + iterCount; + String mangledResultName = newAccuVarPrefix + ":" + iterCount; + mangledComprehensionName = + MangledComprehensionName.of(mangledIterVarName, mangledResultName); + mangledIdentNamesToType.put(mangledComprehensionName, comprehensionEntry.getValue()); + } else { + mangledComprehensionName = + getMangledComprehensionName( + newIterVarPrefix, + newAccuVarPrefix, + comprehensionNode, + comprehensionLevelToType, + comprehensionEntryType); + } + mangledIdentNamesToType.put(mangledComprehensionName, comprehensionEntryType); + + String iterVar = comprehensionExpr.comprehension().iterVar(); + String accuVar = comprehensionExpr.comprehension().accuVar(); + mutatedComprehensionExpr = + mangleIdentsInComprehensionExpr( + mutatedComprehensionExpr, + comprehensionExpr, + iterVar, + accuVar, + mangledComprehensionName); + // Repeat the mangling process for the macro source. + newSource = + mangleIdentsInMacroSource( + newSource, + mutatedComprehensionExpr, + iterVar, + 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 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; + mangledComprehensionName = MangledComprehensionName.of(mangledIterVarName, mangledResultName); + comprehensionLevelToType.put( + comprehensionNestingLevel, comprehensionEntryType, mangledComprehensionName); + } + return mangledComprehensionName; + } + + private static int countComprehensionNestingLevel(CelNavigableMutableExpr 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; + } + + /** + * 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 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()); + } + + 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, + 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)) { + // if (identToMangle.identOrDefault().name().equals(originalIterVar)) { + macroExpr = + mutateExpr( + NO_OP_ID_GENERATOR, + macroExpr, + CelMutableExpr.ofIdent(mangledComprehensionName.iterVarName()), + 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 = null; + 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 */ + public abstract Optional iterVarType(); + + /** Type of comprehension result */ + public abstract CelType resultType(); + + private static MangledComprehensionType of(Optional iterVarType, CelType resultType) { + return new AutoValue_AstMutator_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_AstMutator_MangledComprehensionName(iterVarName, resultName); + } + } +} diff --git a/optimizer/src/main/java/dev/cel/optimizer/BUILD.bazel b/optimizer/src/main/java/dev/cel/optimizer/BUILD.bazel index e145a2088..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", ], ) @@ -66,26 +67,36 @@ java_library( tags = [ ], deps = [ - ":mutable_ast", ":optimization_exception", + "//:auto_value", "//bundle:cel", - "//common", - "//common/ast", - "//common/navigation", + "//common:cel_ast", + "//common:compiler_common", + "@maven//:com_google_guava_guava", ], ) java_library( name = "mutable_ast", - srcs = ["MutableAst.java"], + srcs = [ + "AstMutator.java", + "MutableExprVisitor.java", + ], tags = [ ], deps = [ - "//common", + "//:auto_value", + "//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 652dca0e2..7a14f0f70 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/CelAstOptimizer.java +++ b/optimizer/src/main/java/dev/cel/optimizer/CelAstOptimizer.java @@ -14,37 +14,47 @@ package dev.cel.optimizer; +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.ast.CelExpr; -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, Cel cel) - throws CelOptimizationException; + OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel) throws CelOptimizationException; /** - * Replaces a subtree in the given AST. This operation is intended for AST optimization purposes. + * Denotes the result of a single optimization pass on an AST. * - *

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. + *

The optimizer may optionally populate new variable and function declarations generated as + * part of optimizing an AST. */ - default CelAbstractSyntaxTree replaceSubtree( - CelAbstractSyntaxTree ast, CelExpr newExpr, long exprIdToReplace) { - return MutableAst.replaceSubtree(ast, newExpr, exprIdToReplace); + @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 8ce74e1a3..4ac8764f1 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerImpl.java +++ b/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerImpl.java @@ -20,7 +20,7 @@ import dev.cel.bundle.Cel; 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 { @@ -38,12 +38,20 @@ public CelAbstractSyntaxTree optimize(CelAbstractSyntaxTree ast) throws CelOptim throw new IllegalArgumentException("AST must be type-checked."); } + Cel celOptimizerEnv = cel; CelAbstractSyntaxTree optimizedAst = ast; try { for (CelAstOptimizer optimizer : astOptimizers) { - CelNavigableAst navigableAst = CelNavigableAst.fromAst(ast); - optimizedAst = optimizer.optimize(navigableAst, cel); - optimizedAst = cel.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 64f5913d9..000000000 --- a/optimizer/src/main/java/dev/cel/optimizer/MutableAst.java +++ /dev/null @@ -1,266 +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 com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import dev.cel.common.CelAbstractSyntaxTree; -import dev.cel.common.CelSource; -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; -import dev.cel.common.ast.CelExprIdGeneratorFactory.MonotonicIdGenerator; -import dev.cel.common.ast.CelExprIdGeneratorFactory.StableIdGenerator; -import dev.cel.common.navigation.CelNavigableExpr; -import java.util.Map.Entry; -import java.util.NoSuchElementException; - -/** MutableAst contains logic for mutating a {@link CelExpr}. */ -@Internal -final class MutableAst { - private static final int MAX_ITERATION_COUNT = 500; - private final CelExpr.Builder newExpr; - private final ExprIdGenerator celExprIdGenerator; - private int iterationCount; - private long exprIdToReplace; - - private MutableAst(ExprIdGenerator celExprIdGenerator, CelExpr.Builder newExpr, long exprId) { - this.celExprIdGenerator = celExprIdGenerator; - this.newExpr = newExpr; - this.exprIdToReplace = exprId; - } - - /** - * Replaces a subtree in the given CelExpr. - * - *

This method should remain package-private. - */ - static CelAbstractSyntaxTree replaceSubtree( - CelAbstractSyntaxTree ast, CelExpr newExpr, long exprIdToReplace) { - // Update the IDs in the new expression tree first. This ensures that no ID collision - // occurs while attempting to replace the subtree, potentially leading to infinite loop - MonotonicIdGenerator monotonicIdGenerator = - CelExprIdGeneratorFactory.newMonotonicIdGenerator(getMaxId(ast.getExpr())); - CelExpr.Builder newExprBuilder = - renumberExprIds((unused) -> monotonicIdGenerator.nextExprId(), newExpr.toBuilder()); - - StableIdGenerator stableIdGenerator = CelExprIdGeneratorFactory.newStableIdGenerator(0); - CelExpr.Builder mutatedRoot = - replaceSubtreeImpl( - stableIdGenerator::renumberId, - ast.getExpr().toBuilder(), - newExprBuilder, - exprIdToReplace); - - // If the source info contained macro call information, their IDs must be normalized. - CelSource normalizedSource = - normalizeMacroSource( - ast.getSource(), exprIdToReplace, mutatedRoot, stableIdGenerator::renumberId); - - return CelAbstractSyntaxTree.newParsedAst(mutatedRoot.build(), normalizedSource); - } - - private static 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(); - if (celSource.getMacroCalls().isEmpty()) { - return CelSource.newBuilder().build(); - } - - CelSource.Builder sourceBuilder = CelSource.newBuilder(); - 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); - - 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 = - replaceSubtreeImpl((arg) -> arg, newCall, mutatedExpr.toBuilder(), callChild.id()); - } - } - sourceBuilder.addMacroCalls(callId, newCall.build()); - } - - return sourceBuilder.build(); - } - - private static CelExpr.Builder replaceSubtreeImpl( - ExprIdGenerator idGenerator, - CelExpr.Builder root, - CelExpr.Builder newExpr, - long exprIdToReplace) { - MutableAst mutableAst = new MutableAst(idGenerator, newExpr, exprIdToReplace); - return mutableAst.visit(root); - } - - private static CelExpr.Builder renumberExprIds( - ExprIdGenerator idGenerator, CelExpr.Builder root) { - MutableAst mutableAst = new MutableAst(idGenerator, root, Integer.MIN_VALUE); - return mutableAst.visit(root); - } - - private static long getMaxId(CelExpr newExpr) { - return CelNavigableExpr.fromExpr(newExpr) - .allNodes() - .mapToLong(node -> node.expr().id()) - .max() - .orElseThrow(NoSuchElementException::new); - } - - private CelExpr.Builder visit(CelExpr.Builder expr) { - if (++iterationCount > MAX_ITERATION_COUNT) { - throw new IllegalStateException("Max iteration count reached."); - } - - if (expr.id() == exprIdToReplace) { - exprIdToReplace = Integer.MIN_VALUE; // Marks that the subtree has been replaced. - return visit(newExpr.setId(expr.id())); - } - - expr.setId(celExprIdGenerator.generate(expr.id())); - - switch (expr.exprKind().getKind()) { - case SELECT: - return visit(expr, expr.select().toBuilder()); - case CALL: - return visit(expr, expr.call().toBuilder()); - case CREATE_LIST: - return visit(expr, expr.createList().toBuilder()); - case CREATE_STRUCT: - return visit(expr, expr.createStruct().toBuilder()); - case CREATE_MAP: - return visit(expr, expr.createMap().toBuilder()); - case COMPREHENSION: - return visit(expr, expr.comprehension().toBuilder()); - case CONSTANT: // Fall-through is intended - case IDENT: - case NOT_SET: // Note: comprehension arguments can contain a not set expr. - return expr; - default: - throw new IllegalArgumentException("unexpected expr kind: " + expr.exprKind().getKind()); - } - } - - private CelExpr.Builder visit(CelExpr.Builder expr, CelSelect.Builder select) { - select.setOperand(visit(select.operand().toBuilder()).build()); - return expr.setSelect(select.build()); - } - - private CelExpr.Builder visit(CelExpr.Builder expr, CelCall.Builder call) { - if (call.target().isPresent()) { - call.setTarget(visit(call.target().get().toBuilder()).build()); - } - ImmutableList argsBuilders = call.getArgsBuilders(); - for (int i = 0; i < argsBuilders.size(); i++) { - CelExpr.Builder arg = argsBuilders.get(i); - call.setArg(i, visit(arg).build()); - } - - return expr.setCall(call.build()); - } - - 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); - entry.setValue(visit(entry.value().toBuilder()).build()); - - createStruct.setEntry(i, entry.build()); - } - - return expr.setCreateStruct(createStruct.build()); - } - - 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); - entry.setKey(visit(entry.key().toBuilder()).build()); - entry.setValue(visit(entry.value().toBuilder()).build()); - - createMap.setEntry(i, entry.build()); - } - - return expr.setCreateMap(createMap.build()); - } - - private CelExpr.Builder visit(CelExpr.Builder expr, CelCreateList.Builder createList) { - ImmutableList elementsBuilders = createList.getElementsBuilders(); - for (int i = 0; i < elementsBuilders.size(); i++) { - CelExpr.Builder elem = elementsBuilders.get(i); - createList.setElement(i, visit(elem).build()); - } - - return expr.setCreateList(createList.build()); - } - - 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()); - - return expr.setComprehension(comprehension.build()); - } - - @FunctionalInterface - private interface ExprIdGenerator { - - /** Generates an expression ID based on the provided ID. */ - long generate(long exprId); - } -} diff --git a/optimizer/src/main/java/dev/cel/optimizer/MutableExprVisitor.java b/optimizer/src/main/java/dev/cel/optimizer/MutableExprVisitor.java new file mode 100644 index 000000000..7a04a3c3a --- /dev/null +++ b/optimizer/src/main/java/dev/cel/optimizer/MutableExprVisitor.java @@ -0,0 +1,165 @@ +// 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; + +import com.google.common.base.Preconditions; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import dev.cel.common.annotations.Internal; +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 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. + */ +@Internal +final class MutableExprVisitor { + private final CelMutableExpr newExpr; + private final ExprIdGenerator celExprIdGenerator; + private final long iterationLimit; + private int iterationCount; + private long exprIdToReplace; + + static MutableExprVisitor newInstance( + ExprIdGenerator idGenerator, + CelMutableExpr newExpr, + long exprIdToReplace, + long iterationLimit) { + // iterationLimit * 2, because the expr can be walked twice due to the immutable nature of + // CelExpr. + return new MutableExprVisitor(idGenerator, newExpr, exprIdToReplace, iterationLimit * 2); + } + + 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. + this.newExpr.setId(root.id()); + return visit(this.newExpr); + } + + root.setId(celExprIdGenerator.generate(root.id())); + + switch (root.getKind()) { + case SELECT: + return visit(root, root.select()); + case CALL: + 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()); + 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.getKind()); + } + + @CanIgnoreReturnValue + private CelMutableExpr visit(CelMutableExpr expr, CelMutableSelect select) { + select.setOperand(visit(select.operand())); + return expr; + } + + @CanIgnoreReturnValue + private CelMutableExpr visit(CelMutableExpr expr, CelMutableCall call) { + if (call.target().isPresent()) { + call.setTarget(visit(call.target().get())); + } + List argsBuilders = call.args(); + for (int i = 0; i < argsBuilders.size(); i++) { + CelMutableExpr arg = argsBuilders.get(i); + call.setArg(i, visit(arg)); + } + + return expr; + } + + @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())); + } + + return expr; + } + + @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())); + entry.setValue(visit(entry.value())); + } + + return expr; + } + + @CanIgnoreReturnValue + private CelMutableExpr visit(CelMutableExpr expr, CelMutableList list) { + List elementsBuilders = list.elements(); + for (int i = 0; i < elementsBuilders.size(); i++) { + CelMutableExpr elem = elementsBuilders.get(i); + list.setElement(i, visit(elem)); + } + + return expr; + } + + @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; + } + + private MutableExprVisitor( + ExprIdGenerator celExprIdGenerator, + CelMutableExpr newExpr, + long exprId, + long iterationLimit) { + Preconditions.checkState(iterationLimit > 0L); + this.iterationLimit = iterationLimit; + this.celExprIdGenerator = celExprIdGenerator; + this.newExpr = newExpr; + this.exprIdToReplace = exprId; + } +} 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 730f0c0d2..7728c0ac8 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,16 +16,69 @@ java_library( tags = [ ], deps = [ + ":default_optimizer_constants", + "//:auto_value", "//bundle:cel", - "//common", + "//common:cel_ast", + "//common:cel_source", "//common:compiler_common", + "//common:mutable_ast", "//common/ast", - "//common/ast:expr_util", - "//common/navigation", + "//common/ast:mutable_expr", + "//common/navigation:mutable_navigation", + "//extensions:optional_library", "//optimizer:ast_optimizer", + "//optimizer:mutable_ast", "//optimizer:optimization_exception", "//parser:operator", "//runtime", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "common_subexpression_elimination", + srcs = [ + "SubexpressionOptimizer.java", + ], + tags = [ + ], + deps = [ + ":default_optimizer_constants", + "//:auto_value", + "//bundle:cel", + "//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", + "//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", + "//extensions", + "//extensions:optional_library", + "//parser:operator", "@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 83114d144..e46ee2e98 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java +++ b/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java @@ -13,105 +13,137 @@ // 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 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.common.CelAbstractSyntaxTree; +import dev.cel.common.CelMutableAst; +import dev.cel.common.CelSource; import dev.cel.common.CelValidationException; import dev.cel.common.ast.CelConstant; import dev.cel.common.ast.CelExpr; -import dev.cel.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.navigation.CelNavigableMutableAst; +import dev.cel.common.navigation.CelNavigableMutableExpr; +import dev.cel.extensions.CelOptionalLibrary.Function; +import dev.cel.optimizer.AstMutator; import dev.cel.optimizer.CelAstOptimizer; import dev.cel.optimizer.CelOptimizationException; import dev.cel.parser.Operator; import dev.cel.runtime.CelEvaluationException; +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 * calls and select statements with their evaluated result. */ public final class ConstantFoldingOptimizer implements CelAstOptimizer { - public static final ConstantFoldingOptimizer INSTANCE = new ConstantFoldingOptimizer(); - private static final int MAX_ITERATION_COUNT = 400; + private static final ConstantFoldingOptimizer INSTANCE = + new ConstantFoldingOptimizer(ConstantFoldingOptions.newBuilder().build()); + + /** Returns a default instance of constant folding optimizer with preconfigured defaults. */ + public static ConstantFoldingOptimizer getInstance() { + return INSTANCE; + } + + /** + * Returns a new instance of constant folding optimizer configured with the provided {@link + * ConstantFoldingOptions}. + */ + public static ConstantFoldingOptimizer newInstance( + ConstantFoldingOptions constantFoldingOptions) { + 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 String OPTIONAL_OF_FUNCTION = "optional.of"; - private static final String OPTIONAL_NONE_FUNCTION = "optional.none"; - private static final CelExpr OPTIONAL_NONE_EXPR = - CelExpr.ofCallExpr(0, Optional.empty(), OPTIONAL_NONE_FUNCTION, ImmutableList.of()); + private static CelMutableExpr newOptionalNoneExpr() { + return CelMutableExpr.ofCall(CelMutableCall.create(Function.OPTIONAL_NONE.getFunction())); + } @Override - public CelAbstractSyntaxTree optimize(CelNavigableAst navigableAst, Cel cel) + public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel) throws CelOptimizationException { - Set visitedExprs = new HashSet<>(); + CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); int iterCount = 0; - while (true) { - iterCount++; - if (iterCount == MAX_ITERATION_COUNT) { + 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()); - } + .filter(this::canFold) + .collect(toImmutableList()); + for (CelNavigableMutableExpr foldableExpr : foldableExprs) { + iterCount++; + + Optional mutatedResult; + // Attempt to prune if it is a non-strict call + mutatedResult = maybePruneBranches(mutableAst, foldableExpr.expr()); + if (!mutatedResult.isPresent()) { + // Evaluate the call then fold + mutatedResult = maybeFold(cel, mutableAst, foldableExpr); + } - 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(); + } } // 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. - navigableAst = CelNavigableAst.fromAst(pruneOptionalElements(navigableAst)); - - return navigableAst.getAst(); + 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; + } + + CelMutableCall mutableCall = navigableExpr.expr().call(); + String functionName = mutableCall.function(); // These are already folded or do not need to be folded. - if (functionName.equals(OPTIONAL_OF_FUNCTION) - || functionName.equals(OPTIONAL_NONE_FUNCTION)) { + if (functionName.equals(Function.OPTIONAL_OF.getFunction()) + || functionName.equals(Function.OPTIONAL_NONE.getFunction())) { return false; } @@ -120,26 +152,26 @@ 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.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); @@ -148,25 +180,66 @@ private static boolean canFold(CelNavigableExpr navigableExpr) { } } - private static boolean areChildrenArgConstant(CelNavigableExpr expr) { + private boolean containsFoldableFunctionOnly(CelNavigableMutableExpr navigableExpr) { + return navigableExpr + .allNodes() + .allMatch( + node -> { + if (node.getKind().equals(Kind.CALL)) { + return foldableFunctions.contains(node.expr().call().function()); + } + + return true; + }); + } + + 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.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; } @@ -176,11 +249,12 @@ 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 { Object result; try { - result = CelExprUtil.evaluateExpr(cel, expr); + result = evaluateExpr(cel, CelMutableExprConverter.fromMutableExpr(node.expr())); } catch (CelValidationException | CelEvaluationException e) { throw new CelOptimizationException( "Constant folding failure. Failed to evaluate subtree due to: " + e.getMessage(), e); @@ -191,135 +265,126 @@ 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 -> 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))); } // Evaluated result cannot be folded (e.g: unknowns) return Optional.empty(); } - private Optional maybeRewriteOptional( - Optional optResult, CelAbstractSyntaxTree ast, CelExpr expr) { + private Optional maybeRewriteOptional( + Optional optResult, CelMutableAst mutableAst, CelMutableExpr expr) { if (!optResult.isPresent()) { - if (!expr.callOrDefault().function().equals(OPTIONAL_NONE_FUNCTION)) { + if (!expr.call().function().equals(Function.OPTIONAL_NONE.getFunction())) { // An empty optional value was encountered. Rewrite the tree with optional.none call. // This is to account for other optional functions returning an empty optional value // e.g: optional.ofNonZeroValue(0) - return Optional.of(replaceSubtree(ast, OPTIONAL_NONE_EXPR, expr.id())); + return Optional.of(astMutator.replaceSubtree(mutableAst, newOptionalNoneExpr(), expr.id())); } - } else if (!expr.callOrDefault().function().equals(OPTIONAL_OF_FUNCTION)) { + } else if (!expr.call().function().equals(Function.OPTIONAL_OF.getFunction())) { Object unwrappedResult = optResult.get(); if (!CelConstant.isConstantValue(unwrappedResult)) { // Evaluated result is not a constant. Leave the optional as is. return Optional.empty(); } - CelExpr newOptionalOfCall = - CelExpr.newBuilder() - .setCall( - CelCall.newBuilder() - .setFunction(OPTIONAL_OF_FUNCTION) - .addArgs( - CelExpr.newBuilder() - .setConstant(CelConstant.ofObjectValue(unwrappedResult)) - .build()) - .build()) - .build(); - return Optional.of(replaceSubtree(ast, newOptionalOfCall, expr.id())); + CelMutableExpr newOptionalOfCall = + CelMutableExpr.ofCall( + CelMutableCall.create( + Function.OPTIONAL_OF.getFunction(), + CelMutableExpr.ofConstant(CelConstant.ofObjectValue(unwrappedResult)))); + + return Optional.of(astMutator.replaceSubtree(mutableAst, newOptionalOfCall, expr.id())); } return Optional.empty(); } /** 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(replaceSubtree(ast, result, expr.id())); + return Optional.of(astMutator.replaceSubtree(mutableAst, result, expr.id())); } else if (function.equals(Operator.IN.getFunction())) { - CelCreateList haystack = call.args().get(1).createList(); + CelMutableExpr callArg = call.args().get(1); + if (!callArg.getKind().equals(Kind.LIST)) { + return Optional.empty(); + } + + CelMutableList haystack = callArg.list(); if (haystack.elements().isEmpty()) { return Optional.of( - 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( - replaceSubtree( - ast, - CelExpr.newBuilder().setConstant(CelConstant.ofValue(true)).build(), + astMutator.replaceSubtree( + mutableAst.expr(), + CelMutableExpr.ofConstant(CelConstant.ofValue(true)), expr.id())); } } @@ -329,19 +394,19 @@ private Optional maybePruneBranches( 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; } @@ -350,16 +415,18 @@ private Optional maybeShortCircuitCall( } if (arg.constant().booleanValue() == shortCircuit) { - return Optional.of(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(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(replaceSubtree(ast, newArgs.get(0), expr.id())); + return Optional.of(astMutator.replaceSubtree(mutableAst, newArgs.get(0), expr.id())); } // TODO: Support folding variadic AND/ORs. @@ -367,66 +434,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 (call.function().equals(OPTIONAL_NONE_FUNCTION)) { + 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(OPTIONAL_OF_FUNCTION)) { - CelExpr arg = call.args().get(0); - if (arg.exprKind().getKind().equals(Kind.CONSTANT)) { + } else if (call.function().equals(Function.OPTIONAL_OF.getFunction())) { + CelMutableExpr arg = call.args().get(0); + if (arg.getKind().equals(Kind.CONSTANT)) { updatedElemBuilder.add(call.args().get(0)); continue; } @@ -437,27 +503,22 @@ private CelAbstractSyntaxTree pruneOptionalListElements(CelAbstractSyntaxTree as updatedIndicesBuilder.add(newOptIndex); } - return 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)) { @@ -465,17 +526,18 @@ private CelAbstractSyntaxTree pruneOptionalMapElements(CelAbstractSyntaxTree ast continue; } - CelCall call = value.call(); - if (call.function().equals(OPTIONAL_NONE_FUNCTION)) { + 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(OPTIONAL_OF_FUNCTION)) { - CelExpr arg = call.args().get(0); - if (arg.exprKind().getKind().equals(Kind.CONSTANT)) { + } else if (call.function().equals(Function.OPTIONAL_OF.getFunction())) { + 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; } } @@ -484,44 +546,39 @@ private CelAbstractSyntaxTree pruneOptionalMapElements(CelAbstractSyntaxTree ast } if (modified) { - return 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(); - if (call.function().equals(OPTIONAL_NONE_FUNCTION)) { + 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(OPTIONAL_OF_FUNCTION)) { - CelExpr arg = call.args().get(0); - if (arg.exprKind().getKind().equals(Kind.CONSTANT)) { + } else if (call.function().equals(Function.OPTIONAL_OF.getFunction())) { + 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; } } @@ -530,20 +587,85 @@ private CelAbstractSyntaxTree pruneOptionalStructElements( } if (modified) { - return 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; } - private ConstantFoldingOptimizer() {} + @CanIgnoreReturnValue + private 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(); + } + + /** 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() {} + } + + /** Returns a new options builder with recommended defaults pre-configured. */ + public static Builder newBuilder() { + return new AutoValue_ConstantFoldingOptimizer_ConstantFoldingOptions.Builder() + .maxIterationLimit(400); + } + + ConstantFoldingOptions() {} + } + + private ConstantFoldingOptimizer(ConstantFoldingOptions constantFoldingOptions) { + this.constantFoldingOptions = constantFoldingOptions; + 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..35608a776 --- /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.extensions.CelExtensions; +import dev.cel.extensions.CelOptionalLibrary; +import dev.cel.extensions.CelOptionalLibrary.Function; +import dev.cel.parser.Operator; + +/** + * 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/SubexpressionOptimizer.java b/optimizer/src/main/java/dev/cel/optimizer/optimizers/SubexpressionOptimizer.java new file mode 100644 index 000000000..a39ff813a --- /dev/null +++ b/optimizer/src/main/java/dev/cel/optimizer/optimizers/SubexpressionOptimizer.java @@ -0,0 +1,655 @@ +// 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.base.Preconditions.checkNotNull; +import static com.google.common.collect.ImmutableList.toImmutableList; +import static java.util.stream.Collectors.toCollection; + +import com.google.auto.value.AutoValue; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.base.Verify; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +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.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; +import dev.cel.common.CelSource.Extension.Component; +import dev.cel.common.CelSource.Extension.Version; +import dev.cel.common.CelValidationException; +import dev.cel.common.CelVarDecl; +import dev.cel.common.ast.CelExpr; +import dev.cel.common.ast.CelExpr.CelCall; +import dev.cel.common.ast.CelExpr.ExprKind.Kind; +import dev.cel.common.ast.CelMutableExpr; +import dev.cel.common.ast.CelMutableExprConverter; +import dev.cel.common.navigation.CelNavigableExpr; +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.optimizer.AstMutator; +import dev.cel.optimizer.AstMutator.MangledComprehensionAst; +import dev.cel.optimizer.CelAstOptimizer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Performs Common Subexpression Elimination. + * + *

+ * Subexpressions are extracted into `cel.bind` calls. For example, the expression below:
+ *
+ * {@code
+ *    message.child.text_map[x].startsWith("hello") && message.child.text_map[x].endsWith("world")
+ * }
+ *
+ * will be optimized into the following form:
+ *
+ * {@code
+ *    cel.bind(@r0, message.child.text_map[x],
+ *        @r0.startsWith("hello") && @r0.endsWith("world"))
+ * }
+ *
+ * Or, using the equivalent form of cel.@block (requires special runtime support):
+ * {@code
+ *    cel.block([message.child.text_map[x]],
+ *        @index0.startsWith("hello") && @index1.endsWith("world"))
+ * }
+ * 
+ */ +public 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_ITER_VAR_PREFIX = "@it"; + private static final String MANGLED_COMPREHENSION_ACCU_VAR_PREFIX = "@ac"; + 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); + + private final SubexpressionOptimizerOptions cseOptions; + private final AstMutator astMutator; + private final ImmutableSet cseEliminableFunctions; + + /** + * Returns a default instance of common subexpression elimination optimizer with preconfigured + * defaults. + */ + public static SubexpressionOptimizer getInstance() { + return INSTANCE; + } + + /** + * Returns a new instance of common subexpression elimination optimizer configured with the + * provided {@link SubexpressionOptimizerOptions}. + */ + public static SubexpressionOptimizer newInstance(SubexpressionOptimizerOptions cseOptions) { + return new SubexpressionOptimizer(cseOptions); + } + + @Override + public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel) { + OptimizationResult result = optimizeUsingCelBlock(ast, cel); + + verifyOptimizedAstCorrectness(result.optimizedAst()); + + return result; + } + + private OptimizationResult optimizeUsingCelBlock(CelAbstractSyntaxTree ast, Cel cel) { + CelMutableAst astToModify = CelMutableAst.fromCelAst(ast); + if (!cseOptions.populateMacroCalls()) { + astToModify.source().clearMacroCalls(); + } + + MangledComprehensionAst mangledComprehensionAst = + astMutator.mangleComprehensionIdentifierNames( + astToModify, + MANGLED_COMPREHENSION_ITER_VAR_PREFIX, + MANGLED_COMPREHENSION_ACCU_VAR_PREFIX, + /* incrementSerially= */ false); + astToModify = mangledComprehensionAst.mutableAst(); + CelMutableSource sourceToModify = astToModify.source(); + + int blockIdentifierIndex = 0; + int iterCount; + ArrayList subexpressions = new ArrayList<>(); + + for (iterCount = 0; iterCount < cseOptions.iterationLimit(); iterCount++) { + CelNavigableMutableAst navAst = CelNavigableMutableAst.fromAst(astToModify); + List cseCandidates = getCseCandidates(navAst); + if (cseCandidates.isEmpty()) { + break; + } + + subexpressions.add(cseCandidates.get(0)); + + String blockIdentifier = BLOCK_INDEX_PREFIX + blockIdentifierIndex++; + + // Replace all CSE candidates with new block index identifier + for (CelMutableExpr cseCandidate : cseCandidates) { + iterCount++; + + astToModify = + astMutator.replaceSubtree( + navAst, + CelNavigableMutableAst.fromAst( + CelMutableAst.of( + CelMutableExpr.ofIdent(blockIdentifier), navAst.getAst().source())), + cseCandidate.id()); + + // Retain the existing macro calls in case if the block identifiers are replacing a subtree + // that contains a comprehension. + sourceToModify.addAllMacroCalls(astToModify.source().getMacroCalls()); + astToModify = CelMutableAst.of(astToModify.expr(), sourceToModify); + } + } + + if (iterCount >= cseOptions.iterationLimit()) { + throw new IllegalStateException("Max iteration count reached."); + } + + if (iterCount == 0) { + // No modification has been made. + 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 + .mangledComprehensionMap() + .forEach( + (name, type) -> { + type.iterVarType() + .ifPresent( + iterVarType -> + newVarDecls.add( + CelVarDecl.newVarDeclaration(name.iterVarName(), iterVarType))); + newVarDecls.add(CelVarDecl.newVarDeclaration(name.resultName(), type.resultType())); + }); + + // 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 + astToModify = + astMutator.wrapAstWithNewCelBlock(CEL_BLOCK_FUNCTION, astToModify, subexpressions); + astToModify = astMutator.renumberIdsConsecutively(astToModify); + + // Tag the AST with cel.block designated as an extension + CelAbstractSyntaxTree optimizedAst = tagAstExtension(astToModify.toParsedAst()); + + 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"); + } + + 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 CelAbstractSyntaxTree tagAstExtension(CelAbstractSyntaxTree ast) { + // Tag the extension + CelSource.Builder celSourceBuilder = + ast.getSource().toBuilder().addAllExtensions(CEL_BLOCK_AST_EXTENSION_TAG); + + return CelAbstractSyntaxTree.newParsedAst(ast.getExpr(), celSourceBuilder.build()); + } + + /** + * 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 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 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++) { + CelMutableExpr subexpression = subexpressions.get(i); + + CelAbstractSyntaxTree subAst = + CelAbstractSyntaxTree.newParsedAst( + CelMutableExprConverter.fromMutableExpr(subexpression), + CelSource.newBuilder().build()); + + try { + subAst = celBuilder.build().check(subAst).getAst(); + } catch (CelValidationException e) { + throw new IllegalStateException("Failed to type-check subexpression", e); + } + + CelVarDecl indexVar = CelVarDecl.newVarDeclaration("@index" + i, subAst.getResultType()); + celBuilder.addVarDeclarations(indexVar); + varDeclBuilder.add(indexVar); + } + + return varDeclBuilder.build(); + } + + private List getCseCandidates(CelNavigableMutableAst navAst) { + if (cseOptions.subexpressionMaxRecursionDepth() > 0) { + return getCseCandidatesWithRecursionDepth( + navAst, cseOptions.subexpressionMaxRecursionDepth()); + } else { + return getCseCandidatesWithCommonSubexpr(navAst); + } + } + + /** + * 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 -> canEliminate(node, ineligibleExprs)) + .filter(node -> node.height() <= recursionLimit) + .sorted(Comparator.comparingInt(CelNavigableMutableExpr::height).reversed()) + .collect(toImmutableList()); + if (descendants.isEmpty()) { + return new ArrayList<>(); + } + + List cseCandidates = getCseCandidatesWithCommonSubexpr(descendants); + if (!cseCandidates.isEmpty()) { + return cseCandidates; + } + + // 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; + } + + // The height of the remaining subexpression is already below the recursion limit. No need to + // extract. + return new ArrayList<>(); + } + + 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 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; + } + + semanticallyEqualNodes.add(normalizedExpr); + } + + List cseCandidates = new ArrayList<>(); + if (normalizedCseCandidate == null) { + return cseCandidates; + } + + 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 cseCandidates; + } + + 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); + } + + private boolean containsComprehensionIdentInSubexpr(CelNavigableMutableExpr navExpr) { + if (navExpr.getKind().equals(Kind.COMPREHENSION)) { + return true; + } + + ImmutableList comprehensionIdents = + navExpr + .allNodes() + .filter( + node -> + node.getKind().equals(Kind.IDENT) + && (node.expr() + .ident() + .name() + .startsWith(MANGLED_COMPREHENSION_ITER_VAR_PREFIX) + || node.expr() + .ident() + .name() + .startsWith(MANGLED_COMPREHENSION_ACCU_VAR_PREFIX))) + .collect(toImmutableList()); + + if (comprehensionIdents.isEmpty()) { + return true; + } + + for (CelNavigableMutableExpr ident : comprehensionIdents) { + CelNavigableMutableExpr parent = ident.parent().orElse(null); + while (parent != null) { + if (parent.getKind().equals(Kind.COMPREHENSION)) { + return false; + } + + parent = parent.parent().orElse(null); + } + } + + return true; + } + + /** + * 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(CelNavigableMutableExpr navigableExpr) { + return navigableExpr + .allNodes() + .allMatch( + node -> { + if (node.getKind().equals(Kind.CALL)) { + return cseEliminableFunctions.contains(node.expr().call().function()); + } + + return true; + }); + } + + /** + * Converts the {@link CelMutableExpr} to make it suitable for performing a semantically equals + * check. + * + *

Specifically, this will deep copy the mutable expr then set all expr IDs in the expression + * tree to 0. + */ + private CelMutableExpr normalizeForEquality(CelMutableExpr mutableExpr) { + CelMutableExpr copiedExpr = CelMutableExpr.newInstance(mutableExpr); + + return astMutator.clearExprIds(copiedExpr); + } + + @VisibleForTesting + static CelFunctionDecl newCelBlockFunctionDecl(CelType resultType) { + return CelFunctionDecl.newFunctionDeclaration( + CEL_BLOCK_FUNCTION, + CelOverloadDecl.newGlobalOverload( + "cel_block_list", resultType, ListType.create(SimpleType.DYN), resultType)); + } + + /** Options to configure how Common Subexpression Elimination behave. */ + @AutoValue + public abstract static class SubexpressionOptimizerOptions { + public abstract int iterationLimit(); + + public abstract boolean populateMacroCalls(); + + public abstract boolean enableCelBlock(); + + public abstract int subexpressionMaxRecursionDepth(); + + public abstract ImmutableSet eliminableFunctions(); + + /** Builder for configuring the {@link SubexpressionOptimizerOptions}. */ + @AutoValue.Builder + public abstract static class Builder { + + /** + * Limit the number of iteration while performing CSE. An exception is thrown if the iteration + * count exceeds the set value. + */ + public abstract Builder iterationLimit(int value); + + /** + * Populate the macro_calls map in source_info with macro calls on the resulting optimized + * AST. + */ + public abstract Builder populateMacroCalls(boolean value); + + /** + * @deprecated This option is a no-op. cel.@block is always enabled. + */ + @Deprecated + public abstract Builder enableCelBlock(boolean value); + + /** + * Ensures all extracted subexpressions do not exceed the maximum depth of designated value. + * The purpose of this is to guarantee evaluation and deserialization safety by preventing + * deeply nested ASTs. The trade-off is increased memory usage due to memoizing additional + * block indices during lazy evaluation. + * + *

As a general note, root of a node has a depth of 0. An expression `x.y.z` has a depth of + * 2. + * + *

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 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) + *
+ * + *

+ */ + public abstract Builder subexpressionMaxRecursionDepth(int value); + + abstract ImmutableSet.Builder eliminableFunctionsBuilder(); + + /** + * Adds a collection of custom functions that will be a candidate for common subexpression + * elimination. By default, standard functions are eliminable. + * + *

Note that the implementation of custom functions must be free of side effects. + */ + @CanIgnoreReturnValue + public Builder addEliminableFunctions(Iterable functions) { + checkNotNull(functions); + this.eliminableFunctionsBuilder().addAll(functions); + return this; + } + + /** See {@link #addEliminableFunctions(Iterable)}. */ + @CanIgnoreReturnValue + public Builder addEliminableFunctions(String... functions) { + return addEliminableFunctions(Arrays.asList(functions)); + } + + public abstract SubexpressionOptimizerOptions build(); + + Builder() {} + } + + abstract Builder toBuilder(); + + /** Returns a new options builder with recommended defaults pre-configured. */ + public static Builder newBuilder() { + return new AutoValue_SubexpressionOptimizer_SubexpressionOptimizerOptions.Builder() + .iterationLimit(500) + .enableCelBlock(true) + .populateMacroCalls(false) + .subexpressionMaxRecursionDepth(0); + } + + SubexpressionOptimizerOptions() {} + } + + private SubexpressionOptimizer(SubexpressionOptimizerOptions cseOptions) { + this.cseOptions = cseOptions; + this.astMutator = AstMutator.newInstance(cseOptions.iterationLimit()); + this.cseEliminableFunctions = + ImmutableSet.builder() + .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..37641fac3 --- /dev/null +++ b/optimizer/src/test/java/dev/cel/optimizer/AstMutatorTest.java @@ -0,0 +1,999 @@ +// 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()) + .addRuntimeLibraries(CelOptionalLibrary.INSTANCE) + .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("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", "@ac", true) + .mutableAst() + .toParsedAst(); + + assertThat(mangledAst.getExpr().toString()) + .isEqualTo( + "COMPREHENSION [13] {\n" + + " iter_var: @it:0\n" + + " iter_range: {\n" + + " LIST [1] {\n" + + " elements: {\n" + + " CONSTANT [2] { value: false }\n" + + " }\n" + + " }\n" + + " }\n" + + " accu_var: @ac: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\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " loop_step: {\n" + + " CALL [11] {\n" + + " function: _||_\n" + + " args: {\n" + + " IDENT [10] {\n" + + " name: @ac:0\n" + + " }\n" + + " IDENT [5] {\n" + + " name: @it:0\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " result: {\n" + + " IDENT [12] {\n" + + " name: @ac:0\n" + + " }\n" + + " }\n" + + "}"); + assertThat(CEL_UNPARSER.unparse(mangledAst)).isEqualTo("[false].exists(@it:0, @it: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", "@ac", false) + .mutableAst() + .toParsedAst(); + + assertThat(CEL_UNPARSER.unparse(mangledAst)) + .isEqualTo( + "[1, 2, 3].map(@it:0:0, [1, 2, 3].map(@it:1:0, @it:1:0 + 1)) == " + + "[1, 2, 3].map(@it:0:0, [1, 2, 3].map(@it:1:0, @it:1:0 + 1))"); + 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", "@ac", false) + .mutableAst() + .toParsedAst(); + + assertThat(CEL_UNPARSER.unparse(mangledAst)) + .isEqualTo( + "[1, 2, 3].map(@it:0:0, [1, 2, 3].map(@it:1:0, @it:1:0)) == " + + "dyn([1u, 2u, 3u].map(@it:0:1, [1u, 2u, 3u].map(@it:1:1, @it:1: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", "@ac", true) + .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", "@ac", true) + .mutableAst() + .toParsedAst(); + + assertThat(mangledAst.getExpr().toString()) + .isEqualTo( + "COMPREHENSION [27] {\n" + + " iter_var: @it:1\n" + + " iter_range: {\n" + + " LIST [1] {\n" + + " elements: {\n" + + " IDENT [2] {\n" + + " name: x\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " accu_var: @ac:1\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\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " loop_step: {\n" + + " CALL [25] {\n" + + " function: _||_\n" + + " args: {\n" + + " IDENT [24] {\n" + + " name: @ac:1\n" + + " }\n" + + " COMPREHENSION [19] {\n" + + " iter_var: @it:0\n" + + " iter_range: {\n" + + " LIST [5] {\n" + + " elements: {\n" + + " IDENT [6] {\n" + + " name: @it:1\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " accu_var: @ac: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\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " loop_step: {\n" + + " CALL [17] {\n" + + " function: _||_\n" + + " args: {\n" + + " IDENT [16] {\n" + + " name: @ac:0\n" + + " }\n" + + " CALL [10] {\n" + + " function: _==_\n" + + " args: {\n" + + " IDENT [9] {\n" + + " name: @it:0\n" + + " }\n" + + " CONSTANT [11] { value: 1 }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " result: {\n" + + " IDENT [18] {\n" + + " name: @ac:0\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " result: {\n" + + " IDENT [26] {\n" + + " name: @ac:1\n" + + " }\n" + + " }\n" + + "}"); + + assertThat(CEL_UNPARSER.unparse(mangledAst)) + .isEqualTo("[x].exists(@it:1, [@it:1].exists(@it:0, @it: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", "@ac", true) + .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 a769bc20d..d81fba29f 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/BUILD.bazel +++ b/optimizer/src/test/java/dev/cel/optimizer/BUILD.bazel @@ -1,3 +1,4 @@ +load("@rules_java//java:defs.bzl", "java_library") load("//:testing.bzl", "junit4_test_suites") package(default_applicable_licenses = ["//:license"]) @@ -9,24 +10,32 @@ 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/navigation:mutable_navigation", "//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 65d94b70e..000000000 --- a/optimizer/src/test/java/dev/cel/optimizer/MutableAstTest.java +++ /dev/null @@ -1,443 +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 com.google.common.collect.ImmutableMap; -import com.google.testing.junit.testparameterinjector.TestParameterInjector; -import dev.cel.bundle.Cel; -import dev.cel.bundle.CelFactory; -import dev.cel.common.CelAbstractSyntaxTree; -import dev.cel.common.CelFunctionDecl; -import dev.cel.common.CelOptions; -import dev.cel.common.CelOverloadDecl; -import dev.cel.common.ast.CelConstant; -import dev.cel.common.ast.CelExpr; -import dev.cel.common.ast.CelExpr.CelIdent; -import dev.cel.common.ast.CelExpr.CelSelect; -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.CelOptionalLibrary; -import dev.cel.parser.CelStandardMacro; -import dev.cel.parser.CelUnparser; -import dev.cel.parser.CelUnparserFactory; -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) - .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(); - - @Test - public void constExpr() throws Exception { - CelAbstractSyntaxTree ast = CEL.compile("10").getAst(); - - CelAbstractSyntaxTree mutatedAst = - MutableAst.replaceSubtree( - ast, CelExpr.newBuilder().setConstant(CelConstant.ofValue(true)).build(), 1); - - assertThat(mutatedAst.getExpr()) - .isEqualTo(CelExpr.ofConstantExpr(1, CelConstant.ofValue(true))); - } - - @Test - public void mutableAst_returnsParsedAst() throws Exception { - CelAbstractSyntaxTree ast = CEL.compile("10").getAst(); - - CelAbstractSyntaxTree mutatedAst = - MutableAst.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 = - MutableAst.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().getMacroCalls()).isEmpty(); - } - - @Test - public void mutableAst_macro_sourceMacroCallsPopulated() throws Exception { - CelAbstractSyntaxTree ast = CEL.compile("has(TestAllTypes{}.single_int32)").getAst(); - - CelAbstractSyntaxTree mutatedAst = - MutableAst.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().getMacroCalls()).isNotEmpty(); - } - - @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 = - MutableAst.replaceSubtree( - ast, CelExpr.newBuilder().setConstant(CelConstant.ofValue(10)).build(), 4); - - assertThat(replacedAst.getExpr()).isEqualTo(CelExpr.ofConstantExpr(1, 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 = - MutableAst.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 = - MutableAst.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 = MutableAst.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 = - MutableAst.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 = - MutableAst.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 = - MutableAst.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 = - MutableAst.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 = - MutableAst.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 = - MutableAst.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 = - MutableAst.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 = - MutableAst.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 = - MutableAst.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 = - MutableAst.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 = - MutableAst.replaceSubtree( - ast, CelExpr.newBuilder().setConstant(CelConstant.ofValue(false)).build(), 2); - - assertThat(CEL_UNPARSER.unparse(replacedAst)).isEqualTo("[false].exists(i, i)"); - assertConsistentMacroCalls(ast); - assertThat(CEL.createProgram(CEL.check(replacedAst).getAst()).eval()).isEqualTo(false); - } - - @Test - public void comprehension_replaceAccuInit() throws Exception { - CelAbstractSyntaxTree ast = CEL.compile("[false].exists(i, i)").getAst(); - - CelAbstractSyntaxTree replacedAst = - MutableAst.replaceSubtree( - ast, CelExpr.newBuilder().setConstant(CelConstant.ofValue(true)).build(), 6); - - assertThat(CEL_UNPARSER.unparse(replacedAst)).isEqualTo("[false].exists(i, i)"); - assertConsistentMacroCalls(ast); - assertThat(CEL.createProgram(CEL.check(replacedAst).getAst()).eval()).isEqualTo(true); - } - - @Test - public void comprehension_replaceLoopStep() throws Exception { - CelAbstractSyntaxTree ast = CEL.compile("[false].exists(i, i)").getAst(); - - CelAbstractSyntaxTree replacedAst = - MutableAst.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 comprehension_astContainsDuplicateNodes() throws Exception { - CelAbstractSyntaxTree ast = CEL.compile("[{\"a\": 1}].map(i, i)").getAst(); - - // AST contains two duplicate expr (ID: 9). Just ensure that it doesn't throw. - CelAbstractSyntaxTree replacedAst = - MutableAst.replaceSubtree(ast, CelExpr.newBuilder().build(), -1); - - assertThat(CEL_UNPARSER.unparse(replacedAst)).isEqualTo("[{\"a\": 1}].map(i, i)"); - assertConsistentMacroCalls(ast); - } - - /** - * 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).isEqualTo(e); - } - }); - } - } -} 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 3e1119847..8def5de7a 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel @@ -1,3 +1,4 @@ +load("@rules_java//java:defs.bzl", "java_library") load("//:testing.bzl", "junit4_test_suites") package(default_applicable_licenses = ["//:license"]) @@ -6,29 +7,46 @@ java_library( name = "tests", testonly = 1, srcs = glob(["*.java"]), + resources = ["//optimizer/src/test/resources:baselines"], deps = [ - "//:java_truth", + # "//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/resources/testdata/proto3:test_all_types_java_proto", + "//common/ast", + "//common/navigation:mutable_navigation", "//common/types", + "//extensions", "//extensions:optional_library", "//optimizer", "//optimizer:optimization_exception", "//optimizer:optimizer_builder", + "//optimizer/optimizers:common_subexpression_elimination", "//optimizer/optimizers:constant_folding", "//parser:macro", + "//parser:operator", "//parser:unparser", - "@maven//:com_google_testparameterinjector_test_parameter_injector", + "//runtime", + "//runtime:function_binding", + "//testing:baseline_test_case", "@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", ], ) junit4_test_suites( name = "test_suites", + shard_count = 4, sizes = [ "small", + "medium", ], src_dir = "src/test/java", deps = [":tests"], 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 ed0415f39..d5d204834 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java @@ -17,21 +17,30 @@ 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 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.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; import dev.cel.optimizer.CelOptimizerFactory; +import dev.cel.optimizer.optimizers.ConstantFoldingOptimizer.ConstantFoldingOptions; 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 org.junit.Test; import org.junit.runner.RunWith; @@ -41,15 +50,34 @@ public class ConstantFoldingOptimizerTest { 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)) + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "get_true", + CelOverloadDecl.newGlobalOverload("get_true_overload", SimpleType.BOOL))) + .addFunctionBindings( + CelFunctionBinding.from("get_true_overload", ImmutableList.of(), unused -> true)) .addMessageTypes(TestAllTypes.getDescriptor()) - .setContainer("dev.cel.testing.testdata.proto3") - .addCompilerLibraries(CelOptionalLibrary.INSTANCE) - .addRuntimeLibraries(CelOptionalLibrary.INSTANCE) + .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) + .addCompilerLibraries( + CelExtensions.bindings(), + CelOptionalLibrary.INSTANCE, + CelExtensions.math(CelOptions.DEFAULT), + CelExtensions.strings(), + CelExtensions.sets(CelOptions.DEFAULT), + CelExtensions.encoders()) + .addRuntimeLibraries( + CelOptionalLibrary.INSTANCE, + CelExtensions.math(CelOptions.DEFAULT), + CelExtensions.strings(), + CelExtensions.sets(CelOptions.DEFAULT), + CelExtensions.encoders()) .build(); private static final CelOptimizer CEL_OPTIMIZER = CelOptimizerFactory.standardCelOptimizerBuilder(CEL) - .addAstOptimizers(ConstantFoldingOptimizer.INSTANCE) + .addAstOptimizers(ConstantFoldingOptimizer.getInstance()) .build(); private static final CelUnparser CEL_UNPARSER = CelUnparserFactory.newUnparser(); @@ -152,6 +180,15 @@ public class ConstantFoldingOptimizerTest { @TestParameters("{source: 'x + dyn([1, 2] + [3, 4])', expected: 'x + [1, 2, 3, 4]'}") @TestParameters( "{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'}") // 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 { @@ -191,6 +228,10 @@ 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)'}") public void constantFold_macros_macroCallMetadataPopulated(String source, String expected) throws Exception { Cel cel = @@ -200,12 +241,12 @@ public void constantFold_macros_macroCallMetadataPopulated(String source, String .addMessageTypes(TestAllTypes.getDescriptor()) .setStandardMacros(CelStandardMacro.STANDARD_MACROS) .setOptions(CelOptions.current().populateMacroCalls(true).build()) - .addCompilerLibraries(CelOptionalLibrary.INSTANCE) + .addCompilerLibraries(CelExtensions.bindings(), CelOptionalLibrary.INSTANCE) .addRuntimeLibraries(CelOptionalLibrary.INSTANCE) .build(); CelOptimizer celOptimizer = CelOptimizerFactory.standardCelOptimizerBuilder(cel) - .addAstOptimizers(ConstantFoldingOptimizer.INSTANCE) + .addAstOptimizers(ConstantFoldingOptimizer.getInstance()) .build(); CelAbstractSyntaxTree ast = cel.compile(source).getAst(); @@ -234,6 +275,8 @@ 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() @@ -242,12 +285,12 @@ public void constantFold_macros_withoutMacroCallMetadata(String source) throws E .addMessageTypes(TestAllTypes.getDescriptor()) .setStandardMacros(CelStandardMacro.STANDARD_MACROS) .setOptions(CelOptions.current().populateMacroCalls(false).build()) - .addCompilerLibraries(CelOptionalLibrary.INSTANCE) + .addCompilerLibraries(CelExtensions.bindings(), CelOptionalLibrary.INSTANCE) .addRuntimeLibraries(CelOptionalLibrary.INSTANCE) .build(); CelOptimizer celOptimizer = CelOptimizerFactory.standardCelOptimizerBuilder(cel) - .addAstOptimizers(ConstantFoldingOptimizer.INSTANCE) + .addAstOptimizers(ConstantFoldingOptimizer.getInstance()) .build(); CelAbstractSyntaxTree ast = cel.compile(source).getAst(); @@ -274,6 +317,8 @@ public void constantFold_macros_withoutMacroCallMetadata(String source) throws E @TestParameters("{source: '[optional.none()]'}") @TestParameters("{source: '[?x.?y]'}") @TestParameters("{source: 'TestAllTypes{single_int32: x, repeated_int32: [1, 2, 3]}'}") + @TestParameters("{source: 'get_true() == get_true()'}") + @TestParameters("{source: 'get_true() == true'}") public void constantFold_noOp(String source) throws Exception { CelAbstractSyntaxTree ast = CEL.compile(source).getAst(); @@ -283,20 +328,129 @@ public void constantFold_noOp(String source) throws Exception { } @Test - public void maxIterationCountReached_throws() throws Exception { + 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_withMacroCallPopulated_comprehensionsAreReplacedWithNotSet() + throws Exception { + Cel cel = + CelFactory.standardCelBuilder() + .addVar("x", SimpleType.DYN) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .setOptions(CelOptions.current().populateMacroCalls(true).build()) + .build(); + CelOptimizer celOptimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(cel) + .addAstOptimizers(ConstantFoldingOptimizer.getInstance()) + .build(); + CelAbstractSyntaxTree ast = + cel.compile("[1, 1 + 1, 1 + 1+ 1].map(i, i).filter(j, j % 2 == x)").getAst(); + + CelAbstractSyntaxTree optimizedAst = celOptimizer.optimize(ast); + + assertThat(CEL_UNPARSER.unparse(optimizedAst)) + .isEqualTo("[1, 2, 3].map(i, i).filter(j, j % 2 == x)"); + assertThat(optimizedAst.getSource().getMacroCalls()).hasSize(2); + assertThat(optimizedAst.getSource().getMacroCalls().get(1L).toString()) + .isEqualTo( + "CALL [0] {\n" + + " function: filter\n" + + " target: {\n" + + " NOT_SET [2] {}\n" + + " }\n" + + " args: {\n" + + " IDENT [25] {\n" + + " name: j\n" + + " }\n" + + " CALL [17] {\n" + + " function: _==_\n" + + " args: {\n" + + " CALL [18] {\n" + + " function: _%_\n" + + " args: {\n" + + " IDENT [19] {\n" + + " name: j\n" + + " }\n" + + " CONSTANT [20] { value: 2 }\n" + + " }\n" + + " }\n" + + " IDENT [21] {\n" + + " name: x\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + "}"); + assertThat(optimizedAst.getSource().getMacroCalls().get(2L).toString()) + .isEqualTo( + "CALL [0] {\n" + + " function: map\n" + + " target: {\n" + + " LIST [3] {\n" + + " elements: {\n" + + " CONSTANT [4] { value: 1 }\n" + + " CONSTANT [5] { value: 2 }\n" + + " CONSTANT [6] { value: 3 }\n" + + " }\n" + + " }\n" + + " }\n" + + " args: {\n" + + " IDENT [28] {\n" + + " name: i\n" + + " }\n" + + " IDENT [12] {\n" + + " name: i\n" + + " }\n" + + " }\n" + + "}"); + } + + @Test + public void constantFold_astProducesConsistentlyNumberedIds() throws Exception { + CelAbstractSyntaxTree ast = CEL.compile("[1] + [2] + [3]").getAst(); + + CelAbstractSyntaxTree optimizedAst = CEL_OPTIMIZER.optimize(ast); + + assertThat(optimizedAst.getExpr().toString()) + .isEqualTo( + "LIST [1] {\n" + + " elements: {\n" + + " CONSTANT [2] { value: 1 }\n" + + " CONSTANT [3] { value: 2 }\n" + + " CONSTANT [4] { value: 3 }\n" + + " }\n" + + "}"); + } + + @Test + public void iterationLimitReached_throws() throws Exception { StringBuilder sb = new StringBuilder(); sb.append("0"); - for (int i = 1; i < 400; i++) { + for (int i = 1; i < 200; i++) { sb.append(" + ").append(i); - } // 0 + 1 + 2 + 3 + ... 400 + } // 0 + 1 + 2 + 3 + ... 200 Cel cel = CelFactory.standardCelBuilder() - .setOptions(CelOptions.current().maxParseRecursionDepth(400).build()) + .setOptions(CelOptions.current().maxParseRecursionDepth(200).build()) .build(); CelAbstractSyntaxTree ast = cel.compile(sb.toString()).getAst(); CelOptimizer optimizer = CelOptimizerFactory.standardCelOptimizerBuilder(cel) - .addAstOptimizers(ConstantFoldingOptimizer.INSTANCE) + .addAstOptimizers( + ConstantFoldingOptimizer.newInstance( + ConstantFoldingOptions.newBuilder().maxIterationLimit(200).build())) .build(); CelOptimizationException e = diff --git a/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerBaselineTest.java b/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerBaselineTest.java new file mode 100644 index 000000000..0b8a1e3f3 --- /dev/null +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerBaselineTest.java @@ -0,0 +1,529 @@ +// 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.truth.Truth.assertThat; +import static dev.cel.common.CelOverloadDecl.newGlobalOverload; + +import com.google.common.base.Ascii; +import com.google.common.collect.ImmutableMap; +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.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.CelFunctionBinding; +import dev.cel.testing.BaselineTestCase; +import java.util.Optional; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +// @MediumTest +@RunWith(TestParameterInjector.class) +public class SubexpressionOptimizerBaselineTest extends BaselineTestCase { + private static final CelUnparser CEL_UNPARSER = CelUnparserFactory.newUnparser(); + private static final TestAllTypes TEST_ALL_TYPES_INPUT = + TestAllTypes.newBuilder() + .setSingleInt64(3L) + .setSingleInt32(5) + .setOneofType( + NestedTestAllTypes.newBuilder() + .setPayload( + TestAllTypes.newBuilder() + .setSingleInt32(8) + .setSingleInt64(10L) + .putMapInt32Int64(0, 1) + .putMapInt32Int64(1, 5) + .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) + .addEliminableFunctions("pure_custom_func") + .build(); + + private String overriddenBaseFilePath = ""; + + @Before + public void setUp() { + overriddenBaseFilePath = ""; + } + + @Override + protected String baselineFileName() { + if (overriddenBaseFilePath.isEmpty()) { + return super.baselineFileName(); + } + return overriddenBaseFilePath; + } + + @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 optimizedAst = cseTestOptimizer.cseOptimizer.optimize(ast); + + Object optimizedEvalResult = + CEL.createProgram(optimizedAst) + .eval(ImmutableMap.of("msg", TEST_ALL_TYPES_INPUT, "x", 5L, "opt_x", Optional.of(5L))); + assertThat(optimizedEvalResult).isEqualTo(expectedEvalResult); + } + + @Test + public void subexpression_unparsed() throws Exception { + for (CseTestCase cseTestCase : CseTestCase.values()) { + 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 : CseTestOptimizer.values()) { + String optimizerName = cseTestOptimizer.name(); + CelAbstractSyntaxTree optimizedAst = cseTestOptimizer.cseOptimizer.optimize(ast); + if (!resultPrinted) { + Object optimizedEvalResult = + CEL.createProgram(optimizedAst) + .eval( + ImmutableMap.of( + "msg", TEST_ALL_TYPES_INPUT, "x", 5L, "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 : CseTestCase.values()) { + 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 : CseTestOptimizer.values()) { + String optimizerName = cseTestOptimizer.name(); + CelAbstractSyntaxTree optimizedAst = + cseTestOptimizer.cseWithConstFoldingOptimizer.optimize(ast); + if (!resultPrinted) { + Object optimizedEvalResult = + CEL.createProgram(optimizedAst) + .eval( + ImmutableMap.of( + "msg", TEST_ALL_TYPES_INPUT, "x", 5L, "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 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()) { + testOutput().println("Test case: " + cseTestCase.name()); + testOutput().println("Source: " + cseTestCase.source); + testOutput().println("=====>"); + CelAbstractSyntaxTree ast = CEL.compile(cseTestCase.source).getAst(); + CelAbstractSyntaxTree optimizedAst = cseTestOptimizer.cseOptimizer.optimize(ast); + testOutput().println(optimizedAst.getExpr()); + } + } + + @Test + public void large_expressions_block_common_subexpr() throws Exception { + CelOptimizer celOptimizer = + newCseOptimizer( + CEL, SubexpressionOptimizerOptions.newBuilder().populateMacroCalls(true).build()); + + runLargeTestCases(celOptimizer); + } + + @Test + public void large_expressions_block_recursion_depth_1() throws Exception { + CelOptimizer celOptimizer = + newCseOptimizer( + CEL, + SubexpressionOptimizerOptions.newBuilder() + .populateMacroCalls(true) + .subexpressionMaxRecursionDepth(1) + .build()); + + runLargeTestCases(celOptimizer); + } + + @Test + public void large_expressions_block_recursion_depth_2() throws Exception { + CelOptimizer celOptimizer = + newCseOptimizer( + CEL, + SubexpressionOptimizerOptions.newBuilder() + .populateMacroCalls(true) + .subexpressionMaxRecursionDepth(2) + .build()); + + runLargeTestCases(celOptimizer); + } + + @Test + public void large_expressions_block_recursion_depth_3() throws Exception { + CelOptimizer celOptimizer = + newCseOptimizer( + CEL, + SubexpressionOptimizerOptions.newBuilder() + .populateMacroCalls(true) + .subexpressionMaxRecursionDepth(3) + .build()); + + runLargeTestCases(celOptimizer); + } + + private void runLargeTestCases(CelOptimizer celOptimizer) throws Exception { + for (CseLargeTestCase cseTestCase : CseLargeTestCase.values()) { + testOutput().println("Test case: " + cseTestCase.name()); + testOutput().println("Source: " + cseTestCase.source); + testOutput().println("=====>"); + CelAbstractSyntaxTree ast = CEL.compile(cseTestCase.source).getAst(); + + CelAbstractSyntaxTree optimizedAst = celOptimizer.optimize(ast); + Object optimizedEvalResult = + CEL.createProgram(optimizedAst) + .eval( + ImmutableMap.of("msg", TEST_ALL_TYPES_INPUT, "x", 5L, "opt_x", Optional.of(5L))); + testOutput().println("Result: " + optimizedEvalResult); + try { + testOutput().printf("Unparsed: %s", CEL_UNPARSER.unparse(optimizedAst)); + } catch (RuntimeException e) { + testOutput().printf("Unparse Error: %s", e); + } + testOutput().println(); + testOutput().println(); + } + } + + private static CelBuilder newCelBuilder() { + return CelFactory.standardCelBuilder() + .addMessageTypes(TestAllTypes.getDescriptor()) + .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .setOptions( + CelOptions.current().enableTimestampEpoch(true).populateMacroCalls(true).build()) + .addCompilerLibraries(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)) + .build(); + } + + @SuppressWarnings("Immutable") // Test only + private enum CseTestOptimizer { + BLOCK_COMMON_SUBEXPR_ONLY(OPTIMIZER_COMMON_OPTIONS), + BLOCK_RECURSION_DEPTH_1( + OPTIMIZER_COMMON_OPTIONS.toBuilder().subexpressionMaxRecursionDepth(1).build()), + BLOCK_RECURSION_DEPTH_2( + OPTIMIZER_COMMON_OPTIONS.toBuilder().subexpressionMaxRecursionDepth(2).build()), + BLOCK_RECURSION_DEPTH_3( + OPTIMIZER_COMMON_OPTIONS.toBuilder().subexpressionMaxRecursionDepth(3).build()), + BLOCK_RECURSION_DEPTH_4( + OPTIMIZER_COMMON_OPTIONS.toBuilder().subexpressionMaxRecursionDepth(4).build()), + BLOCK_RECURSION_DEPTH_5( + OPTIMIZER_COMMON_OPTIONS.toBuilder().subexpressionMaxRecursionDepth(5).build()), + BLOCK_RECURSION_DEPTH_6( + OPTIMIZER_COMMON_OPTIONS.toBuilder().subexpressionMaxRecursionDepth(6).build()), + BLOCK_RECURSION_DEPTH_7( + OPTIMIZER_COMMON_OPTIONS.toBuilder().subexpressionMaxRecursionDepth(7).build()), + BLOCK_RECURSION_DEPTH_8( + OPTIMIZER_COMMON_OPTIONS.toBuilder().subexpressionMaxRecursionDepth(8).build()), + BLOCK_RECURSION_DEPTH_9( + OPTIMIZER_COMMON_OPTIONS.toBuilder().subexpressionMaxRecursionDepth(9).build()); + + private final CelOptimizer cseOptimizer; + private final CelOptimizer cseWithConstFoldingOptimizer; + + CseTestOptimizer(SubexpressionOptimizerOptions option) { + this.cseOptimizer = newCseOptimizer(CEL, option); + this.cseWithConstFoldingOptimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(CEL) + .addAstOptimizers( + ConstantFoldingOptimizer.getInstance(), + SubexpressionOptimizer.newInstance(option)) + .build(); + } + } + + private enum CseTestCase { + SIZE_1("size([1,2]) + size([1,2]) + 1 == 5"), + SIZE_2("2 + size([1,2]) + size([1,2]) + 1 == 7"), + SIZE_3("size([0]) + size([0]) + size([1,2]) + size([1,2]) == 6"), + SIZE_4( + "5 + size([0]) + size([0]) + size([1,2]) + size([1,2]) + " + + "size([1,2,3]) + size([1,2,3]) == 17"), + TIMESTAMP( + "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"), + MAP_INDEX("{\"a\": 2}[\"a\"] + {\"a\": 2}[\"a\"] * {\"a\": 2}[\"a\"] == 6"), + /** + * Input map is: + * + *

{@code
+     * {
+     *    "a": { "b": 1 },
+     *    "c": { "b": 1 },
+     *    "d": {
+     *       "e": { "b": 1 }
+     *    },
+     *    "e":{
+     *       "e": { "b": 1 }
+     *    }
+     * }
+     * }
+ */ + NESTED_MAP_CONSTRUCTION( + "{'a': {'b': 1}, 'c': {'b': 1}, 'd': {'e': {'b': 1}}, 'e': {'e': {'b': 1}}}"), + NESTED_LIST_CONSTRUCTION( + "[1, [1,2,3,4], 2, [1,2,3,4], 5, [1,2,3,4], 7, [[1,2], [1,2,3,4]], [1,2]]"), + SELECT("msg.single_int64 + msg.single_int64 == 6"), + SELECT_NESTED_1( + "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"), + SELECT_NESTED_2( + "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"), + SELECT_NESTED_MESSAGE_MAP_INDEX_1( + "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"), + SELECT_NESTED_MESSAGE_MAP_INDEX_2( + "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"), + SELECT_NESTED_NO_COMMON_SUBEXPR( + "msg.oneof_type.payload.oneof_type.payload.oneof_type.payload.oneof_type.payload.single_int64"), + TERNARY("(msg.single_int64 > 0 ? msg.single_int64 : 0) == 3"), + TERNARY_BIND_RHS_ONLY( + "false ? false : (msg.single_int64) + ((msg.single_int64 + 1) * 2) == 11"), + NESTED_TERNARY( + "(msg.single_int64 > 0 ? (msg.single_int32 > 0 ? " + + "msg.single_int64 + msg.single_int32 : 0) : 0) == 8"), + MULTIPLE_MACROS_1( + // Note that all of these have different iteration variables, but they are still logically + // the same. + "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"), + 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)"), + 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]]"), + 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))"), + 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_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])"), + 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( + "(has(msg.oneof_type.payload) ? msg.oneof_type.payload.single_int64 :" + + " msg.oneof_type.payload.single_int64 * 0) == 10"), + PRESENCE_TEST_WITH_TERNARY_3( + "(has(msg.oneof_type.payload.single_int64) ? msg.oneof_type.payload.single_int64 :" + + " msg.oneof_type.payload.single_int64 * 0) == 10"), + /** + * Input: + * + *
{@code
+     * (
+     *   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
+     * }
+ */ + PRESENCE_TEST_WITH_TERNARY_NESTED( + "(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"), + OPTIONAL_LIST( + "[10, ?optional.none(), [?optional.none(), ?opt_x], [?optional.none(), ?opt_x]] == [10," + + " [5], [5]]"), + OPTIONAL_MAP( + "{?'hello': optional.of('hello')}['hello'] + {?'hello': optional.of('hello')}['hello'] ==" + + " 'hellohello'"), + OPTIONAL_MAP_CHAINED( + "{?'key': optional.of('test')}[?'bogus'].or({'key': 'test'}[?'bogus']).orValue({'key':" + + " 'test'}['key']) == 'test'"), + OPTIONAL_MESSAGE( + "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("('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('h' + 'e' + 'l' + 'l' + 'o')"), + CALL_ARGUMENT_NESTED_NO_COMMON_SUBEXPR("'hello world'.matches('h' + 'e' + 'l' + 'l' + 'o')"), + CALL_TARGET_NESTED_NO_COMMON_SUBEXPR( + "('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('hello')"), + CALL_BOTH_ARGUMENT_TARGET_NESTED_NO_COMMON_SUBEXPR( + "('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('w' + 'o' + 'r' + 'l' + 'd')"), + CUSTOM_FUNCTION_INELIMINABLE( + "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)"), + CUSTOM_FUNCTION_ELIMINABLE( + "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)"); + private final String source; + + CseTestCase(String source) { + this.source = source; + } + } + + private enum CseLargeTestCase { + CALC_FOUR_COMMON_SUBEXPR( + "[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]"), + CALC_ALL_COMMON_SUBEXPR( + "[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]"), + NESTED_MACROS( + "[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]))))))))"); + ; + + private final String source; + + CseLargeTestCase(String source) { + this.source = source; + } + } +} diff --git a/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerTest.java b/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerTest.java new file mode 100644 index 000000000..c6999e46b --- /dev/null +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerTest.java @@ -0,0 +1,586 @@ +// 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.truth.Truth.assertThat; +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; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import com.google.testing.junit.testparameterinjector.TestParameters; +import dev.cel.bundle.Cel; +import dev.cel.bundle.CelBuilder; +import dev.cel.bundle.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; +import dev.cel.common.CelSource.Extension.Component; +import dev.cel.common.CelSource.Extension.Version; +import dev.cel.common.CelValidationException; +import dev.cel.common.CelVarDecl; +import dev.cel.common.ast.CelExpr.ExprKind.Kind; +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.optimizers.SubexpressionOptimizer.SubexpressionOptimizerOptions; +import dev.cel.parser.CelStandardMacro; +import dev.cel.parser.CelUnparser; +import dev.cel.parser.CelUnparserFactory; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.CelRuntime; +import dev.cel.runtime.CelRuntimeFactory; +import java.util.concurrent.atomic.AtomicInteger; +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 final CelUnparser CEL_UNPARSER = CelUnparserFactory.newUnparser(); + + private static CelBuilder newCelBuilder() { + return CelFactory.standardCelBuilder() + .addMessageTypes(TestAllTypes.getDescriptor()) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .setOptions( + CelOptions.current().enableTimestampEpoch(true).populateMacroCalls(true).build()) + .addCompilerLibraries(CelExtensions.bindings()) + .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())); + } + + private static CelOptimizer newCseOptimizer(SubexpressionOptimizerOptions options) { + return CelOptimizerFactory.standardCelOptimizerBuilder(CEL) + .addAstOptimizers(SubexpressionOptimizer.newInstance(options)) + .build(); + } + + @Test + public void cse_resultTypeSet_celBlockOptimizationSuccess() throws Exception { + Cel cel = newCelBuilder().setResultType(SimpleType.BOOL).build(); + CelOptimizer celOptimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(cel) + .addAstOptimizers( + SubexpressionOptimizer.newInstance( + SubexpressionOptimizerOptions.newBuilder().build())) + .build(); + CelAbstractSyntaxTree ast = CEL.compile("size('a') + size('a') == 2").getAst(); + + CelAbstractSyntaxTree optimizedAst = celOptimizer.optimize(ast); + + assertThat(CEL.createProgram(optimizedAst).eval()).isEqualTo(true); + assertThat(CEL_UNPARSER.unparse(optimizedAst)) + .isEqualTo("cel.@block([size(\"a\")], @index0 + @index0 == 2)"); + } + + private enum CseNoOpTestCase { + // Nothing to optimize + NO_COMMON_SUBEXPR("size(\"hello\")"), + // Constants and identifiers + INT_CONST_ONLY("2 + 2 + 2 + 2"), + IDENT_ONLY("x + x + x + x"), + BOOL_CONST_ONLY("true == true && false == false"), + // Constants and identifiers within a function + CONST_WITHIN_FUNCTION("size(\"hello\" + \"hello\" + \"hello\")"), + IDENT_WITHIN_FUNCTION("string(x + x + x)"), + // Non-standard functions that have not been explicitly added as a candidate are not + // optimized. + NON_STANDARD_FUNCTION_1("non_pure_custom_func(1) + non_pure_custom_func(1)"), + NON_STANDARD_FUNCTION_2("1 + non_pure_custom_func(1) + 1 + non_pure_custom_func(1)"), + // Duplicated but nested calls. + 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"), + MACRO("[1, 2, 3].exists(x, x > 0)"); + + private final String source; + + CseNoOpTestCase(String source) { + this.source = source; + } + } + + @Test + public void cse_withCelBind_noop(@TestParameter CseNoOpTestCase testCase) throws Exception { + CelAbstractSyntaxTree ast = CEL.compile(testCase.source).getAst(); + + CelAbstractSyntaxTree optimizedAst = + 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_withCelBlock_noop(@TestParameter CseNoOpTestCase testCase) throws Exception { + CelAbstractSyntaxTree ast = CEL.compile(testCase.source).getAst(); + + CelAbstractSyntaxTree optimizedAst = + 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") + .getAst(); + CelOptimizer optimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(CEL) + .addAstOptimizers( + SubexpressionOptimizer.newInstance( + SubexpressionOptimizerOptions.newBuilder().build()), + ConstantFoldingOptimizer.getInstance()) + .build(); + + CelAbstractSyntaxTree optimizedAst = optimizer.optimize(ast); + + assertThat(CEL_UNPARSER.unparse(optimizedAst)) + .isEqualTo("cel.@block([1, 2], @index0 + @index0 + @index1 + @index1 + x)"); + } + + @Test + public void cse_applyConstFoldingAfter_nothingToFold() throws Exception { + CelAbstractSyntaxTree ast = CEL.compile("size(x) + size(x)").getAst(); + CelOptimizer optimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(CEL) + .addAstOptimizers( + SubexpressionOptimizer.newInstance( + SubexpressionOptimizerOptions.newBuilder().populateMacroCalls(true).build()), + ConstantFoldingOptimizer.getInstance()) + .build(); + + CelAbstractSyntaxTree optimizedAst = optimizer.optimize(ast); + + assertThat(CEL_UNPARSER.unparse(optimizedAst)) + .isEqualTo("cel.@block([size(x)], @index0 + @index0)"); + } + + @Test + public void iterationLimitReached_throws() throws Exception { + StringBuilder largeExprBuilder = new StringBuilder(); + int iterationLimit = 100; + for (int i = 0; i < iterationLimit; i++) { + largeExprBuilder.append("[1,2]"); + if (i < iterationLimit - 1) { + largeExprBuilder.append("+"); + } + } + CelAbstractSyntaxTree ast = CEL.compile(largeExprBuilder.toString()).getAst(); + + CelOptimizationException e = + assertThrows( + CelOptimizationException.class, + () -> + newCseOptimizer( + SubexpressionOptimizerOptions.newBuilder() + .iterationLimit(iterationLimit) + .build()) + .optimize(ast)); + assertThat(e).hasMessageThat().isEqualTo("Optimization failure: Max iteration count reached."); + } + + @Test + public void celBlock_astExtensionTagged() throws Exception { + CelAbstractSyntaxTree ast = CEL.compile("size(x) + size(x)").getAst(); + CelOptimizer optimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(CEL) + .addAstOptimizers( + SubexpressionOptimizer.newInstance( + SubexpressionOptimizerOptions.newBuilder().populateMacroCalls(true).build()), + ConstantFoldingOptimizer.getInstance()) + .build(); + + CelAbstractSyntaxTree optimizedAst = optimizer.optimize(ast); + + assertThat(optimizedAst.getSource().getExtensions()) + .containsExactly( + Extension.create("cel_block", Version.of(1L, 1L), Component.COMPONENT_RUNTIME)); + } + + private enum BlockTestCase { + BOOL_LITERAL("cel.block([true, false], index0 || index1)"), + STRING_CONCAT("cel.block(['a' + 'b', index0 + 'c'], index1 + 'd') == 'abcd'"), + + BLOCK_WITH_EXISTS_TRUE("cel.block([[1, 2, 3], [3, 4, 5].exists(e, e in index0)], index1)"), + BLOCK_WITH_EXISTS_FALSE("cel.block([[1, 2, 3], ![4, 5].exists(e, e in index0)], index1)"), + ; + + private final String source; + + BlockTestCase(String source) { + this.source = source; + } + } + + @Test + public void block_success(@TestParameter BlockTestCase testCase) throws Exception { + CelAbstractSyntaxTree ast = compileUsingInternalFunctions(testCase.source); + + Object evaluatedResult = CEL_FOR_EVALUATING_BLOCK.createProgram(ast).eval(); + + assertThat(evaluatedResult).isNotNull(); + } + + @Test + @SuppressWarnings("Immutable") // Test only + public void lazyEval_blockIndexNeverReferenced() 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()], has(msg.single_int64) ? index0 : false)"); + + boolean result = + (boolean) + celRuntime + .createProgram(ast) + .eval(ImmutableMap.of("msg", TestAllTypes.getDefaultInstance())); + + assertThat(result).isFalse(); + assertThat(invocation.get()).isEqualTo(0); + } + + @Test + @SuppressWarnings("Immutable") // Test only + public void lazyEval_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()], index0 && index0 && index0)"); + + boolean result = (boolean) celRuntime.createProgram(ast).eval(); + + assertThat(result).isTrue(); + assertThat(invocation.get()).isEqualTo(1); + } + + @Test + @SuppressWarnings("Immutable") // Test only + public void lazyEval_multipleBlockIndices_inResultExpr() 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(), get_true(), get_true()], index0 && index0 && index1 && index1" + + " && index2 && index2)"); + + boolean result = (boolean) celRuntime.createProgram(ast).eval(); + + assertThat(result).isTrue(); + assertThat(invocation.get()).isEqualTo(3); + } + + @Test + @SuppressWarnings("Immutable") // Test only + public void lazyEval_multipleBlockIndices_cascaded() 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(), index0, index1], index2)"); + + boolean result = (boolean) celRuntime.createProgram(ast).eval(); + + assertThat(result).isTrue(); + assertThat(invocation.get()).isEqualTo(1); + } + + @Test + @SuppressWarnings("Immutable") // Test only + public void lazyEval_nestedComprehension_indexReferencedInNestedScopes() 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(); + // 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]]]"); + + boolean result = (boolean) celRuntime.createProgram(ast).eval(); + + assertThat(result).isTrue(); + // Even though the function get_true() is referenced across different comprehension scopes, + // it still gets memoized only once. + assertThat(invocation.get()).isEqualTo(1); + } + + @Test + @TestParameters("{source: 'cel.block([])'}") + @TestParameters("{source: 'cel.block([1])'}") + @TestParameters("{source: 'cel.block(1, 2)'}") + @TestParameters("{source: 'cel.block(1, [1])'}") + public void block_invalidArguments_throws(String source) { + CelValidationException e = + assertThrows(CelValidationException.class, () -> compileUsingInternalFunctions(source)); + + assertThat(e).hasMessageThat().contains("found no matching overload for 'cel.block'"); + } + + @Test + public void blockIndex_invalidArgument_throws() { + CelValidationException e = + assertThrows( + CelValidationException.class, + () -> compileUsingInternalFunctions("cel.block([1], index)")); + + 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"); + } + + /** + * Converts AST containing cel.block related test functions to internal functions (e.g: cel.block + * -> cel.@block) + */ + private static CelAbstractSyntaxTree compileUsingInternalFunctions(String expression) + throws CelValidationException { + CelAbstractSyntaxTree astToModify = CEL_FOR_EVALUATING_BLOCK.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); + }); + + return CEL_FOR_EVALUATING_BLOCK.check(mutableAst.toParsedAst()).getAst(); + } +} diff --git a/optimizer/src/test/resources/BUILD.bazel b/optimizer/src/test/resources/BUILD.bazel new file mode 100644 index 000000000..10fb339f9 --- /dev/null +++ b/optimizer/src/test/resources/BUILD.bazel @@ -0,0 +1,15 @@ +package( + default_applicable_licenses = [ + "//:license", + ], + default_testonly = True, + default_visibility = [ + "//optimizer:__subpackages__", + ], +) + +filegroup( + name = "baselines", + testonly = True, + srcs = glob(["*.baseline"]), +) 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..0d3f25bcf --- /dev/null +++ b/optimizer/src/test/resources/constfold_before_subexpression_unparsed.baseline @@ -0,0 +1,659 @@ +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: 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: 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: 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_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: 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_block_common_subexpr.baseline b/optimizer/src/test/resources/large_expressions_block_common_subexpr.baseline new file mode 100644 index 000000000..87623446d --- /dev/null +++ b/optimizer/src/test/resources/large_expressions_block_common_subexpr.baseline @@ -0,0 +1,17 @@ +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 + @index0 + @index1 + @index2 + @index3 + @index0 + @index1 + @index2 + @index3 + @index0 + @index1 + @index2 + @index3 + @index0 + @index1 + @index2 + @index3 + @index0 + @index1 + @index2 + @index3 + @index0 + @index1 + @index2 + @index3 + @index0 + @index1 + @index2 + @index3 + @index0 + @index1 + @index2 + @index3 + @index0 + @index1 + @index2 + @index3 + @index0 + @index1 + @index2 + @index3 + @index0 + @index1 + @index2 + @index3 + @index0 + @index1 + @index2 + @index3 + @index0 + @index1 + @index2 + @index3 + @index0 + @index1 + @index2 + @index3 + @index0 + @index1 + @index2 + @index3 + @index0 + @index1 + @index2 + @index3 + @index0 + @index1 + @index2 + @index3 + @index0 + @index1 + @index2 + @index3 + @index0 + @index1 + @index2 + @index3 + @index0 + @index1 + @index2 + @index3 + @index0 + @index1 + @index2 + @index3 + @index0 + @index1 + @index2 + @index3 + @index0 + @index1 + @index2 + @index3 + @index0 + @index1 + @index2 + @index3 + @index0 + @index1 + @index2 + @index3 + @index0 + @index1 + @index2 + @index3 + @index0 + @index1 + @index2 + @index3 + @index0 + @index1 + @index2 + @index3 + @index0 + @index1 + @index2 + @index3 + @index0 + @index1 + @index2 + @index3 + @index0 + @index1 + @index2 + @index3 + @index0 + @index1 + @index2 + @index3 + @index0 + @index1 + @index2 + @index3 + @index0 + @index1 + @index2 + @index3 + @index0 + @index1 + @index2 + @index3 + @index0 + @index1 + @index2 + @index3 + @index0 + @index1 + @index2 + @index3 + @index0 + @index1 + @index2 + @index3 + @index0 + @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 + @index2 + @index2 + @index3 + @index3 + @index4 + @index4 + @index5 + @index5 + @index6 + @index6 + @index7 + @index7 + @index8 + @index8 + @index9 + @index9 + @index10 + @index10 + @index11 + @index11 + @index12 + @index12 + @index13 + @index13 + @index14 + @index14 + @index15 + @index15 + @index16 + @index16 + @index17 + @index17 + @index18 + @index18 + @index19 + @index19 + @index20 + @index20 + @index21 + @index21 + @index22 + @index22 + @index23 + @index23 + @index24 + @index24 + @index25 + @index25 + @index26 + @index26 + @index27 + @index27 + @index28 + @index28 + @index29 + @index29 + @index30 + @index30 + @index31 + @index31 + @index32 + @index32 + @index33 + @index33 + @index34 + @index34 + @index35 + @index35 + @index36 + @index36 + @index37 + @index37 + @index38 + @index38 + @index39 + @index39 + @index40 + @index40 + @index41 + @index41 + @index42 + @index42 + @index43 + @index43 + @index44 + @index44 + @index45 + @index45 + @index46 + @index46 + @index47 + @index47 + @index48 + @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(@it:0:0, @index0.map(@it:1:0, @index0.map(@it:2:0, @index0.map(@it:3:0, @index0.map(@it:4:0, @index0.map(@it:5:0, @index0.map(@it:6:0, @index0.map(@it:7: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 new file mode 100644 index 000000000..57e109c22 --- /dev/null +++ b/optimizer/src/test/resources/large_expressions_block_recursion_depth_1.baseline @@ -0,0 +1,17 @@ +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, @index4 + @index2, @index5 + @index3, @index6 + @index0, @index7 + @index1, @index8 + @index2, @index9 + @index3, @index10 + @index0, @index11 + @index1, @index12 + @index2, @index13 + @index3, @index14 + @index0, @index15 + @index1, @index16 + @index2, @index17 + @index3, @index18 + @index0, @index19 + @index1, @index20 + @index2, @index21 + @index3, @index22 + @index0, @index23 + @index1, @index24 + @index2, @index25 + @index3, @index26 + @index0, @index27 + @index1, @index28 + @index2, @index29 + @index3, @index30 + @index0, @index31 + @index1, @index32 + @index2, @index33 + @index3, @index34 + @index0, @index35 + @index1, @index36 + @index2, @index37 + @index3, @index38 + @index0, @index39 + @index1, @index40 + @index2, @index41 + @index3, @index42 + @index0, @index43 + @index1, @index44 + @index2, @index45 + @index3, @index46 + @index0, @index47 + @index1, @index48 + @index2, @index49 + @index3, @index50 + @index0, @index51 + @index1, @index52 + @index2, @index53 + @index3, @index54 + @index0, @index55 + @index1, @index56 + @index2, @index57 + @index3, @index58 + @index0, @index59 + @index1, @index60 + @index2, @index61 + @index3, @index62 + @index0, @index63 + @index1, @index64 + @index2, @index65 + @index3, @index66 + @index0, @index67 + @index1, @index68 + @index2, @index69 + @index3, @index70 + @index0, @index71 + @index1, @index72 + @index2, @index73 + @index3, @index74 + @index0, @index75 + @index1, @index76 + @index2, @index77 + @index3, @index78 + @index0, @index79 + @index1, @index80 + @index2, @index81 + @index3, @index82 + @index0, @index83 + @index1, @index84 + @index2, @index85 + @index3, @index86 + @index0, @index87 + @index1, @index88 + @index2, @index89 + @index3, @index90 + @index0, @index91 + @index1, @index92 + @index2, @index93 + @index3, @index94 + @index0, @index95 + @index1, @index96 + @index2, @index97 + @index3, @index98 + @index0, @index99 + @index1, @index100 + @index2, @index101 + @index3, @index102 + @index0, @index103 + @index1, @index104 + @index2, @index105 + @index3, @index106 + @index0, @index107 + @index1, @index108 + @index2, @index109 + @index3, @index110 + @index0, @index111 + @index1, @index112 + @index2, @index113 + @index3, @index114 + @index0, @index115 + @index1, @index116 + @index2, @index117 + @index3, @index118 + @index0, @index119 + @index1, @index120 + @index2, @index121 + @index3, @index122 + @index0, @index123 + @index1, @index124 + @index2, @index125 + @index3, @index126 + @index0, @index127 + @index1, @index128 + @index2, @index129 + @index3, @index130 + @index0, @index131 + @index1, @index132 + @index2, @index133 + @index3, @index134 + @index0, @index135 + @index1, @index136 + @index2, @index137 + @index3, @index138 + @index0, @index139 + @index1, @index140 + @index2, @index141 + @index3, @index142 + @index0, @index143 + @index1, @index144 + @index2, @index145 + @index3, @index146 + @index0, @index147 + @index1, @index148 + @index2, @index149 + @index3, @index150 + @index0, @index151 + @index1, @index152 + @index2, @index153 + @index3, @index154 + @index0, @index155 + @index1, @index156 + @index2, @index157 + @index3, @index158 + @index0, @index159 + @index1, @index160 + @index2], @index161 + @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, @index50 + @index1, @index51 + @index1, @index52 + @index2, @index53 + @index2, @index54 + @index3, @index55 + @index3, @index56 + @index4, @index57 + @index4, @index58 + @index5, @index59 + @index5, @index60 + @index6, @index61 + @index6, @index62 + @index7, @index63 + @index7, @index64 + @index8, @index65 + @index8, @index66 + @index9, @index67 + @index9, @index68 + @index10, @index69 + @index10, @index70 + @index11, @index71 + @index11, @index72 + @index12, @index73 + @index12, @index74 + @index13, @index75 + @index13, @index76 + @index14, @index77 + @index14, @index78 + @index15, @index79 + @index15, @index80 + @index16, @index81 + @index16, @index82 + @index17, @index83 + @index17, @index84 + @index18, @index85 + @index18, @index86 + @index19, @index87 + @index19, @index88 + @index20, @index89 + @index20, @index90 + @index21, @index91 + @index21, @index92 + @index22, @index93 + @index22, @index94 + @index23, @index95 + @index23, @index96 + @index24, @index97 + @index24, @index98 + @index25, @index99 + @index25, @index100 + @index26, @index101 + @index26, @index102 + @index27, @index103 + @index27, @index104 + @index28, @index105 + @index28, @index106 + @index29, @index107 + @index29, @index108 + @index30, @index109 + @index30, @index110 + @index31, @index111 + @index31, @index112 + @index32, @index113 + @index32, @index114 + @index33, @index115 + @index33, @index116 + @index34, @index117 + @index34, @index118 + @index35, @index119 + @index35, @index120 + @index36, @index121 + @index36, @index122 + @index37, @index123 + @index37, @index124 + @index38, @index125 + @index38, @index126 + @index39, @index127 + @index39, @index128 + @index40, @index129 + @index40, @index130 + @index41, @index131 + @index41, @index132 + @index42, @index133 + @index42, @index134 + @index43, @index135 + @index43, @index136 + @index44, @index137 + @index44, @index138 + @index45, @index139 + @index45, @index140 + @index46, @index141 + @index46, @index142 + @index47, @index143 + @index47, @index144 + @index48, @index145 + @index48, @index146 + @index49], @index147 + @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]], @index0.map(@it:0:0, @index0.map(@it:1:0, @index0.map(@it:2:0, @index0.map(@it:3:0, @index0.map(@it:4:0, @index0.map(@it:5:0, @index0.map(@it:6:0, @index0.map(@it:7: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 new file mode 100644 index 000000000..1cbd1d4e7 --- /dev/null +++ b/optimizer/src/test/resources/large_expressions_block_recursion_depth_2.baseline @@ -0,0 +1,17 @@ +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, @index4 + @index3 + @index0, @index5 + @index1 + @index2, @index6 + @index3 + @index0, @index7 + @index1 + @index2, @index8 + @index3 + @index0, @index9 + @index1 + @index2, @index10 + @index3 + @index0, @index11 + @index1 + @index2, @index12 + @index3 + @index0, @index13 + @index1 + @index2, @index14 + @index3 + @index0, @index15 + @index1 + @index2, @index16 + @index3 + @index0, @index17 + @index1 + @index2, @index18 + @index3 + @index0, @index19 + @index1 + @index2, @index20 + @index3 + @index0, @index21 + @index1 + @index2, @index22 + @index3 + @index0, @index23 + @index1 + @index2, @index24 + @index3 + @index0, @index25 + @index1 + @index2, @index26 + @index3 + @index0, @index27 + @index1 + @index2, @index28 + @index3 + @index0, @index29 + @index1 + @index2, @index30 + @index3 + @index0, @index31 + @index1 + @index2, @index32 + @index3 + @index0, @index33 + @index1 + @index2, @index34 + @index3 + @index0, @index35 + @index1 + @index2, @index36 + @index3 + @index0, @index37 + @index1 + @index2, @index38 + @index3 + @index0, @index39 + @index1 + @index2, @index40 + @index3 + @index0, @index41 + @index1 + @index2, @index42 + @index3 + @index0, @index43 + @index1 + @index2, @index44 + @index3 + @index0, @index45 + @index1 + @index2, @index46 + @index3 + @index0, @index47 + @index1 + @index2, @index48 + @index3 + @index0, @index49 + @index1 + @index2, @index50 + @index3 + @index0, @index51 + @index1 + @index2, @index52 + @index3 + @index0, @index53 + @index1 + @index2, @index54 + @index3 + @index0, @index55 + @index1 + @index2, @index56 + @index3 + @index0, @index57 + @index1 + @index2, @index58 + @index3 + @index0, @index59 + @index1 + @index2, @index60 + @index3 + @index0, @index61 + @index1 + @index2, @index62 + @index3 + @index0, @index63 + @index1 + @index2, @index64 + @index3 + @index0, @index65 + @index1 + @index2, @index66 + @index3 + @index0, @index67 + @index1 + @index2, @index68 + @index3 + @index0, @index69 + @index1 + @index2, @index70 + @index3 + @index0, @index71 + @index1 + @index2, @index72 + @index3 + @index0, @index73 + @index1 + @index2, @index74 + @index3 + @index0, @index75 + @index1 + @index2, @index76 + @index3 + @index0, @index77 + @index1 + @index2, @index78 + @index3 + @index0, @index79 + @index1 + @index2, @index80 + @index3 + @index0, @index81 + @index1 + @index2], @index82 + @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, @index50 + @index1 + @index2, @index51 + @index2 + @index3, @index52 + @index3 + @index4, @index53 + @index4 + @index5, @index54 + @index5 + @index6, @index55 + @index6 + @index7, @index56 + @index7 + @index8, @index57 + @index8 + @index9, @index58 + @index9 + @index10, @index59 + @index10 + @index11, @index60 + @index11 + @index12, @index61 + @index12 + @index13, @index62 + @index13 + @index14, @index63 + @index14 + @index15, @index64 + @index15 + @index16, @index65 + @index16 + @index17, @index66 + @index17 + @index18, @index67 + @index18 + @index19, @index68 + @index19 + @index20, @index69 + @index20 + @index21, @index70 + @index21 + @index22, @index71 + @index22 + @index23, @index72 + @index23 + @index24, @index73 + @index24 + @index25, @index74 + @index25 + @index26, @index75 + @index26 + @index27, @index76 + @index27 + @index28, @index77 + @index28 + @index29, @index78 + @index29 + @index30, @index79 + @index30 + @index31, @index80 + @index31 + @index32, @index81 + @index32 + @index33, @index82 + @index33 + @index34, @index83 + @index34 + @index35, @index84 + @index35 + @index36, @index85 + @index36 + @index37, @index86 + @index37 + @index38, @index87 + @index38 + @index39, @index88 + @index39 + @index40, @index89 + @index40 + @index41, @index90 + @index41 + @index42, @index91 + @index42 + @index43, @index92 + @index43 + @index44, @index93 + @index44 + @index45, @index94 + @index45 + @index46, @index95 + @index46 + @index47, @index96 + @index47 + @index48, @index97 + @index48 + @index49], @index98 + @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], @index0.map(@it:7:0, @index0), [@index2], @index0.map(@it:6:0, @index2), [@index4], @index0.map(@it:5:0, @index4), [@index6], @index0.map(@it:4:0, @index6), [@index8], @index0.map(@it:3:0, @index8), [@index10], @index0.map(@it:2:0, @index10), [@index12], @index0.map(@it:1:0, @index12), [@index14]], @index0.map(@it:0: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 new file mode 100644 index 000000000..3ce295592 --- /dev/null +++ b/optimizer/src/test/resources/large_expressions_block_recursion_depth_3.baseline @@ -0,0 +1,17 @@ +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 + @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 + @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(@it:7:0, @index0), @index0.map(@it:6:0, @index1), @index0.map(@it:5:0, @index2), @index0.map(@it:4:0, @index3), @index0.map(@it:3:0, @index4), @index0.map(@it:2:0, @index5), @index0.map(@it:1:0, @index6)], @index0.map(@it:0: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 new file mode 100644 index 000000000..43782477c --- /dev/null +++ b/optimizer/src/test/resources/subexpression_ast_block_common_subexpr_only.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: 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: 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: 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: 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_recursion_depth_1.baseline b/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_1.baseline new file mode 100644 index 000000000..5bce4d49a --- /dev/null +++ b/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_1.baseline @@ -0,0 +1,4390 @@ +Test case: SIZE_1 +Source: size([1,2]) + size([1,2]) + 1 == 5 +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + CONSTANT [5] { value: 2 } + } + } + CALL [6] { + function: size + args: { + IDENT [7] { + name: @index0 + } + } + } + CALL [8] { + function: _+_ + args: { + IDENT [9] { + name: @index1 + } + IDENT [10] { + name: @index1 + } + } + } + CALL [11] { + function: _+_ + args: { + IDENT [12] { + name: @index2 + } + CONSTANT [13] { value: 1 } + } + } + } + } + CALL [14] { + function: _==_ + args: { + IDENT [15] { + name: @index3 + } + CONSTANT [16] { 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: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + CONSTANT [5] { value: 2 } + } + } + CALL [6] { + function: size + args: { + IDENT [7] { + name: @index0 + } + } + } + CALL [8] { + function: _+_ + args: { + CONSTANT [9] { value: 2 } + IDENT [10] { + name: @index1 + } + } + } + CALL [11] { + function: _+_ + args: { + IDENT [12] { + name: @index2 + } + IDENT [13] { + name: @index1 + } + } + } + CALL [14] { + function: _+_ + args: { + IDENT [15] { + name: @index3 + } + CONSTANT [16] { value: 1 } + } + } + } + } + CALL [17] { + function: _==_ + args: { + IDENT [18] { + name: @index4 + } + CONSTANT [19] { 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: { + LIST [3] { + elements: { + CONSTANT [4] { value: 0 } + } + } + CALL [5] { + function: size + args: { + IDENT [6] { + name: @index0 + } + } + } + LIST [7] { + elements: { + CONSTANT [8] { value: 1 } + CONSTANT [9] { value: 2 } + } + } + CALL [10] { + function: size + args: { + IDENT [11] { + name: @index2 + } + } + } + CALL [12] { + function: _+_ + args: { + IDENT [13] { + name: @index1 + } + IDENT [14] { + name: @index1 + } + } + } + CALL [15] { + function: _+_ + args: { + IDENT [16] { + name: @index4 + } + IDENT [17] { + name: @index3 + } + } + } + CALL [18] { + function: _+_ + args: { + IDENT [19] { + name: @index5 + } + IDENT [20] { + name: @index3 + } + } + } + } + } + CALL [21] { + function: _==_ + args: { + IDENT [22] { + name: @index6 + } + CONSTANT [23] { 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: { + LIST [3] { + elements: { + CONSTANT [4] { value: 0 } + } + } + CALL [5] { + function: size + args: { + IDENT [6] { + name: @index0 + } + } + } + LIST [7] { + elements: { + CONSTANT [8] { value: 1 } + CONSTANT [9] { value: 2 } + } + } + CALL [10] { + function: size + args: { + IDENT [11] { + name: @index2 + } + } + } + LIST [12] { + elements: { + CONSTANT [13] { value: 1 } + CONSTANT [14] { value: 2 } + CONSTANT [15] { value: 3 } + } + } + CALL [16] { + function: size + args: { + IDENT [17] { + name: @index4 + } + } + } + CALL [18] { + function: _+_ + args: { + CONSTANT [19] { value: 5 } + IDENT [20] { + name: @index1 + } + } + } + CALL [21] { + function: _+_ + args: { + IDENT [22] { + name: @index6 + } + IDENT [23] { + name: @index1 + } + } + } + CALL [24] { + function: _+_ + args: { + IDENT [25] { + name: @index7 + } + IDENT [26] { + name: @index3 + } + } + } + CALL [27] { + function: _+_ + args: { + IDENT [28] { + name: @index8 + } + IDENT [29] { + name: @index3 + } + } + } + CALL [30] { + function: _+_ + args: { + IDENT [31] { + name: @index9 + } + IDENT [32] { + name: @index5 + } + } + } + CALL [33] { + function: _+_ + args: { + IDENT [34] { + name: @index10 + } + IDENT [35] { + name: @index5 + } + } + } + } + } + CALL [36] { + function: _==_ + args: { + IDENT [37] { + name: @index11 + } + CONSTANT [38] { 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: 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 [9] { + function: getFullYear + target: { + IDENT [10] { + name: @index2 + } + } + 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 [17] { + function: timestamp + args: { + CONSTANT [18] { value: 200 } + } + } + CALL [19] { + function: int + args: { + IDENT [20] { + name: @index7 + } + } + } + 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: getFullYear + target: { + IDENT [32] { + name: @index13 + } + } + args: { + } + } + CALL [33] { + function: _+_ + args: { + IDENT [34] { + name: @index3 + } + IDENT [35] { + name: @index14 + } + } + } + CALL [36] { + function: getFullYear + target: { + IDENT [37] { + name: @index6 + } + } + args: { + } + } + CALL [38] { + function: _+_ + args: { + IDENT [39] { + name: @index15 + } + IDENT [40] { + name: @index16 + } + } + } + CALL [41] { + function: _+_ + args: { + IDENT [42] { + name: @index17 + } + IDENT [43] { + name: @index3 + } + } + } + CALL [44] { + function: getSeconds + target: { + IDENT [45] { + name: @index6 + } + } + args: { + } + } + CALL [46] { + function: _+_ + args: { + IDENT [47] { + name: @index18 + } + IDENT [48] { + name: @index19 + } + } + } + CALL [49] { + function: _+_ + args: { + IDENT [50] { + name: @index20 + } + IDENT [51] { + name: @index10 + } + } + } + CALL [52] { + function: _+_ + args: { + IDENT [53] { + name: @index21 + } + IDENT [54] { + name: @index10 + } + } + } + CALL [55] { + function: getMinutes + target: { + IDENT [56] { + name: @index13 + } + } + args: { + } + } + CALL [57] { + function: _+_ + args: { + IDENT [58] { + name: @index22 + } + IDENT [59] { + name: @index23 + } + } + } + CALL [60] { + function: _+_ + args: { + IDENT [61] { + name: @index24 + } + IDENT [62] { + name: @index3 + } + } + } + } + } + CALL [63] { + function: _==_ + args: { + IDENT [64] { + name: @index25 + } + CONSTANT [65] { 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: { + MAP [3] { + MAP_ENTRY [4] { + key: { + CONSTANT [5] { value: "a" } + } + value: { + CONSTANT [6] { value: 2 } + } + } + } + CALL [7] { + function: _[_] + args: { + IDENT [8] { + name: @index0 + } + CONSTANT [9] { value: "a" } + } + } + CALL [10] { + function: _*_ + args: { + IDENT [11] { + name: @index1 + } + IDENT [12] { + name: @index1 + } + } + } + CALL [13] { + function: _+_ + args: { + IDENT [14] { + name: @index1 + } + IDENT [15] { + name: @index2 + } + } + } + } + } + CALL [16] { + function: _==_ + args: { + IDENT [17] { + name: @index3 + } + CONSTANT [18] { 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: { + IDENT [12] { + name: @index1 + } + IDENT [13] { + name: @index0 + } + } + } + } + } + LIST [14] { + elements: { + CONSTANT [15] { value: 1 } + IDENT [16] { + name: @index0 + } + CONSTANT [17] { value: 2 } + IDENT [18] { + name: @index0 + } + CONSTANT [19] { value: 5 } + IDENT [20] { + name: @index0 + } + CONSTANT [21] { value: 7 } + IDENT [22] { + name: @index2 + } + IDENT [23] { + 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: { + IDENT [6] { + name: @index0 + } + IDENT [7] { + name: @index0 + } + } + } + } + } + CALL [8] { + function: _==_ + args: { + IDENT [9] { + name: @index1 + } + CONSTANT [10] { 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] { + 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: @index1 + }.single_int32 + } + CALL [11] { + function: _+_ + args: { + IDENT [12] { + name: @index2 + } + IDENT [13] { + name: @index3 + } + } + } + CALL [14] { + function: _+_ + args: { + IDENT [15] { + name: @index4 + } + IDENT [16] { + name: @index2 + } + } + } + SELECT [17] { + IDENT [18] { + name: msg + }.single_int64 + } + CALL [19] { + function: _+_ + args: { + IDENT [20] { + name: @index5 + } + 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: @index7 + } + IDENT [30] { + name: @index10 + } + } + } + } + } + CALL [31] { + function: _==_ + args: { + IDENT [32] { + name: @index11 + } + CONSTANT [33] { 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] { + 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] { + IDENT [14] { + name: @index4 + }.payload + } + SELECT [15] { + IDENT [16] { + name: @index5 + }.oneof_type + } + SELECT [17] { + IDENT [18] { + name: @index6 + }.payload + } + SELECT [19] { + IDENT [20] { + name: @index7 + }.single_bool + } + CALL [21] { + function: _||_ + args: { + CONSTANT [22] { value: true } + IDENT [23] { + name: @index8 + } + } + } + SELECT [24] { + IDENT [25] { + name: @index4 + }.child + } + SELECT [26] { + IDENT [27] { + name: @index10 + }.child + } + SELECT [28] { + IDENT [29] { + name: @index11 + }.payload + } + SELECT [30] { + IDENT [31] { + name: @index12 + }.single_bool + } + } + } + CALL [32] { + function: _||_ + args: { + IDENT [33] { + name: @index9 + } + IDENT [34] { + name: @index13 + } + } + } + } +} +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: { + 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] { + function: _[_] + args: { + IDENT [10] { + name: @index2 + } + CONSTANT [11] { value: 1 } + } + } + CALL [12] { + function: _+_ + args: { + IDENT [13] { + name: @index3 + } + IDENT [14] { + name: @index3 + } + } + } + CALL [15] { + function: _+_ + args: { + IDENT [16] { + name: @index4 + } + IDENT [17] { + name: @index3 + } + } + } + } + } + CALL [18] { + function: _==_ + args: { + IDENT [19] { + name: @index5 + } + CONSTANT [20] { 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] { + IDENT [4] { + name: msg + }.oneof_type + } + SELECT [5] { + IDENT [6] { + name: @index0 + }.payload + } + SELECT [7] { + IDENT [8] { + name: @index1 + }.map_int32_int64 + } + CALL [9] { + function: _[_] + args: { + IDENT [10] { + name: @index2 + } + CONSTANT [11] { value: 0 } + } + } + CALL [12] { + function: _[_] + args: { + IDENT [13] { + name: @index2 + } + CONSTANT [14] { value: 1 } + } + } + CALL [15] { + function: _+_ + args: { + IDENT [16] { + name: @index3 + } + IDENT [17] { + name: @index4 + } + } + } + CALL [18] { + function: _[_] + args: { + IDENT [19] { + name: @index2 + } + CONSTANT [20] { value: 2 } + } + } + CALL [21] { + function: _+_ + args: { + IDENT [22] { + name: @index5 + } + IDENT [23] { + name: @index6 + } + } + } + } + } + CALL [24] { + function: _==_ + args: { + IDENT [25] { + name: @index7 + } + CONSTANT [26] { 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 +=====> +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 + }.oneof_type + } + SELECT [9] { + IDENT [10] { + name: @index2 + }.payload + } + SELECT [11] { + IDENT [12] { + name: @index3 + }.oneof_type + } + SELECT [13] { + IDENT [14] { + name: @index4 + }.payload + } + SELECT [15] { + IDENT [16] { + name: @index5 + }.oneof_type + } + SELECT [17] { + IDENT [18] { + name: @index6 + }.payload + } + } + } + SELECT [19] { + IDENT [20] { + name: @index7 + }.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: { + IDENT [6] { + name: @index0 + } + CONSTANT [7] { value: 0 } + } + } + CALL [8] { + function: _?_:_ + args: { + IDENT [9] { + name: @index1 + } + IDENT [10] { + name: @index0 + } + CONSTANT [11] { value: 0 } + } + } + } + } + CALL [12] { + function: _==_ + args: { + IDENT [13] { + name: @index2 + } + CONSTANT [14] { 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: { + IDENT [6] { + name: @index0 + } + CONSTANT [7] { value: 1 } + } + } + CALL [8] { + function: _*_ + args: { + IDENT [9] { + name: @index1 + } + CONSTANT [10] { value: 2 } + } + } + CALL [11] { + function: _+_ + args: { + IDENT [12] { + name: @index0 + } + IDENT [13] { + name: @index2 + } + } + } + CALL [14] { + function: _==_ + args: { + IDENT [15] { + name: @index3 + } + CONSTANT [16] { value: 11 } + } + } + } + } + CALL [17] { + function: _?_:_ + args: { + CONSTANT [18] { value: false } + CONSTANT [19] { value: false } + IDENT [20] { + name: @index4 + } + } + } + } +} +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: { + IDENT [8] { + name: @index0 + } + CONSTANT [9] { value: 0 } + } + } + CALL [10] { + function: _>_ + args: { + IDENT [11] { + name: @index1 + } + CONSTANT [12] { value: 0 } + } + } + CALL [13] { + function: _+_ + args: { + IDENT [14] { + name: @index0 + } + IDENT [15] { + name: @index1 + } + } + } + CALL [16] { + function: _?_:_ + args: { + IDENT [17] { + name: @index3 + } + IDENT [18] { + name: @index4 + } + CONSTANT [19] { value: 0 } + } + } + CALL [20] { + function: _?_:_ + args: { + IDENT [21] { + name: @index2 + } + IDENT [22] { + name: @index5 + } + CONSTANT [23] { value: 0 } + } + } + } + } + CALL [24] { + function: _==_ + args: { + IDENT [25] { + name: @index6 + } + CONSTANT [26] { 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: { + 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_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: { + IDENT [22] { + name: @it:0:0 + } + CONSTANT [23] { value: 0 } + } + } + } + } + } + result: { + IDENT [24] { + name: @ac:0:0 + } + } + } + } + } + } + } + CALL [25] { + function: size + args: { + LIST [26] { + elements: { + COMPREHENSION [27] { + iter_var: @it:0:0 + iter_range: { + IDENT [28] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [29] { value: false } + } + loop_condition: { + CALL [30] { + function: @not_strictly_false + args: { + CALL [31] { + function: !_ + args: { + IDENT [32] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + 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 [39] { + function: size + args: { + LIST [40] { + elements: { + COMPREHENSION [41] { + iter_var: @it:0:0 + iter_range: { + IDENT [42] { + name: @index1 + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [43] { value: false } + } + loop_condition: { + CALL [44] { + function: @not_strictly_false + args: { + CALL [45] { + function: !_ + args: { + IDENT [46] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + 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: @ac:0:0 + } + } + } + } + } + } + } + } + } + CALL [53] { + function: size + args: { + LIST [54] { + elements: { + COMPREHENSION [55] { + iter_var: @it:0:0 + iter_range: { + IDENT [56] { + name: @index1 + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [57] { value: false } + } + loop_condition: { + CALL [58] { + function: @not_strictly_false + args: { + CALL [59] { + function: !_ + args: { + IDENT [60] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + 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 [66] { + name: @ac:0:0 + } + } + } + } + } + } + } + } + } + CONSTANT [67] { 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: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + } + } + LIST [5] { + elements: { + CONSTANT [6] { value: "a" } + } + } + LIST [7] { + elements: { + CONSTANT [8] { value: true } + CONSTANT [9] { value: true } + CONSTANT [10] { value: true } + CONSTANT [11] { value: true } + } + } + } + } + CALL [12] { + function: _==_ + args: { + CALL [13] { + function: _+_ + args: { + CALL [14] { + function: _+_ + args: { + CALL [15] { + function: _+_ + args: { + LIST [16] { + elements: { + COMPREHENSION [17] { + iter_var: @it:0:0 + iter_range: { + IDENT [18] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [19] { value: false } + } + loop_condition: { + CALL [20] { + function: @not_strictly_false + args: { + CALL [21] { + function: !_ + args: { + IDENT [22] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + 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 [28] { + name: @ac:0:0 + } + } + } + } + } + LIST [29] { + elements: { + COMPREHENSION [30] { + iter_var: @it:0:0 + iter_range: { + IDENT [31] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [32] { value: false } + } + loop_condition: { + CALL [33] { + function: @not_strictly_false + args: { + CALL [34] { + function: !_ + args: { + IDENT [35] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + 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 [41] { + name: @ac:0:0 + } + } + } + } + } + } + } + LIST [42] { + elements: { + COMPREHENSION [43] { + iter_var: @it:0:1 + iter_range: { + IDENT [44] { + name: @index1 + } + } + accu_var: @ac:0:1 + accu_init: { + CONSTANT [45] { value: false } + } + loop_condition: { + CALL [46] { + function: @not_strictly_false + args: { + CALL [47] { + function: !_ + args: { + IDENT [48] { + name: @ac:0:1 + } + } + } + } + } + } + loop_step: { + 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: @ac:0:1 + } + } + } + } + } + } + } + LIST [55] { + elements: { + COMPREHENSION [56] { + iter_var: @it:0:1 + iter_range: { + IDENT [57] { + name: @index1 + } + } + accu_var: @ac:0:1 + accu_init: { + CONSTANT [58] { value: false } + } + loop_condition: { + CALL [59] { + function: @not_strictly_false + args: { + CALL [60] { + function: !_ + args: { + IDENT [61] { + name: @ac:0:1 + } + } + } + } + } + } + loop_step: { + 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 [67] { + name: @ac:0:1 + } + } + } + } + } + } + } + IDENT [68] { + name: @index2 + } + } + } + } +} +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: { + 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_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: { + IDENT [18] { + name: @it:0:0 + } + CONSTANT [19] { value: 0 } + } + } + } + } + } + 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 + } + } + } + } + } + } + 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 [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: 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:0:0 + iter_range: { + IDENT [17] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [18] { + elements: { + } + } + } + loop_condition: { + CONSTANT [19] { value: true } + } + loop_step: { + CALL [20] { + function: _+_ + args: { + IDENT [21] { + name: @ac:0:0 + } + LIST [22] { + elements: { + 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: { + CALL [30] { + function: _+_ + args: { + IDENT [31] { + name: @it:1:0 + } + CONSTANT [32] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [33] { + name: @ac:1:0 + } + } + } + } + } + } + } + } + result: { + IDENT [34] { + name: @ac:0: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:0:0 + iter_range: { + IDENT [19] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [20] { + elements: { + } + } + } + loop_condition: { + CONSTANT [21] { value: true } + } + loop_step: { + CALL [22] { + function: _+_ + args: { + IDENT [23] { + name: @ac:0:0 + } + LIST [24] { + elements: { + 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: { + CALL [30] { + function: _==_ + args: { + IDENT [31] { + name: @it:1:0 + } + IDENT [32] { + name: @it:0:0 + } + } + } + CALL [33] { + function: _+_ + args: { + IDENT [34] { + name: @ac:1:0 + } + LIST [35] { + elements: { + IDENT [36] { + name: @it:1:0 + } + } + } + } + } + IDENT [37] { + name: @ac:1:0 + } + } + } + } + result: { + IDENT [38] { + name: @ac:1:0 + } + } + } + } + } + } + } + } + result: { + IDENT [39] { + name: @ac:0:0 + } + } + } + IDENT [40] { + name: @index4 + } + } + } + } +} +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: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: { + COMPREHENSION [15] { + iter_var: @it:1:0 + iter_range: { + IDENT [16] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [17] { + elements: { + } + } + } + loop_condition: { + CONSTANT [18] { value: true } + } + loop_step: { + CALL [19] { + function: _+_ + args: { + IDENT [20] { + name: @ac:1:0 + } + LIST [21] { + elements: { + CALL [22] { + function: _+_ + args: { + IDENT [23] { + name: @it:1:0 + } + CONSTANT [24] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [25] { + name: @ac:1:0 + } + } + } + } + } + } + } + } + result: { + IDENT [26] { + name: @ac:0:0 + } + } + } + COMPREHENSION [27] { + iter_var: @it:0:0 + iter_range: { + IDENT [28] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [29] { + elements: { + } + } + } + loop_condition: { + CONSTANT [30] { value: true } + } + loop_step: { + CALL [31] { + function: _+_ + args: { + IDENT [32] { + name: @ac:0:0 + } + LIST [33] { + elements: { + COMPREHENSION [34] { + iter_var: @it:1:0 + iter_range: { + IDENT [35] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [36] { + elements: { + } + } + } + loop_condition: { + CONSTANT [37] { value: true } + } + loop_step: { + CALL [38] { + function: _+_ + args: { + IDENT [39] { + name: @ac:1:0 + } + LIST [40] { + elements: { + CALL [41] { + function: _+_ + args: { + IDENT [42] { + name: @it:1:0 + } + CONSTANT [43] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [44] { + name: @ac:1:0 + } + } + } + } + } + } + } + } + result: { + IDENT [45] { + name: @ac:0: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: { + 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 + args: { + CONSTANT [11] { value: 2 } + IDENT [12] { + name: @index0 + } + } + } + CALL [13] { + function: _&&_ + args: { + IDENT [14] { + name: @index1 + } + IDENT [15] { + name: @index2 + } + } + } + LIST [16] { + elements: { + CONSTANT [17] { value: 3 } + IDENT [18] { + name: @index0 + } + } + } + CALL [19] { + function: @in + args: { + CONSTANT [20] { value: 3 } + IDENT [21] { + name: @index4 + } + } + } + CALL [22] { + function: _&&_ + args: { + IDENT [23] { + name: @index5 + } + IDENT [24] { + name: @index1 + } + } + } + } + } + CALL [25] { + function: _&&_ + args: { + IDENT [26] { + name: @index3 + } + IDENT [27] { + name: @index6 + } + } + } + } +} +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 } + } + } + } + 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 + } + } + } + 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:0:0 + iter_range: { + IDENT [19] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [20] { + elements: { + } + } + } + loop_condition: { + CONSTANT [21] { value: true } + } + loop_step: { + CALL [22] { + function: _+_ + args: { + IDENT [23] { + name: @ac:0:0 + } + LIST [24] { + elements: { + COMPREHENSION [25] { + iter_var: @it: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 + } + IDENT [31] { + name: @index3 + } + } + } + } + result: { + IDENT [32] { + name: @ac:1:0 + } + } + } + } + } + } + } + } + result: { + IDENT [33] { + name: @ac:0:0 + } + } + } + IDENT [34] { + name: @index4 + } + } + } + } +} +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: { + IDENT [10] { + name: @index1 + } + IDENT [11] { + name: @index0 + } + CONSTANT [12] { value: 5 } + } + } + LIST [13] { + elements: { + IDENT [14] { + name: @index2 + } + } + } + } + } + CALL [15] { + function: _||_ + args: { + COMPREHENSION [16] { + iter_var: @it:0:0 + iter_range: { + IDENT [17] { + name: @index3 + } + } + 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 + } + } + } + } + } + } + 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 + } + } + } + IDENT [30] { + name: @index1 + } + } + } + } +} +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:0:0 + iter_range: { + COMPREHENSION [7] { + iter_var: @it:1:0 + iter_range: { + IDENT [8] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [9] { + elements: { + } + } + } + loop_condition: { + CONSTANT [10] { value: true } + } + loop_step: { + CALL [11] { + function: _+_ + args: { + IDENT [12] { + name: @ac:1:0 + } + LIST [13] { + elements: { + LIST [14] { + elements: { + CALL [15] { + function: _+_ + args: { + IDENT [16] { + name: @it:1:0 + } + IDENT [17] { + name: @it:1:0 + } + } + } + CALL [18] { + function: _+_ + args: { + IDENT [19] { + name: @it:1:0 + } + IDENT [20] { + name: @it:1:0 + } + } + } + } + } + } + } + } + } + } + result: { + IDENT [21] { + name: @ac:1:0 + } + } + } + } + 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: { + LIST [27] { + elements: { + CALL [28] { + function: _+_ + args: { + IDENT [29] { + name: @it:0:0 + } + IDENT [30] { + name: @it:0:0 + } + } + } + CALL [31] { + function: _+_ + args: { + IDENT [32] { + name: @it:0:0 + } + IDENT [33] { + 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 } + } + } + } + SELECT [7] { + IDENT [8] { + name: @index0 + }.a~presence_test + } + CALL [9] { + function: _[_] + args: { + IDENT [10] { + name: @index0 + } + CONSTANT [11] { value: "a" } + } + } + } + } + CALL [12] { + function: _&&_ + args: { + 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 + } + } + } + CALL [9] { + function: _&&_ + args: { + IDENT [10] { + name: @index1 + } + IDENT [11] { + name: @index1 + } + } + } + } +} +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 + } + SELECT [5] { + IDENT [6] { + name: @index0 + }.payload~presence_test + } + SELECT [7] { + IDENT [8] { + name: @index0 + }.payload + } + SELECT [9] { + IDENT [10] { + name: @index2 + }.single_int64 + } + CALL [11] { + function: _?_:_ + args: { + IDENT [12] { + name: @index1 + } + IDENT [13] { + name: @index3 + } + CONSTANT [14] { value: 0 } + } + } + } + } + CALL [15] { + function: _==_ + args: { + IDENT [16] { + name: @index4 + } + CONSTANT [17] { 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] { + IDENT [6] { + name: @index0 + }.payload + } + SELECT [7] { + IDENT [8] { + name: @index1 + }.single_int64 + } + SELECT [9] { + IDENT [10] { + name: @index0 + }.payload~presence_test + } + CALL [11] { + function: _*_ + args: { + IDENT [12] { + name: @index2 + } + CONSTANT [13] { value: 0 } + } + } + CALL [14] { + function: _?_:_ + args: { + IDENT [15] { + name: @index3 + } + IDENT [16] { + name: @index2 + } + IDENT [17] { + name: @index4 + } + } + } + } + } + CALL [18] { + function: _==_ + args: { + IDENT [19] { + name: @index5 + } + CONSTANT [20] { 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] { + 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: @index1 + }.single_int64~presence_test + } + CALL [11] { + function: _*_ + args: { + IDENT [12] { + name: @index2 + } + CONSTANT [13] { value: 0 } + } + } + CALL [14] { + function: _?_:_ + args: { + IDENT [15] { + name: @index3 + } + IDENT [16] { + name: @index2 + } + IDENT [17] { + name: @index4 + } + } + } + } + } + CALL [18] { + function: _==_ + args: { + IDENT [19] { + name: @index5 + } + CONSTANT [20] { 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 + } + SELECT [9] { + IDENT [10] { + name: msg + }.oneof_type~presence_test + } + SELECT [11] { + IDENT [12] { + name: @index0 + }.payload~presence_test + } + CALL [13] { + function: _&&_ + args: { + IDENT [14] { + name: @index3 + } + IDENT [15] { + name: @index4 + } + } + } + SELECT [16] { + IDENT [17] { + name: @index1 + }.single_int64~presence_test + } + CALL [18] { + function: _&&_ + args: { + IDENT [19] { + name: @index5 + } + IDENT [20] { + name: @index6 + } + } + } + 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: { + IDENT [34] { + name: @index10 + } + IDENT [35] { + name: @index12 + } + CONSTANT [36] { value: false } + } + } + } + } + CALL [37] { + function: _?_:_ + args: { + IDENT [38] { + name: @index7 + } + IDENT [39] { + name: @index13 + } + CONSTANT [40] { 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 } + } + } + LIST [9] { + elements: { + CONSTANT [10] { value: 10 } + IDENT [11] { + name: @index0 + } + IDENT [12] { + name: @index1 + } + IDENT [13] { + name: @index1 + } + } + optional_indices: [0] + } + LIST [14] { + elements: { + CONSTANT [15] { value: 10 } + IDENT [16] { + name: @index2 + } + IDENT [17] { + name: @index2 + } + } + } + } + } + CALL [18] { + function: _==_ + args: { + IDENT [19] { + name: @index3 + } + IDENT [20] { + name: @index4 + } + } + } + } +} +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: optional.of + args: { + CONSTANT [4] { value: "hello" } + } + } + 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 + } + CONSTANT [11] { value: "hello" } + } + } + CALL [12] { + function: _+_ + args: { + IDENT [13] { + name: @index2 + } + IDENT [14] { + name: @index2 + } + } + } + } + } + CALL [15] { + function: _==_ + args: { + IDENT [16] { + name: @index3 + } + CONSTANT [17] { 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: optional.of + args: { + CONSTANT [8] { value: "test" } + } + } + MAP [9] { + MAP_ENTRY [10] { + key: { + CONSTANT [11] { value: "key" } + } + optional_entry: true + value: { + IDENT [12] { + name: @index1 + } + } + } + } + CALL [13] { + function: _[?_] + args: { + IDENT [14] { + name: @index2 + } + CONSTANT [15] { value: "bogus" } + } + } + CALL [16] { + function: _[?_] + args: { + IDENT [17] { + name: @index0 + } + CONSTANT [18] { value: "bogus" } + } + } + CALL [19] { + function: or + target: { + IDENT [20] { + name: @index3 + } + } + args: { + IDENT [21] { + name: @index4 + } + } + } + CALL [22] { + function: _[_] + args: { + IDENT [23] { + name: @index0 + } + CONSTANT [24] { value: "key" } + } + } + CALL [25] { + function: orValue + target: { + IDENT [26] { + name: @index5 + } + } + args: { + IDENT [27] { + name: @index6 + } + } + } + } + } + CALL [28] { + function: _==_ + args: { + IDENT [29] { + name: @index7 + } + CONSTANT [30] { 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: { + CALL [3] { + function: optional.ofNonZeroValue + args: { + CONSTANT [4] { value: 1 } + } + } + CALL [5] { + function: optional.of + args: { + CONSTANT [6] { value: 4 } + } + } + 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 + } + } + } + } + } + SELECT [12] { + IDENT [13] { + name: @index2 + }.single_int32 + } + SELECT [14] { + IDENT [15] { + name: @index2 + }.single_int64 + } + CALL [16] { + function: _+_ + args: { + IDENT [17] { + name: @index3 + } + IDENT [18] { + name: @index4 + } + } + } + } + } + CALL [19] { + function: _==_ + args: { + IDENT [20] { + name: @index5 + } + CONSTANT [21] { 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: { + 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 + } + CONSTANT [17] { value: " world" } + } + } + } + } + CALL [18] { + function: matches + target: { + IDENT [19] { + name: @index4 + } + } + args: { + IDENT [20] { + name: @index3 + } + } + } + } +} +Test case: CALL_ARGUMENT_NESTED_NO_COMMON_SUBEXPR +Source: 'hello world'.matches('h' + 'e' + 'l' + 'l' + 'o') +=====> +CALL [1] { + function: cel.@block + args: { + 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: matches + target: { + CONSTANT [16] { value: "hello world" } + } + args: { + IDENT [17] { + name: @index3 + } + } + } + } +} +Test case: CALL_TARGET_NESTED_NO_COMMON_SUBEXPR +Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('hello') +=====> +CALL [1] { + function: cel.@block + args: { + 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 + } + CONSTANT [17] { value: " world" } + } + } + } + } + CALL [18] { + function: matches + target: { + IDENT [19] { + name: @index4 + } + } + args: { + CONSTANT [20] { 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 [1] { + function: cel.@block + args: { + 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 + } + CONSTANT [17] { value: " world" } + } + } + CALL [18] { + function: _+_ + args: { + CONSTANT [19] { value: "w" } + CONSTANT [20] { value: "o" } + } + } + CALL [21] { + function: _+_ + args: { + IDENT [22] { + name: @index5 + } + CONSTANT [23] { value: "r" } + } + } + CALL [24] { + function: _+_ + args: { + IDENT [25] { + name: @index6 + } + CONSTANT [26] { value: "l" } + } + } + CALL [27] { + function: _+_ + args: { + IDENT [28] { + name: @index7 + } + CONSTANT [29] { value: "d" } + } + } + } + } + CALL [30] { + function: matches + target: { + IDENT [31] { + name: @index4 + } + } + args: { + IDENT [32] { + name: @index8 + } + } + } + } +} +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] { + IDENT [4] { + name: msg + }.oneof_type + } + SELECT [5] { + IDENT [6] { + name: @index0 + }.payload + } + SELECT [7] { + IDENT [8] { + name: @index1 + }.single_int64 + } + } + } + CALL [9] { + function: _+_ + args: { + CALL [10] { + function: _+_ + args: { + CALL [11] { + function: _+_ + args: { + CALL [12] { + function: non_pure_custom_func + args: { + IDENT [13] { + name: @index2 + } + } + } + CALL [14] { + function: non_pure_custom_func + args: { + SELECT [15] { + IDENT [16] { + name: @index1 + }.single_int32 + } + } + } + } + } + CALL [17] { + function: non_pure_custom_func + args: { + IDENT [18] { + name: @index2 + } + } + } + } + } + CALL [19] { + function: non_pure_custom_func + args: { + SELECT [20] { + IDENT [21] { + 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] { + 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 + } + } + } + SELECT [11] { + IDENT [12] { + name: @index1 + }.single_int32 + } + CALL [13] { + function: pure_custom_func + args: { + IDENT [14] { + name: @index4 + } + } + } + CALL [15] { + function: _+_ + args: { + IDENT [16] { + name: @index3 + } + IDENT [17] { + name: @index5 + } + } + } + CALL [18] { + function: _+_ + args: { + IDENT [19] { + name: @index6 + } + IDENT [20] { + name: @index3 + } + } + } + SELECT [21] { + IDENT [22] { + name: msg + }.single_int64 + } + CALL [23] { + function: pure_custom_func + args: { + IDENT [24] { + name: @index8 + } + } + } + } + } + CALL [25] { + function: _+_ + args: { + IDENT [26] { + name: @index7 + } + IDENT [27] { + 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 new file mode 100644 index 000000000..285b6183c --- /dev/null +++ b/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_2.baseline @@ -0,0 +1,4068 @@ +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: { + IDENT [9] { + name: @index0 + } + IDENT [10] { + name: @index0 + } + } + } + CONSTANT [11] { value: 1 } + } + } + } + } + CALL [12] { + function: _==_ + args: { + IDENT [13] { + name: @index1 + } + CONSTANT [14] { 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: { + CONSTANT [9] { value: 2 } + IDENT [10] { + name: @index0 + } + } + } + IDENT [11] { + name: @index0 + } + } + } + } + } + CALL [12] { + function: _==_ + args: { + CALL [13] { + function: _+_ + args: { + IDENT [14] { + name: @index1 + } + CONSTANT [15] { value: 1 } + } + } + CONSTANT [16] { 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: { + IDENT [12] { + name: @index0 + } + IDENT [13] { + name: @index0 + } + } + } + IDENT [14] { + name: @index1 + } + } + } + } + } + CALL [15] { + function: _==_ + args: { + CALL [16] { + function: _+_ + args: { + IDENT [17] { + name: @index2 + } + IDENT [18] { + name: @index1 + } + } + } + CONSTANT [19] { 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: { + CONSTANT [17] { value: 5 } + IDENT [18] { + name: @index0 + } + } + } + IDENT [19] { + name: @index0 + } + } + } + CALL [20] { + function: _+_ + args: { + CALL [21] { + function: _+_ + args: { + IDENT [22] { + name: @index3 + } + IDENT [23] { + name: @index1 + } + } + } + IDENT [24] { + name: @index1 + } + } + } + CALL [25] { + function: _+_ + args: { + CALL [26] { + function: _+_ + args: { + IDENT [27] { + name: @index4 + } + IDENT [28] { + name: @index2 + } + } + } + IDENT [29] { + name: @index2 + } + } + } + } + } + CALL [30] { + function: _==_ + args: { + IDENT [31] { + name: @index5 + } + CONSTANT [32] { 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: int + args: { + CALL [4] { + function: timestamp + args: { + CONSTANT [5] { value: 1000000000 } + } + } + } + } + CALL [6] { + function: getFullYear + target: { + CALL [7] { + function: timestamp + args: { + IDENT [8] { + name: @index0 + } + } + } + } + args: { + } + } + CALL [9] { + function: int + args: { + CALL [10] { + function: timestamp + args: { + CONSTANT [11] { value: 50 } + } + } + } + } + CALL [12] { + function: int + args: { + CALL [13] { + function: timestamp + args: { + CONSTANT [14] { value: 200 } + } + } + } + } + CALL [15] { + function: getFullYear + target: { + CALL [16] { + function: timestamp + args: { + IDENT [17] { + name: @index3 + } + } + } + } + args: { + } + } + CALL [18] { + function: int + args: { + CALL [19] { + function: timestamp + args: { + CONSTANT [20] { value: 75 } + } + } + } + } + CALL [21] { + function: timestamp + args: { + IDENT [22] { + name: @index2 + } + } + } + CALL [23] { + function: timestamp + args: { + IDENT [24] { + name: @index5 + } + } + } + CALL [25] { + function: _+_ + args: { + IDENT [26] { + name: @index1 + } + CALL [27] { + function: getFullYear + target: { + IDENT [28] { + name: @index7 + } + } + args: { + } + } + } + } + CALL [29] { + function: _+_ + args: { + IDENT [30] { + name: @index8 + } + CALL [31] { + function: getFullYear + target: { + IDENT [32] { + name: @index6 + } + } + args: { + } + } + } + } + CALL [33] { + function: _+_ + args: { + CALL [34] { + function: _+_ + args: { + IDENT [35] { + name: @index9 + } + IDENT [36] { + name: @index1 + } + } + } + CALL [37] { + function: getSeconds + target: { + IDENT [38] { + name: @index6 + } + } + args: { + } + } + } + } + CALL [39] { + function: _+_ + args: { + CALL [40] { + function: _+_ + args: { + IDENT [41] { + name: @index10 + } + IDENT [42] { + name: @index4 + } + } + } + IDENT [43] { + name: @index4 + } + } + } + CALL [44] { + function: _+_ + args: { + IDENT [45] { + name: @index11 + } + CALL [46] { + function: getMinutes + target: { + IDENT [47] { + name: @index7 + } + } + args: { + } + } + } + } + } + } + CALL [48] { + function: _==_ + args: { + CALL [49] { + function: _+_ + args: { + IDENT [50] { + name: @index12 + } + IDENT [51] { + name: @index1 + } + } + } + CONSTANT [52] { 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: { + IDENT [10] { + name: @index0 + } + CALL [11] { + function: _*_ + args: { + IDENT [12] { + name: @index0 + } + IDENT [13] { + name: @index0 + } + } + } + } + } + } + } + CALL [14] { + function: _==_ + args: { + IDENT [15] { + name: @index1 + } + CONSTANT [16] { 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: "e" } + } + value: { + MAP [6] { + MAP_ENTRY [7] { + key: { + CONSTANT [8] { value: "b" } + } + value: { + CONSTANT [9] { value: 1 } + } + } + } + } + } + } + MAP [10] { + MAP_ENTRY [11] { + key: { + CONSTANT [12] { value: "b" } + } + value: { + CONSTANT [13] { value: 1 } + } + } + } + } + } + MAP [14] { + MAP_ENTRY [15] { + key: { + CONSTANT [16] { value: "a" } + } + value: { + IDENT [17] { + name: @index1 + } + } + } + MAP_ENTRY [18] { + key: { + CONSTANT [19] { value: "c" } + } + value: { + IDENT [20] { + name: @index1 + } + } + } + MAP_ENTRY [21] { + key: { + CONSTANT [22] { value: "d" } + } + value: { + IDENT [23] { + name: @index0 + } + } + } + MAP_ENTRY [24] { + key: { + CONSTANT [25] { value: "e" } + } + value: { + IDENT [26] { + name: @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]] +=====> +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: { + IDENT [9] { + name: @index1 + } + SELECT [10] { + IDENT [11] { + name: @index0 + }.single_int32 + } + } + } + CALL [12] { + function: _+_ + args: { + CALL [13] { + function: _+_ + args: { + IDENT [14] { + name: @index2 + } + IDENT [15] { + name: @index1 + } + } + } + SELECT [16] { + IDENT [17] { + name: msg + }.single_int64 + } + } + } + SELECT [18] { + SELECT [19] { + IDENT [20] { + name: @index0 + }.oneof_type + }.payload + } + CALL [21] { + function: _+_ + args: { + IDENT [22] { + name: @index3 + } + SELECT [23] { + IDENT [24] { + name: @index4 + }.single_int64 + } + } + } + } + } + CALL [25] { + function: _==_ + args: { + IDENT [26] { + name: @index5 + } + CONSTANT [27] { 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] { + IDENT [5] { + name: msg + }.oneof_type + }.payload + } + SELECT [6] { + SELECT [7] { + IDENT [8] { + name: @index0 + }.oneof_type + }.payload + } + SELECT [9] { + IDENT [10] { + name: @index1 + }.oneof_type + } + SELECT [11] { + SELECT [12] { + IDENT [13] { + name: @index2 + }.payload + }.oneof_type + } + SELECT [14] { + SELECT [15] { + IDENT [16] { + name: @index3 + }.payload + }.single_bool + } + SELECT [17] { + SELECT [18] { + IDENT [19] { + name: @index2 + }.child + }.child + } + SELECT [20] { + SELECT [21] { + IDENT [22] { + name: @index5 + }.payload + }.single_bool + } + } + } + CALL [23] { + function: _||_ + args: { + CALL [24] { + function: _||_ + args: { + CONSTANT [25] { value: true } + IDENT [26] { + name: @index4 + } + } + } + IDENT [27] { + name: @index6 + } + } + } + } +} +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: { + SELECT [3] { + SELECT [4] { + IDENT [5] { + name: msg + }.oneof_type + }.payload + } + CALL [6] { + function: _[_] + args: { + SELECT [7] { + IDENT [8] { + name: @index0 + }.map_int32_int64 + } + CONSTANT [9] { value: 1 } + } + } + CALL [10] { + function: _+_ + args: { + CALL [11] { + function: _+_ + args: { + IDENT [12] { + name: @index1 + } + IDENT [13] { + name: @index1 + } + } + } + IDENT [14] { + name: @index1 + } + } + } + } + } + CALL [15] { + function: _==_ + args: { + IDENT [16] { + name: @index2 + } + CONSTANT [17] { 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] { + IDENT [5] { + name: msg + }.oneof_type + }.payload + } + SELECT [6] { + IDENT [7] { + name: @index0 + }.map_int32_int64 + } + CALL [8] { + function: _+_ + args: { + CALL [9] { + function: _[_] + args: { + IDENT [10] { + name: @index1 + } + CONSTANT [11] { value: 0 } + } + } + CALL [12] { + function: _[_] + args: { + IDENT [13] { + name: @index1 + } + CONSTANT [14] { value: 1 } + } + } + } + } + CALL [15] { + function: _+_ + args: { + IDENT [16] { + name: @index2 + } + CALL [17] { + function: _[_] + args: { + IDENT [18] { + name: @index1 + } + CONSTANT [19] { value: 2 } + } + } + } + } + } + } + CALL [20] { + function: _==_ + args: { + IDENT [21] { + name: @index3 + } + CONSTANT [22] { 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 +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + SELECT [3] { + SELECT [4] { + IDENT [5] { + name: msg + }.oneof_type + }.payload + } + SELECT [6] { + SELECT [7] { + IDENT [8] { + name: @index0 + }.oneof_type + }.payload + } + SELECT [9] { + SELECT [10] { + IDENT [11] { + name: @index1 + }.oneof_type + }.payload + } + SELECT [12] { + SELECT [13] { + IDENT [14] { + name: @index2 + }.oneof_type + }.payload + } + } + } + SELECT [15] { + IDENT [16] { + name: @index3 + }.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: { + IDENT [7] { + name: @index0 + } + CONSTANT [8] { value: 0 } + } + } + IDENT [9] { + name: @index0 + } + CONSTANT [10] { value: 0 } + } + } + } + } + CALL [11] { + function: _==_ + args: { + IDENT [12] { + name: @index1 + } + CONSTANT [13] { 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: { + CALL [6] { + function: _+_ + args: { + IDENT [7] { + name: @index0 + } + CONSTANT [8] { value: 1 } + } + } + CONSTANT [9] { value: 2 } + } + } + CALL [10] { + function: _==_ + args: { + CALL [11] { + function: _+_ + args: { + IDENT [12] { + name: @index0 + } + IDENT [13] { + name: @index1 + } + } + } + CONSTANT [14] { value: 11 } + } + } + } + } + CALL [15] { + function: _?_:_ + args: { + CONSTANT [16] { value: false } + CONSTANT [17] { value: false } + IDENT [18] { + name: @index2 + } + } + } + } +} +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: { + IDENT [9] { + name: @index1 + } + CONSTANT [10] { value: 0 } + } + } + CALL [11] { + function: _+_ + args: { + IDENT [12] { + name: @index0 + } + IDENT [13] { + name: @index1 + } + } + } + CONSTANT [14] { value: 0 } + } + } + CALL [15] { + function: _?_:_ + args: { + CALL [16] { + function: _>_ + args: { + IDENT [17] { + name: @index0 + } + CONSTANT [18] { value: 0 } + } + } + IDENT [19] { + name: @index2 + } + CONSTANT [20] { value: 0 } + } + } + } + } + CALL [21] { + function: _==_ + args: { + IDENT [22] { + name: @index3 + } + CONSTANT [23] { 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: { + 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_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: { + IDENT [22] { + name: @it:0:0 + } + CONSTANT [23] { value: 0 } + } + } + } + } + } + result: { + IDENT [24] { + name: @ac:0:0 + } + } + } + } + } + } + } + CALL [25] { + function: size + args: { + LIST [26] { + elements: { + COMPREHENSION [27] { + iter_var: @it:0:0 + iter_range: { + IDENT [28] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [29] { value: false } + } + loop_condition: { + CALL [30] { + function: @not_strictly_false + args: { + CALL [31] { + function: !_ + args: { + IDENT [32] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + 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 [39] { + function: size + args: { + LIST [40] { + elements: { + COMPREHENSION [41] { + iter_var: @it:0:0 + iter_range: { + IDENT [42] { + name: @index1 + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [43] { value: false } + } + loop_condition: { + CALL [44] { + function: @not_strictly_false + args: { + CALL [45] { + function: !_ + args: { + IDENT [46] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + 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: @ac:0:0 + } + } + } + } + } + } + } + } + } + CALL [53] { + function: size + args: { + LIST [54] { + elements: { + COMPREHENSION [55] { + iter_var: @it:0:0 + iter_range: { + IDENT [56] { + name: @index1 + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [57] { value: false } + } + loop_condition: { + CALL [58] { + function: @not_strictly_false + args: { + CALL [59] { + function: !_ + args: { + IDENT [60] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + 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 [66] { + name: @ac:0:0 + } + } + } + } + } + } + } + } + } + CONSTANT [67] { 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: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + } + } + LIST [5] { + elements: { + CONSTANT [6] { value: "a" } + } + } + LIST [7] { + elements: { + CONSTANT [8] { value: true } + CONSTANT [9] { value: true } + CONSTANT [10] { value: true } + CONSTANT [11] { value: true } + } + } + } + } + CALL [12] { + function: _==_ + args: { + CALL [13] { + function: _+_ + args: { + CALL [14] { + function: _+_ + args: { + CALL [15] { + function: _+_ + args: { + LIST [16] { + elements: { + COMPREHENSION [17] { + iter_var: @it:0:0 + iter_range: { + IDENT [18] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [19] { value: false } + } + loop_condition: { + CALL [20] { + function: @not_strictly_false + args: { + CALL [21] { + function: !_ + args: { + IDENT [22] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + 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 [28] { + name: @ac:0:0 + } + } + } + } + } + LIST [29] { + elements: { + COMPREHENSION [30] { + iter_var: @it:0:0 + iter_range: { + IDENT [31] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [32] { value: false } + } + loop_condition: { + CALL [33] { + function: @not_strictly_false + args: { + CALL [34] { + function: !_ + args: { + IDENT [35] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + 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 [41] { + name: @ac:0:0 + } + } + } + } + } + } + } + LIST [42] { + elements: { + COMPREHENSION [43] { + iter_var: @it:0:1 + iter_range: { + IDENT [44] { + name: @index1 + } + } + accu_var: @ac:0:1 + accu_init: { + CONSTANT [45] { value: false } + } + loop_condition: { + CALL [46] { + function: @not_strictly_false + args: { + CALL [47] { + function: !_ + args: { + IDENT [48] { + name: @ac:0:1 + } + } + } + } + } + } + loop_step: { + 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: @ac:0:1 + } + } + } + } + } + } + } + LIST [55] { + elements: { + COMPREHENSION [56] { + iter_var: @it:0:1 + iter_range: { + IDENT [57] { + name: @index1 + } + } + accu_var: @ac:0:1 + accu_init: { + CONSTANT [58] { value: false } + } + loop_condition: { + CALL [59] { + function: @not_strictly_false + args: { + CALL [60] { + function: !_ + args: { + IDENT [61] { + name: @ac:0:1 + } + } + } + } + } + } + loop_step: { + 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 [67] { + name: @ac:0:1 + } + } + } + } + } + } + } + IDENT [68] { + name: @index2 + } + } + } + } +} +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: { + 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_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: { + IDENT [18] { + name: @it:0:0 + } + CONSTANT [19] { value: 0 } + } + } + } + } + } + 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 + } + } + } + } + } + } + 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 [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: 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:0:0 + iter_range: { + IDENT [17] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [18] { + elements: { + } + } + } + loop_condition: { + CONSTANT [19] { value: true } + } + loop_step: { + CALL [20] { + function: _+_ + args: { + IDENT [21] { + name: @ac:0:0 + } + LIST [22] { + elements: { + 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: { + CALL [30] { + function: _+_ + args: { + IDENT [31] { + name: @it:1:0 + } + CONSTANT [32] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [33] { + name: @ac:1:0 + } + } + } + } + } + } + } + } + result: { + IDENT [34] { + name: @ac:0: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:0:0 + iter_range: { + IDENT [17] { + name: @index1 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [18] { + elements: { + } + } + } + loop_condition: { + CONSTANT [19] { value: true } + } + loop_step: { + CALL [20] { + function: _+_ + args: { + IDENT [21] { + name: @ac:0:0 + } + LIST [22] { + elements: { + COMPREHENSION [23] { + iter_var: @it:1:0 + iter_range: { + IDENT [24] { + name: @index2 + } + } + accu_var: @ac:1: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:1:0 + } + IDENT [30] { + name: @it:0:0 + } + } + } + CALL [31] { + function: _+_ + args: { + IDENT [32] { + name: @ac:1:0 + } + LIST [33] { + elements: { + IDENT [34] { + name: @it:1:0 + } + } + } + } + } + IDENT [35] { + name: @ac:1:0 + } + } + } + } + result: { + IDENT [36] { + name: @ac:1:0 + } + } + } + } + } + } + } + } + result: { + IDENT [37] { + name: @ac:0:0 + } + } + } + IDENT [38] { + 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: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: { + COMPREHENSION [15] { + iter_var: @it:1:0 + iter_range: { + IDENT [16] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [17] { + elements: { + } + } + } + loop_condition: { + CONSTANT [18] { value: true } + } + loop_step: { + CALL [19] { + function: _+_ + args: { + IDENT [20] { + name: @ac:1:0 + } + LIST [21] { + elements: { + CALL [22] { + function: _+_ + args: { + IDENT [23] { + name: @it:1:0 + } + CONSTANT [24] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [25] { + name: @ac:1:0 + } + } + } + } + } + } + } + } + result: { + IDENT [26] { + name: @ac:0:0 + } + } + } + COMPREHENSION [27] { + iter_var: @it:0:0 + iter_range: { + IDENT [28] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [29] { + elements: { + } + } + } + loop_condition: { + CONSTANT [30] { value: true } + } + loop_step: { + CALL [31] { + function: _+_ + args: { + IDENT [32] { + name: @ac:0:0 + } + LIST [33] { + elements: { + COMPREHENSION [34] { + iter_var: @it:1:0 + iter_range: { + IDENT [35] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [36] { + elements: { + } + } + } + loop_condition: { + CONSTANT [37] { value: true } + } + loop_step: { + CALL [38] { + function: _+_ + args: { + IDENT [39] { + name: @ac:1:0 + } + LIST [40] { + elements: { + CALL [41] { + function: _+_ + args: { + IDENT [42] { + name: @it:1:0 + } + CONSTANT [43] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [44] { + name: @ac:1:0 + } + } + } + } + } + } + } + } + result: { + IDENT [45] { + name: @ac:0: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 + } + } + } + } + } + CALL [18] { + function: @in + args: { + CONSTANT [19] { value: 3 } + LIST [20] { + elements: { + CONSTANT [21] { value: 3 } + IDENT [22] { + name: @index1 + } + } + } + } + } + } + } + CALL [23] { + function: _&&_ + args: { + IDENT [24] { + name: @index2 + } + CALL [25] { + function: _&&_ + args: { + IDENT [26] { + name: @index3 + } + IDENT [27] { + 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 } + } + } + LIST [13] { + elements: { + LIST [14] { + elements: { + CONSTANT [15] { value: 3 } + CONSTANT [16] { value: 4 } + } + } + } + } + COMPREHENSION [17] { + iter_var: @it:1:0 + iter_range: { + IDENT [18] { + name: @index1 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [19] { + elements: { + } + } + } + loop_condition: { + CONSTANT [20] { value: true } + } + loop_step: { + CALL [21] { + function: _+_ + args: { + IDENT [22] { + name: @ac:1:0 + } + IDENT [23] { + name: @index2 + } + } + } + } + result: { + IDENT [24] { + name: @ac:1:0 + } + } + } + LIST [25] { + elements: { + IDENT [26] { + name: @index3 + } + } + } + } + } + CALL [27] { + function: _==_ + args: { + COMPREHENSION [28] { + iter_var: @it:0:0 + iter_range: { + IDENT [29] { + name: @index1 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [30] { + elements: { + } + } + } + loop_condition: { + CONSTANT [31] { value: true } + } + loop_step: { + CALL [32] { + function: _+_ + args: { + IDENT [33] { + name: @ac:0:0 + } + IDENT [34] { + name: @index4 + } + } + } + } + result: { + IDENT [35] { + name: @ac:0:0 + } + } + } + LIST [36] { + elements: { + IDENT [37] { + name: @index0 + } + IDENT [38] { + 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: { + IDENT [9] { + name: @index0 + } + CALL [10] { + function: _-_ + args: { + IDENT [11] { + name: x + } + CONSTANT [12] { value: 1 } + } + } + CONSTANT [13] { value: 5 } + } + } + LIST [14] { + elements: { + IDENT [15] { + name: @index1 + } + } + } + } + } + CALL [16] { + function: _||_ + args: { + COMPREHENSION [17] { + iter_var: @it:0:0 + iter_range: { + IDENT [18] { + name: @index2 + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [19] { value: false } + } + loop_condition: { + CALL [20] { + function: @not_strictly_false + args: { + CALL [21] { + function: !_ + args: { + IDENT [22] { + name: @ac:0:0 + } + } + } + } + } + } + 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 + } + } + } + IDENT [31] { + 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:0:0 + iter_range: { + COMPREHENSION [7] { + iter_var: @it:1:0 + iter_range: { + IDENT [8] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [9] { + elements: { + } + } + } + loop_condition: { + CONSTANT [10] { value: true } + } + loop_step: { + CALL [11] { + function: _+_ + args: { + IDENT [12] { + name: @ac:1:0 + } + LIST [13] { + elements: { + LIST [14] { + elements: { + CALL [15] { + function: _+_ + args: { + IDENT [16] { + name: @it:1:0 + } + IDENT [17] { + name: @it:1:0 + } + } + } + CALL [18] { + function: _+_ + args: { + IDENT [19] { + name: @it:1:0 + } + IDENT [20] { + name: @it:1:0 + } + } + } + } + } + } + } + } + } + } + result: { + IDENT [21] { + name: @ac:1:0 + } + } + } + } + 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: { + LIST [27] { + elements: { + CALL [28] { + function: _+_ + args: { + IDENT [29] { + name: @it:0:0 + } + IDENT [30] { + name: @it:0:0 + } + } + } + CALL [31] { + function: _+_ + args: { + IDENT [32] { + name: @it:0:0 + } + IDENT [33] { + 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 + } + SELECT [5] { + SELECT [6] { + IDENT [7] { + name: @index0 + }.payload + }.single_int64 + } + CALL [8] { + function: _?_:_ + args: { + SELECT [9] { + IDENT [10] { + name: @index0 + }.payload~presence_test + } + IDENT [11] { + name: @index1 + } + CONSTANT [12] { value: 0 } + } + } + } + } + CALL [13] { + function: _==_ + args: { + IDENT [14] { + name: @index2 + } + CONSTANT [15] { 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] { + SELECT [4] { + IDENT [5] { + name: msg + }.oneof_type + }.payload + } + SELECT [6] { + IDENT [7] { + name: @index0 + }.single_int64 + } + SELECT [8] { + SELECT [9] { + IDENT [10] { + name: msg + }.oneof_type + }.payload~presence_test + } + CALL [11] { + function: _?_:_ + args: { + IDENT [12] { + name: @index2 + } + IDENT [13] { + name: @index1 + } + CALL [14] { + function: _*_ + args: { + IDENT [15] { + name: @index1 + } + CONSTANT [16] { value: 0 } + } + } + } + } + } + } + CALL [17] { + function: _==_ + args: { + IDENT [18] { + name: @index3 + } + CONSTANT [19] { 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: { + SELECT [9] { + IDENT [10] { + name: @index0 + }.single_int64~presence_test + } + IDENT [11] { + name: @index1 + } + CALL [12] { + function: _*_ + args: { + IDENT [13] { + name: @index1 + } + CONSTANT [14] { value: 0 } + } + } + } + } + } + } + CALL [15] { + function: _==_ + args: { + IDENT [16] { + name: @index2 + } + CONSTANT [17] { 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] { + SELECT [4] { + IDENT [5] { + name: msg + }.oneof_type + }.payload + } + SELECT [6] { + IDENT [7] { + name: @index0 + }.map_string_string + } + SELECT [8] { + SELECT [9] { + IDENT [10] { + name: msg + }.oneof_type + }.payload~presence_test + } + CALL [11] { + function: _&&_ + args: { + SELECT [12] { + IDENT [13] { + name: msg + }.oneof_type~presence_test + } + IDENT [14] { + name: @index2 + } + } + } + CALL [15] { + function: _&&_ + args: { + IDENT [16] { + name: @index3 + } + SELECT [17] { + IDENT [18] { + name: @index0 + }.single_int64~presence_test + } + } + } + CALL [19] { + function: _&&_ + args: { + SELECT [20] { + IDENT [21] { + name: @index0 + }.map_string_string~presence_test + } + SELECT [22] { + IDENT [23] { + name: @index1 + }.key~presence_test + } + } + } + CALL [24] { + function: _==_ + args: { + SELECT [25] { + IDENT [26] { + name: @index1 + }.key + } + CONSTANT [27] { value: "A" } + } + } + } + } + CALL [28] { + function: _?_:_ + args: { + IDENT [29] { + name: @index4 + } + CALL [30] { + function: _?_:_ + args: { + IDENT [31] { + name: @index5 + } + IDENT [32] { + name: @index6 + } + CONSTANT [33] { value: false } + } + } + CONSTANT [34] { 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: { + LIST [3] { + elements: { + CALL [4] { + function: optional.none + args: { + } + } + IDENT [5] { + name: opt_x + } + } + optional_indices: [0, 1] + } + LIST [6] { + elements: { + CONSTANT [7] { value: 5 } + } + } + LIST [8] { + elements: { + CONSTANT [9] { value: 10 } + CALL [10] { + function: optional.none + args: { + } + } + IDENT [11] { + name: @index0 + } + IDENT [12] { + name: @index0 + } + } + optional_indices: [0] + } + } + } + CALL [13] { + function: _==_ + args: { + IDENT [14] { + name: @index2 + } + LIST [15] { + elements: { + CONSTANT [16] { value: 10 } + IDENT [17] { + name: @index1 + } + IDENT [18] { + name: @index1 + } + } + } + } + } + } +} +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: { + MAP [3] { + MAP_ENTRY [4] { + key: { + CONSTANT [5] { value: "hello" } + } + optional_entry: true + value: { + CALL [6] { + function: optional.of + args: { + CONSTANT [7] { value: "hello" } + } + } + } + } + } + CALL [8] { + function: _[_] + args: { + IDENT [9] { + name: @index0 + } + CONSTANT [10] { value: "hello" } + } + } + } + } + CALL [11] { + function: _==_ + args: { + CALL [12] { + function: _+_ + args: { + IDENT [13] { + name: @index1 + } + IDENT [14] { + name: @index1 + } + } + } + CONSTANT [15] { 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" } + } + } + } + MAP [7] { + MAP_ENTRY [8] { + key: { + CONSTANT [9] { value: "key" } + } + optional_entry: true + value: { + CALL [10] { + function: optional.of + args: { + CONSTANT [11] { value: "test" } + } + } + } + } + } + CALL [12] { + function: or + target: { + CALL [13] { + function: _[?_] + args: { + IDENT [14] { + name: @index1 + } + CONSTANT [15] { value: "bogus" } + } + } + } + args: { + CALL [16] { + function: _[?_] + args: { + IDENT [17] { + name: @index0 + } + CONSTANT [18] { value: "bogus" } + } + } + } + } + CALL [19] { + function: orValue + target: { + IDENT [20] { + name: @index2 + } + } + args: { + CALL [21] { + function: _[_] + args: { + IDENT [22] { + name: @index0 + } + CONSTANT [23] { value: "key" } + } + } + } + } + } + } + CALL [24] { + function: _==_ + args: { + IDENT [25] { + name: @index3 + } + CONSTANT [26] { 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: { + SELECT [11] { + IDENT [12] { + name: @index0 + }.single_int32 + } + SELECT [13] { + IDENT [14] { + name: @index0 + }.single_int64 + } + } + } + } + } + CALL [15] { + function: _==_ + args: { + IDENT [16] { + name: @index1 + } + CONSTANT [17] { 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: { + CONSTANT [5] { value: "h" } + CONSTANT [6] { value: "e" } + } + } + CONSTANT [7] { value: "l" } + } + } + CALL [8] { + function: _+_ + args: { + CALL [9] { + function: _+_ + args: { + IDENT [10] { + name: @index0 + } + CONSTANT [11] { value: "l" } + } + } + CONSTANT [12] { value: "o" } + } + } + } + } + CALL [13] { + function: matches + target: { + CALL [14] { + function: _+_ + args: { + IDENT [15] { + name: @index1 + } + CONSTANT [16] { value: " world" } + } + } + } + args: { + IDENT [17] { + name: @index1 + } + } + } + } +} +Test case: CALL_ARGUMENT_NESTED_NO_COMMON_SUBEXPR +Source: 'hello world'.matches('h' + 'e' + 'l' + 'l' + 'o') +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + CALL [3] { + function: _+_ + args: { + CALL [4] { + function: _+_ + args: { + CONSTANT [5] { value: "h" } + CONSTANT [6] { value: "e" } + } + } + CONSTANT [7] { value: "l" } + } + } + CALL [8] { + function: _+_ + args: { + CALL [9] { + function: _+_ + args: { + IDENT [10] { + name: @index0 + } + CONSTANT [11] { value: "l" } + } + } + CONSTANT [12] { value: "o" } + } + } + } + } + CALL [13] { + function: matches + target: { + CONSTANT [14] { value: "hello world" } + } + args: { + IDENT [15] { + name: @index1 + } + } + } + } +} +Test case: CALL_TARGET_NESTED_NO_COMMON_SUBEXPR +Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('hello') +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + CALL [3] { + function: _+_ + args: { + CALL [4] { + function: _+_ + args: { + CONSTANT [5] { value: "h" } + CONSTANT [6] { value: "e" } + } + } + CONSTANT [7] { value: "l" } + } + } + CALL [8] { + function: _+_ + args: { + CALL [9] { + function: _+_ + args: { + IDENT [10] { + name: @index0 + } + CONSTANT [11] { value: "l" } + } + } + CONSTANT [12] { value: "o" } + } + } + } + } + CALL [13] { + function: matches + target: { + CALL [14] { + function: _+_ + args: { + IDENT [15] { + name: @index1 + } + CONSTANT [16] { value: " world" } + } + } + } + args: { + CONSTANT [17] { 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 [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + CALL [3] { + function: _+_ + args: { + CALL [4] { + function: _+_ + args: { + CONSTANT [5] { value: "h" } + CONSTANT [6] { value: "e" } + } + } + CONSTANT [7] { value: "l" } + } + } + CALL [8] { + function: _+_ + args: { + CALL [9] { + function: _+_ + args: { + IDENT [10] { + name: @index0 + } + CONSTANT [11] { value: "l" } + } + } + CONSTANT [12] { value: "o" } + } + } + CALL [13] { + function: _+_ + args: { + CALL [14] { + function: _+_ + args: { + CONSTANT [15] { value: "w" } + CONSTANT [16] { value: "o" } + } + } + CONSTANT [17] { value: "r" } + } + } + CALL [18] { + function: _+_ + args: { + CALL [19] { + function: _+_ + args: { + IDENT [20] { + name: @index2 + } + CONSTANT [21] { value: "l" } + } + } + CONSTANT [22] { value: "d" } + } + } + } + } + CALL [23] { + function: matches + target: { + CALL [24] { + function: _+_ + args: { + IDENT [25] { + name: @index1 + } + CONSTANT [26] { value: " world" } + } + } + } + args: { + IDENT [27] { + name: @index3 + } + } + } + } +} +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: pure_custom_func + args: { + SELECT [10] { + IDENT [11] { + name: @index0 + }.single_int32 + } + } + } + CALL [12] { + function: _+_ + args: { + CALL [13] { + function: _+_ + args: { + IDENT [14] { + name: @index1 + } + IDENT [15] { + name: @index2 + } + } + } + IDENT [16] { + name: @index1 + } + } + } + CALL [17] { + function: pure_custom_func + args: { + SELECT [18] { + IDENT [19] { + name: msg + }.single_int64 + } + } + } + } + } + CALL [20] { + function: _+_ + args: { + IDENT [21] { + name: @index3 + } + 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 new file mode 100644 index 000000000..d9211978c --- /dev/null +++ b/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_3.baseline @@ -0,0 +1,3719 @@ +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: { + CONSTANT [10] { value: 2 } + IDENT [11] { + name: @index0 + } + } + } + IDENT [12] { + name: @index0 + } + } + } + CONSTANT [13] { value: 1 } + } + } + } + } + CALL [14] { + function: _==_ + args: { + IDENT [15] { + name: @index1 + } + CONSTANT [16] { 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: { + IDENT [13] { + name: @index0 + } + IDENT [14] { + name: @index0 + } + } + } + IDENT [15] { + name: @index1 + } + } + } + IDENT [16] { + name: @index1 + } + } + } + } + } + CALL [17] { + function: _==_ + args: { + IDENT [18] { + name: @index2 + } + CONSTANT [19] { 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: { + CONSTANT [18] { value: 5 } + IDENT [19] { + name: @index0 + } + } + } + IDENT [20] { + name: @index0 + } + } + } + IDENT [21] { + name: @index1 + } + } + } + CALL [22] { + function: _+_ + args: { + CALL [23] { + function: _+_ + args: { + CALL [24] { + function: _+_ + args: { + IDENT [25] { + name: @index3 + } + IDENT [26] { + name: @index1 + } + } + } + IDENT [27] { + name: @index2 + } + } + } + IDENT [28] { + name: @index2 + } + } + } + } + } + CALL [29] { + function: _==_ + args: { + IDENT [30] { + name: @index4 + } + CONSTANT [31] { 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: timestamp + args: { + CALL [4] { + function: int + args: { + CALL [5] { + function: timestamp + args: { + CONSTANT [6] { value: 1000000000 } + } + } + } + } + } + } + CALL [7] { + function: timestamp + args: { + CALL [8] { + function: int + args: { + CALL [9] { + function: timestamp + args: { + CONSTANT [10] { value: 50 } + } + } + } + } + } + } + CALL [11] { + function: timestamp + args: { + CALL [12] { + function: int + args: { + CALL [13] { + function: timestamp + args: { + CONSTANT [14] { value: 200 } + } + } + } + } + } + } + CALL [15] { + function: timestamp + args: { + CALL [16] { + function: int + args: { + CALL [17] { + function: timestamp + args: { + CONSTANT [18] { value: 75 } + } + } + } + } + } + } + CALL [19] { + function: getFullYear + target: { + IDENT [20] { + name: @index0 + } + } + args: { + } + } + CALL [21] { + function: getFullYear + target: { + IDENT [22] { + name: @index2 + } + } + args: { + } + } + CALL [23] { + function: _+_ + args: { + CALL [24] { + function: _+_ + args: { + IDENT [25] { + name: @index4 + } + CALL [26] { + function: getFullYear + target: { + IDENT [27] { + name: @index3 + } + } + args: { + } + } + } + } + CALL [28] { + function: getFullYear + target: { + IDENT [29] { + name: @index1 + } + } + args: { + } + } + } + } + CALL [30] { + function: _+_ + args: { + CALL [31] { + function: _+_ + args: { + CALL [32] { + function: _+_ + args: { + IDENT [33] { + name: @index6 + } + IDENT [34] { + name: @index4 + } + } + } + CALL [35] { + function: getSeconds + target: { + IDENT [36] { + name: @index1 + } + } + args: { + } + } + } + } + IDENT [37] { + name: @index5 + } + } + } + CALL [38] { + function: _+_ + args: { + CALL [39] { + function: _+_ + args: { + CALL [40] { + function: _+_ + args: { + IDENT [41] { + name: @index7 + } + IDENT [42] { + name: @index5 + } + } + } + CALL [43] { + function: getMinutes + target: { + IDENT [44] { + name: @index3 + } + } + args: { + } + } + } + } + IDENT [45] { + name: @index4 + } + } + } + } + } + CALL [46] { + function: _==_ + args: { + IDENT [47] { + name: @index8 + } + CONSTANT [48] { 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: "e" } + } + value: { + MAP [6] { + MAP_ENTRY [7] { + key: { + CONSTANT [8] { value: "b" } + } + value: { + CONSTANT [9] { value: 1 } + } + } + } + } + } + } + MAP [10] { + MAP_ENTRY [11] { + key: { + CONSTANT [12] { value: "b" } + } + value: { + CONSTANT [13] { value: 1 } + } + } + } + } + } + MAP [14] { + MAP_ENTRY [15] { + key: { + CONSTANT [16] { value: "a" } + } + value: { + IDENT [17] { + name: @index1 + } + } + } + MAP_ENTRY [18] { + key: { + CONSTANT [19] { value: "c" } + } + value: { + IDENT [20] { + name: @index1 + } + } + } + MAP_ENTRY [21] { + key: { + CONSTANT [22] { value: "d" } + } + value: { + IDENT [23] { + name: @index0 + } + } + } + MAP_ENTRY [24] { + key: { + CONSTANT [25] { value: "e" } + } + value: { + IDENT [26] { + name: @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]] +=====> +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] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.single_int64 + } + SELECT [7] { + SELECT [8] { + IDENT [9] { + name: msg + }.oneof_type + }.payload + } + CALL [10] { + function: _+_ + args: { + CALL [11] { + function: _+_ + args: { + IDENT [12] { + name: @index0 + } + SELECT [13] { + IDENT [14] { + name: @index1 + }.single_int32 + } + } + } + IDENT [15] { + name: @index0 + } + } + } + SELECT [16] { + SELECT [17] { + SELECT [18] { + IDENT [19] { + name: @index1 + }.oneof_type + }.payload + }.single_int64 + } + CALL [20] { + function: _+_ + args: { + CALL [21] { + function: _+_ + args: { + IDENT [22] { + name: @index2 + } + SELECT [23] { + IDENT [24] { + name: msg + }.single_int64 + } + } + } + IDENT [25] { + name: @index3 + } + } + } + } + } + CALL [26] { + function: _==_ + args: { + IDENT [27] { + name: @index4 + } + CONSTANT [28] { 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] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.oneof_type + } + SELECT [7] { + SELECT [8] { + IDENT [9] { + name: @index0 + }.payload + }.oneof_type + } + SELECT [10] { + SELECT [11] { + SELECT [12] { + IDENT [13] { + name: @index1 + }.payload + }.oneof_type + }.payload + } + SELECT [14] { + SELECT [15] { + SELECT [16] { + IDENT [17] { + name: @index1 + }.child + }.child + }.payload + } + } + } + CALL [18] { + function: _||_ + args: { + CALL [19] { + function: _||_ + args: { + CONSTANT [20] { value: true } + SELECT [21] { + IDENT [22] { + name: @index2 + }.single_bool + } + } + } + SELECT [23] { + IDENT [24] { + name: @index3 + }.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: { + SELECT [3] { + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.map_int32_int64 + } + CALL [7] { + function: _[_] + args: { + IDENT [8] { + name: @index0 + } + CONSTANT [9] { value: 1 } + } + } + } + } + CALL [10] { + function: _==_ + args: { + CALL [11] { + function: _+_ + args: { + CALL [12] { + function: _+_ + args: { + IDENT [13] { + name: @index1 + } + IDENT [14] { + name: @index1 + } + } + } + IDENT [15] { + name: @index1 + } + } + } + CONSTANT [16] { 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: { + IDENT [10] { + name: @index0 + } + CONSTANT [11] { value: 0 } + } + } + CALL [12] { + function: _[_] + args: { + IDENT [13] { + name: @index0 + } + CONSTANT [14] { value: 1 } + } + } + } + } + CALL [15] { + function: _[_] + args: { + IDENT [16] { + name: @index0 + } + CONSTANT [17] { value: 2 } + } + } + } + } + } + } + CALL [18] { + function: _==_ + args: { + IDENT [19] { + name: @index1 + } + CONSTANT [20] { 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 +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + SELECT [3] { + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.oneof_type + } + SELECT [7] { + SELECT [8] { + SELECT [9] { + IDENT [10] { + name: @index0 + }.payload + }.oneof_type + }.payload + } + } + } + SELECT [11] { + SELECT [12] { + SELECT [13] { + IDENT [14] { + name: @index1 + }.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: { + IDENT [6] { + name: @index0 + } + CALL [7] { + function: _*_ + args: { + CALL [8] { + function: _+_ + args: { + IDENT [9] { + name: @index0 + } + CONSTANT [10] { value: 1 } + } + } + CONSTANT [11] { value: 2 } + } + } + } + } + } + } + CALL [12] { + function: _?_:_ + args: { + CONSTANT [13] { value: false } + CONSTANT [14] { value: false } + CALL [15] { + function: _==_ + args: { + IDENT [16] { + name: @index1 + } + CONSTANT [17] { 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: { + IDENT [9] { + name: @index0 + } + CONSTANT [10] { value: 0 } + } + } + CALL [11] { + function: _?_:_ + args: { + CALL [12] { + function: _>_ + args: { + IDENT [13] { + name: @index1 + } + CONSTANT [14] { value: 0 } + } + } + CALL [15] { + function: _+_ + args: { + IDENT [16] { + name: @index0 + } + IDENT [17] { + name: @index1 + } + } + } + CONSTANT [18] { value: 0 } + } + } + CONSTANT [19] { value: 0 } + } + } + } + } + CALL [20] { + function: _==_ + args: { + IDENT [21] { + name: @index2 + } + CONSTANT [22] { 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 + } + } + } + COMPREHENSION [16] { + iter_var: @it:0:0 + iter_range: { + LIST [17] { + elements: { + CONSTANT [18] { value: 2 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [19] { value: false } + } + loop_condition: { + CALL [20] { + function: @not_strictly_false + args: { + CALL [21] { + function: !_ + args: { + IDENT [22] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + 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 [28] { + name: @ac:0:0 + } + } + } + CALL [29] { + function: size + args: { + LIST [30] { + elements: { + IDENT [31] { + name: @index0 + } + } + } + } + } + CALL [32] { + function: size + args: { + LIST [33] { + elements: { + IDENT [34] { + name: @index1 + } + } + } + } + } + CALL [35] { + function: _+_ + args: { + CALL [36] { + function: _+_ + args: { + CALL [37] { + function: _+_ + args: { + IDENT [38] { + name: @index2 + } + IDENT [39] { + name: @index2 + } + } + } + IDENT [40] { + name: @index3 + } + } + } + IDENT [41] { + name: @index3 + } + } + } + } + } + CALL [42] { + function: _==_ + args: { + IDENT [43] { + name: @index4 + } + CONSTANT [44] { 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 + } + } + } + COMPREHENSION [16] { + iter_var: @it:0:1 + iter_range: { + LIST [17] { + elements: { + CONSTANT [18] { value: "a" } + } + } + } + accu_var: @ac:0:1 + accu_init: { + CONSTANT [19] { value: false } + } + loop_condition: { + CALL [20] { + function: @not_strictly_false + args: { + CALL [21] { + function: !_ + args: { + IDENT [22] { + name: @ac:0:1 + } + } + } + } + } + } + loop_step: { + 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 [28] { + name: @ac:0:1 + } + } + } + LIST [29] { + elements: { + IDENT [30] { + name: @index0 + } + } + } + LIST [31] { + elements: { + IDENT [32] { + name: @index1 + } + } + } + CALL [33] { + function: _+_ + args: { + CALL [34] { + function: _+_ + args: { + CALL [35] { + function: _+_ + args: { + IDENT [36] { + name: @index2 + } + IDENT [37] { + name: @index2 + } + } + } + IDENT [38] { + name: @index3 + } + } + } + IDENT [39] { + name: @index3 + } + } + } + } + } + CALL [40] { + function: _==_ + args: { + IDENT [41] { + name: @index4 + } + LIST [42] { + elements: { + CONSTANT [43] { value: true } + CONSTANT [44] { value: true } + CONSTANT [45] { value: true } + CONSTANT [46] { 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 } + } + } + LIST [11] { + elements: { + IDENT [12] { + name: @index1 + } + IDENT [13] { + name: @index1 + } + IDENT [14] { + name: @index1 + } + } + } + } + } + CALL [15] { + function: _==_ + args: { + COMPREHENSION [16] { + iter_var: @it:0:0 + iter_range: { + IDENT [17] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [18] { + elements: { + } + } + } + loop_condition: { + CONSTANT [19] { value: true } + } + loop_step: { + CALL [20] { + function: _+_ + args: { + IDENT [21] { + name: @ac:0:0 + } + LIST [22] { + elements: { + 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: { + CALL [30] { + function: _+_ + args: { + IDENT [31] { + name: @it:1:0 + } + CONSTANT [32] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [33] { + name: @ac:1:0 + } + } + } + } + } + } + } + } + result: { + IDENT [34] { + name: @ac:0: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:0:0 + iter_range: { + IDENT [17] { + name: @index1 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [18] { + elements: { + } + } + } + loop_condition: { + CONSTANT [19] { value: true } + } + loop_step: { + CALL [20] { + function: _+_ + args: { + IDENT [21] { + name: @ac:0:0 + } + LIST [22] { + elements: { + COMPREHENSION [23] { + iter_var: @it:1:0 + iter_range: { + IDENT [24] { + name: @index2 + } + } + accu_var: @ac:1: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:1:0 + } + IDENT [30] { + name: @it:0:0 + } + } + } + CALL [31] { + function: _+_ + args: { + IDENT [32] { + name: @ac:1:0 + } + LIST [33] { + elements: { + IDENT [34] { + name: @it:1:0 + } + } + } + } + } + IDENT [35] { + name: @ac:1:0 + } + } + } + } + result: { + IDENT [36] { + name: @ac:1:0 + } + } + } + } + } + } + } + } + result: { + IDENT [37] { + name: @ac:0:0 + } + } + } + IDENT [38] { + 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: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: { + COMPREHENSION [15] { + iter_var: @it:1:0 + iter_range: { + IDENT [16] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [17] { + elements: { + } + } + } + loop_condition: { + CONSTANT [18] { value: true } + } + loop_step: { + CALL [19] { + function: _+_ + args: { + IDENT [20] { + name: @ac:1:0 + } + LIST [21] { + elements: { + CALL [22] { + function: _+_ + args: { + IDENT [23] { + name: @it:1:0 + } + CONSTANT [24] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [25] { + name: @ac:1:0 + } + } + } + } + } + } + } + } + result: { + IDENT [26] { + name: @ac:0:0 + } + } + } + COMPREHENSION [27] { + iter_var: @it:0:0 + iter_range: { + IDENT [28] { + name: @index0 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [29] { + elements: { + } + } + } + loop_condition: { + CONSTANT [30] { value: true } + } + loop_step: { + CALL [31] { + function: _+_ + args: { + IDENT [32] { + name: @ac:0:0 + } + LIST [33] { + elements: { + COMPREHENSION [34] { + iter_var: @it:1:0 + iter_range: { + IDENT [35] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [36] { + elements: { + } + } + } + loop_condition: { + CONSTANT [37] { value: true } + } + loop_step: { + CALL [38] { + function: _+_ + args: { + IDENT [39] { + name: @ac:1:0 + } + LIST [40] { + elements: { + CALL [41] { + function: _+_ + args: { + IDENT [42] { + name: @it:1:0 + } + CONSTANT [43] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [44] { + name: @ac:1:0 + } + } + } + } + } + } + } + } + result: { + IDENT [45] { + name: @ac:0: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 + } + } + } + } + } + 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:1:0 + iter_range: { + IDENT [18] { + name: @index1 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [19] { + elements: { + } + } + } + loop_condition: { + CONSTANT [20] { value: true } + } + loop_step: { + CALL [21] { + function: _+_ + args: { + IDENT [22] { + name: @ac:1:0 + } + IDENT [23] { + name: @index2 + } + } + } + } + result: { + IDENT [24] { + name: @ac:1:0 + } + } + } + } + } + CALL [25] { + function: _==_ + args: { + COMPREHENSION [26] { + iter_var: @it:0:0 + iter_range: { + IDENT [27] { + name: @index1 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [28] { + elements: { + } + } + } + loop_condition: { + CONSTANT [29] { value: true } + } + loop_step: { + CALL [30] { + function: _+_ + args: { + IDENT [31] { + name: @ac:0:0 + } + LIST [32] { + elements: { + IDENT [33] { + name: @index3 + } + } + } + } + } + } + result: { + IDENT [34] { + name: @ac:0:0 + } + } + } + LIST [35] { + elements: { + IDENT [36] { + name: @index0 + } + IDENT [37] { + 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 } + } + } + 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 } + } + } + } + } + } + } + CALL [15] { + function: _||_ + args: { + COMPREHENSION [16] { + iter_var: @it:0:0 + iter_range: { + IDENT [17] { + name: @index1 + } + } + 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 + } + } + } + } + } + } + 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 + } + } + } + IDENT [30] { + 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:0:0 + iter_range: { + COMPREHENSION [7] { + iter_var: @it:1:0 + iter_range: { + IDENT [8] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [9] { + elements: { + } + } + } + loop_condition: { + CONSTANT [10] { value: true } + } + loop_step: { + CALL [11] { + function: _+_ + args: { + IDENT [12] { + name: @ac:1:0 + } + LIST [13] { + elements: { + LIST [14] { + elements: { + CALL [15] { + function: _+_ + args: { + IDENT [16] { + name: @it:1:0 + } + IDENT [17] { + name: @it:1:0 + } + } + } + CALL [18] { + function: _+_ + args: { + IDENT [19] { + name: @it:1:0 + } + IDENT [20] { + name: @it:1:0 + } + } + } + } + } + } + } + } + } + } + result: { + IDENT [21] { + name: @ac:1:0 + } + } + } + } + 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: { + LIST [27] { + elements: { + CALL [28] { + function: _+_ + args: { + IDENT [29] { + name: @it:0:0 + } + IDENT [30] { + name: @it:0:0 + } + } + } + CALL [31] { + function: _+_ + args: { + IDENT [32] { + name: @it:0:0 + } + IDENT [33] { + 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: { + SELECT [6] { + IDENT [7] { + name: @index0 + }.payload~presence_test + } + SELECT [8] { + SELECT [9] { + IDENT [10] { + name: @index0 + }.payload + }.single_int64 + } + CONSTANT [11] { value: 0 } + } + } + } + } + CALL [12] { + function: _==_ + args: { + IDENT [13] { + name: @index1 + } + CONSTANT [14] { 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] { + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.single_int64 + } + CALL [7] { + function: _?_:_ + args: { + SELECT [8] { + SELECT [9] { + IDENT [10] { + name: msg + }.oneof_type + }.payload~presence_test + } + IDENT [11] { + name: @index0 + } + CALL [12] { + function: _*_ + args: { + IDENT [13] { + name: @index0 + } + CONSTANT [14] { value: 0 } + } + } + } + } + } + } + CALL [15] { + function: _==_ + args: { + IDENT [16] { + name: @index1 + } + CONSTANT [17] { 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] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.single_int64 + } + SELECT [7] { + SELECT [8] { + SELECT [9] { + IDENT [10] { + name: msg + }.oneof_type + }.payload + }.single_int64~presence_test + } + } + } + CALL [11] { + function: _==_ + args: { + CALL [12] { + function: _?_:_ + args: { + IDENT [13] { + name: @index1 + } + IDENT [14] { + name: @index0 + } + CALL [15] { + function: _*_ + args: { + IDENT [16] { + name: @index0 + } + CONSTANT [17] { value: 0 } + } + } + } + } + CONSTANT [18] { 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] { + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.map_string_string + } + SELECT [7] { + SELECT [8] { + IDENT [9] { + name: msg + }.oneof_type + }.payload + } + 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 [16] { + function: _?_:_ + args: { + CALL [17] { + function: _&&_ + args: { + SELECT [18] { + IDENT [19] { + name: @index1 + }.map_string_string~presence_test + } + SELECT [20] { + IDENT [21] { + name: @index0 + }.key~presence_test + } + } + } + CALL [22] { + function: _==_ + args: { + SELECT [23] { + IDENT [24] { + name: @index0 + }.key + } + CONSTANT [25] { value: "A" } + } + } + CONSTANT [26] { value: false } + } + } + } + } + CALL [27] { + function: _?_:_ + args: { + CALL [28] { + function: _&&_ + args: { + IDENT [29] { + name: @index2 + } + SELECT [30] { + IDENT [31] { + name: @index1 + }.single_int64~presence_test + } + } + } + IDENT [32] { + name: @index3 + } + CONSTANT [33] { 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: { + LIST [3] { + elements: { + CALL [4] { + function: optional.none + args: { + } + } + IDENT [5] { + name: opt_x + } + } + optional_indices: [0, 1] + } + LIST [6] { + elements: { + CONSTANT [7] { value: 5 } + } + } + } + } + CALL [8] { + function: _==_ + args: { + LIST [9] { + elements: { + CONSTANT [10] { value: 10 } + CALL [11] { + function: optional.none + args: { + } + } + IDENT [12] { + name: @index0 + } + IDENT [13] { + name: @index0 + } + } + optional_indices: [0] + } + LIST [14] { + elements: { + CONSTANT [15] { value: 10 } + IDENT [16] { + name: @index1 + } + IDENT [17] { + name: @index1 + } + } + } + } + } + } +} +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: { + MAP [8] { + MAP_ENTRY [9] { + key: { + CONSTANT [10] { value: "key" } + } + optional_entry: true + value: { + CALL [11] { + function: optional.of + args: { + CONSTANT [12] { value: "test" } + } + } + } + } + } + CONSTANT [13] { value: "bogus" } + } + } + CALL [14] { + function: orValue + target: { + CALL [15] { + function: or + target: { + IDENT [16] { + name: @index1 + } + } + 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" } + } + } + } + } + } + } + CALL [23] { + function: _==_ + args: { + IDENT [24] { + name: @index2 + } + CONSTANT [25] { 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: { + CONSTANT [6] { value: "h" } + CONSTANT [7] { value: "e" } + } + } + CONSTANT [8] { value: "l" } + } + } + CONSTANT [9] { value: "l" } + } + } + CALL [10] { + function: _+_ + args: { + IDENT [11] { + name: @index0 + } + CONSTANT [12] { value: "o" } + } + } + } + } + CALL [13] { + function: matches + target: { + CALL [14] { + function: _+_ + args: { + IDENT [15] { + name: @index1 + } + CONSTANT [16] { value: " world" } + } + } + } + args: { + IDENT [17] { + name: @index1 + } + } + } + } +} +Test case: CALL_ARGUMENT_NESTED_NO_COMMON_SUBEXPR +Source: 'hello 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: { + CONSTANT [6] { value: "h" } + CONSTANT [7] { value: "e" } + } + } + CONSTANT [8] { value: "l" } + } + } + CONSTANT [9] { value: "l" } + } + } + } + } + CALL [10] { + function: matches + target: { + CONSTANT [11] { value: "hello world" } + } + args: { + CALL [12] { + function: _+_ + args: { + IDENT [13] { + name: @index0 + } + CONSTANT [14] { 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: { + LIST [2] { + elements: { + CALL [3] { + function: _+_ + args: { + CALL [4] { + function: _+_ + args: { + CALL [5] { + function: _+_ + args: { + CONSTANT [6] { value: "h" } + CONSTANT [7] { value: "e" } + } + } + CONSTANT [8] { value: "l" } + } + } + CONSTANT [9] { value: "l" } + } + } + } + } + CALL [10] { + function: matches + target: { + CALL [11] { + function: _+_ + args: { + CALL [12] { + function: _+_ + args: { + IDENT [13] { + name: @index0 + } + CONSTANT [14] { value: "o" } + } + } + CONSTANT [15] { value: " world" } + } + } + } + args: { + CONSTANT [16] { 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 [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + CALL [3] { + function: _+_ + args: { + CALL [4] { + function: _+_ + args: { + CALL [5] { + function: _+_ + args: { + CONSTANT [6] { value: "h" } + CONSTANT [7] { value: "e" } + } + } + CONSTANT [8] { value: "l" } + } + } + CONSTANT [9] { value: "l" } + } + } + CALL [10] { + function: _+_ + args: { + CALL [11] { + function: _+_ + args: { + CALL [12] { + function: _+_ + args: { + CONSTANT [13] { value: "w" } + CONSTANT [14] { value: "o" } + } + } + CONSTANT [15] { value: "r" } + } + } + CONSTANT [16] { value: "l" } + } + } + } + } + CALL [17] { + function: matches + target: { + CALL [18] { + function: _+_ + args: { + CALL [19] { + function: _+_ + args: { + IDENT [20] { + name: @index0 + } + CONSTANT [21] { value: "o" } + } + } + CONSTANT [22] { value: " world" } + } + } + } + args: { + CALL [23] { + function: _+_ + args: { + IDENT [24] { + name: @index1 + } + CONSTANT [25] { 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] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.single_int64 + } + } + } + CALL [7] { + function: _+_ + args: { + CALL [8] { + function: _+_ + args: { + CALL [9] { + function: _+_ + args: { + CALL [10] { + function: non_pure_custom_func + args: { + IDENT [11] { + name: @index0 + } + } + } + CALL [12] { + function: non_pure_custom_func + args: { + SELECT [13] { + SELECT [14] { + SELECT [15] { + IDENT [16] { + name: msg + }.oneof_type + }.payload + }.single_int32 + } + } + } + } + } + CALL [17] { + function: non_pure_custom_func + args: { + IDENT [18] { + name: @index0 + } + } + } + } + } + CALL [19] { + function: non_pure_custom_func + args: { + SELECT [20] { + IDENT [21] { + 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] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.single_int64 + } + CALL [7] { + function: pure_custom_func + args: { + IDENT [8] { + name: @index0 + } + } + } + SELECT [9] { + SELECT [10] { + SELECT [11] { + IDENT [12] { + name: msg + }.oneof_type + }.payload + }.single_int32 + } + CALL [13] { + function: _+_ + args: { + CALL [14] { + function: _+_ + args: { + IDENT [15] { + name: @index1 + } + CALL [16] { + function: pure_custom_func + args: { + IDENT [17] { + name: @index2 + } + } + } + } + } + IDENT [18] { + name: @index1 + } + } + } + } + } + CALL [19] { + function: _+_ + args: { + IDENT [20] { + name: @index3 + } + 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 new file mode 100644 index 000000000..cf40f26ee --- /dev/null +++ b/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_4.baseline @@ -0,0 +1,3574 @@ +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: { + CONSTANT [19] { value: 5 } + IDENT [20] { + name: @index0 + } + } + } + IDENT [21] { + name: @index0 + } + } + } + IDENT [22] { + name: @index1 + } + } + } + IDENT [23] { + name: @index1 + } + } + } + } + } + CALL [24] { + function: _==_ + args: { + CALL [25] { + function: _+_ + args: { + CALL [26] { + function: _+_ + args: { + IDENT [27] { + name: @index3 + } + IDENT [28] { + name: @index2 + } + } + } + IDENT [29] { + name: @index2 + } + } + } + CONSTANT [30] { 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: getFullYear + target: { + CALL [9] { + function: timestamp + args: { + CALL [10] { + function: int + args: { + CALL [11] { + function: timestamp + args: { + CONSTANT [12] { value: 200 } + } + } + } + } + } + } + } + args: { + } + } + CALL [13] { + function: timestamp + args: { + CALL [14] { + function: int + args: { + CALL [15] { + function: timestamp + args: { + CONSTANT [16] { value: 50 } + } + } + } + } + } + } + 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: { + IDENT [24] { + name: @index0 + } + CALL [25] { + function: getFullYear + target: { + IDENT [26] { + name: @index3 + } + } + args: { + } + } + } + } + CALL [27] { + function: getFullYear + target: { + IDENT [28] { + name: @index2 + } + } + args: { + } + } + } + } + IDENT [29] { + name: @index0 + } + } + } + CALL [30] { + function: _+_ + args: { + CALL [31] { + function: _+_ + args: { + CALL [32] { + function: _+_ + args: { + IDENT [33] { + name: @index4 + } + CALL [34] { + function: getSeconds + target: { + IDENT [35] { + name: @index2 + } + } + args: { + } + } + } + } + IDENT [36] { + name: @index1 + } + } + } + IDENT [37] { + name: @index1 + } + } + } + } + } + CALL [38] { + function: _==_ + args: { + CALL [39] { + function: _+_ + args: { + CALL [40] { + function: _+_ + args: { + IDENT [41] { + name: @index5 + } + CALL [42] { + function: getMinutes + target: { + IDENT [43] { + name: @index3 + } + } + args: { + } + } + } + } + IDENT [44] { + name: @index0 + } + } + } + CONSTANT [45] { 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: "e" } + } + value: { + MAP [6] { + MAP_ENTRY [7] { + key: { + CONSTANT [8] { value: "b" } + } + value: { + CONSTANT [9] { value: 1 } + } + } + } + } + } + } + MAP [10] { + MAP_ENTRY [11] { + key: { + CONSTANT [12] { value: "b" } + } + value: { + CONSTANT [13] { value: 1 } + } + } + } + } + } + MAP [14] { + MAP_ENTRY [15] { + key: { + CONSTANT [16] { value: "a" } + } + value: { + IDENT [17] { + name: @index1 + } + } + } + MAP_ENTRY [18] { + key: { + CONSTANT [19] { value: "c" } + } + value: { + IDENT [20] { + name: @index1 + } + } + } + MAP_ENTRY [21] { + key: { + CONSTANT [22] { value: "d" } + } + value: { + IDENT [23] { + name: @index0 + } + } + } + MAP_ENTRY [24] { + key: { + CONSTANT [25] { value: "e" } + } + value: { + IDENT [26] { + name: @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]] +=====> +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] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.single_int64 + } + SELECT [7] { + SELECT [8] { + IDENT [9] { + name: msg + }.oneof_type + }.payload + } + CALL [10] { + function: _+_ + args: { + CALL [11] { + function: _+_ + args: { + CALL [12] { + function: _+_ + args: { + IDENT [13] { + name: @index0 + } + SELECT [14] { + IDENT [15] { + name: @index1 + }.single_int32 + } + } + } + IDENT [16] { + name: @index0 + } + } + } + SELECT [17] { + IDENT [18] { + name: msg + }.single_int64 + } + } + } + CALL [19] { + function: _+_ + args: { + IDENT [20] { + name: @index2 + } + SELECT [21] { + SELECT [22] { + SELECT [23] { + IDENT [24] { + name: @index1 + }.oneof_type + }.payload + }.single_int64 + } + } + } + } + } + CALL [25] { + function: _==_ + args: { + IDENT [26] { + name: @index3 + } + CONSTANT [27] { 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] { + IDENT [7] { + name: msg + }.oneof_type + }.payload + }.oneof_type + }.payload + } + SELECT [8] { + IDENT [9] { + name: @index0 + }.oneof_type + } + SELECT [10] { + SELECT [11] { + SELECT [12] { + SELECT [13] { + IDENT [14] { + name: @index1 + }.payload + }.oneof_type + }.payload + }.single_bool + } + SELECT [15] { + SELECT [16] { + SELECT [17] { + SELECT [18] { + IDENT [19] { + name: @index1 + }.child + }.child + }.payload + }.single_bool + } + } + } + CALL [20] { + function: _||_ + args: { + CALL [21] { + function: _||_ + args: { + CONSTANT [22] { value: true } + IDENT [23] { + name: @index2 + } + } + } + IDENT [24] { + name: @index3 + } + } + } + } +} +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 +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + SELECT [3] { + SELECT [4] { + SELECT [5] { + SELECT [6] { + IDENT [7] { + name: msg + }.oneof_type + }.payload + }.oneof_type + }.payload + } + SELECT [8] { + SELECT [9] { + SELECT [10] { + SELECT [11] { + IDENT [12] { + name: @index0 + }.oneof_type + }.payload + }.oneof_type + }.payload + } + } + } + SELECT [13] { + IDENT [14] { + name: @index1 + }.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: { + CALL [6] { + function: _+_ + args: { + IDENT [7] { + name: @index0 + } + CALL [8] { + function: _*_ + args: { + CALL [9] { + function: _+_ + args: { + IDENT [10] { + name: @index0 + } + CONSTANT [11] { value: 1 } + } + } + CONSTANT [12] { value: 2 } + } + } + } + } + CONSTANT [13] { value: 11 } + } + } + } + } + CALL [14] { + function: _?_:_ + args: { + CONSTANT [15] { value: false } + CONSTANT [16] { value: false } + IDENT [17] { + name: @index1 + } + } + } + } +} +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 + } + } + } + COMPREHENSION [16] { + iter_var: @it:0:0 + iter_range: { + LIST [17] { + elements: { + CONSTANT [18] { value: 2 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [19] { value: false } + } + loop_condition: { + CALL [20] { + function: @not_strictly_false + args: { + CALL [21] { + function: !_ + args: { + IDENT [22] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + 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 [28] { + name: @ac:0:0 + } + } + } + CALL [29] { + function: size + args: { + LIST [30] { + elements: { + IDENT [31] { + name: @index0 + } + } + } + } + } + CALL [32] { + function: size + args: { + LIST [33] { + elements: { + IDENT [34] { + name: @index1 + } + } + } + } + } + } + } + CALL [35] { + function: _==_ + args: { + CALL [36] { + function: _+_ + args: { + CALL [37] { + function: _+_ + args: { + CALL [38] { + function: _+_ + args: { + IDENT [39] { + name: @index2 + } + IDENT [40] { + name: @index2 + } + } + } + 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 + } + } + } + COMPREHENSION [16] { + iter_var: @it:0:1 + iter_range: { + LIST [17] { + elements: { + CONSTANT [18] { value: "a" } + } + } + } + accu_var: @ac:0:1 + accu_init: { + CONSTANT [19] { value: false } + } + loop_condition: { + CALL [20] { + function: @not_strictly_false + args: { + CALL [21] { + function: !_ + args: { + IDENT [22] { + name: @ac:0:1 + } + } + } + } + } + } + loop_step: { + 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 [28] { + name: @ac:0:1 + } + } + } + LIST [29] { + elements: { + IDENT [30] { + name: @index0 + } + } + } + LIST [31] { + elements: { + IDENT [32] { + name: @index1 + } + } + } + } + } + CALL [33] { + function: _==_ + args: { + CALL [34] { + function: _+_ + args: { + CALL [35] { + function: _+_ + args: { + CALL [36] { + function: _+_ + args: { + IDENT [37] { + name: @index2 + } + IDENT [38] { + name: @index2 + } + } + } + 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 } + } + } + COMPREHENSION [11] { + iter_var: @it:1:0 + iter_range: { + IDENT [12] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [13] { + elements: { + } + } + } + loop_condition: { + CONSTANT [14] { value: true } + } + loop_step: { + CALL [15] { + function: _+_ + args: { + IDENT [16] { + name: @ac:1:0 + } + LIST [17] { + elements: { + CALL [18] { + function: _+_ + args: { + IDENT [19] { + name: @it:1:0 + } + CONSTANT [20] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [21] { + name: @ac:1:0 + } + } + } + } + } + CALL [22] { + function: _==_ + args: { + 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: { + IDENT [30] { + name: @index2 + } + } + } + } + } + } + result: { + IDENT [31] { + name: @ac:0: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: { + 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: { + CALL [11] { + function: _==_ + args: { + IDENT [12] { + name: @it:1:0 + } + IDENT [13] { + name: @it:0:0 + } + } + } + CALL [14] { + function: _+_ + args: { + IDENT [15] { + name: @ac:1:0 + } + LIST [16] { + elements: { + IDENT [17] { + name: @it:1:0 + } + } + } + } + } + IDENT [18] { + name: @ac:1:0 + } + } + } + } + result: { + IDENT [19] { + name: @ac:1:0 + } + } + } + } + } + CALL [20] { + function: _==_ + args: { + COMPREHENSION [21] { + iter_var: @it:0:0 + iter_range: { + LIST [22] { + elements: { + CONSTANT [23] { value: 1 } + CONSTANT [24] { value: 2 } + } + } + } + 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: { + IDENT [30] { + name: @index0 + } + } + } + } + } + } + result: { + IDENT [31] { + 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: { + 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: { + CALL [13] { + function: _+_ + args: { + IDENT [14] { + name: @it:1:0 + } + CONSTANT [15] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [16] { + name: @ac:1:0 + } + } + } + COMPREHENSION [17] { + iter_var: @it:0:0 + iter_range: { + LIST [18] { + elements: { + CONSTANT [19] { value: 1 } + CONSTANT [20] { value: 2 } + CONSTANT [21] { value: 3 } + } + } + } + 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: @index0 + } + } + } + } + } + } + result: { + IDENT [28] { + name: @ac:0:0 + } + } + } + } + } + CALL [29] { + function: _==_ + args: { + IDENT [30] { + name: @index1 + } + IDENT [31] { + 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:1:0 + iter_range: { + IDENT [14] { + name: @index1 + } + } + 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: { + LIST [20] { + elements: { + CONSTANT [21] { value: 3 } + CONSTANT [22] { value: 4 } + } + } + } + } + } + } + } + result: { + IDENT [23] { + name: @ac:1:0 + } + } + } + } + } + CALL [24] { + function: _==_ + args: { + 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: { + IDENT [30] { + name: @ac:0:0 + } + LIST [31] { + elements: { + IDENT [32] { + name: @index2 + } + } + } + } + } + } + result: { + IDENT [33] { + name: @ac:0: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:0:0 + iter_range: { + COMPREHENSION [7] { + iter_var: @it:1:0 + iter_range: { + IDENT [8] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [9] { + elements: { + } + } + } + loop_condition: { + CONSTANT [10] { value: true } + } + loop_step: { + CALL [11] { + function: _+_ + args: { + IDENT [12] { + name: @ac:1:0 + } + LIST [13] { + elements: { + LIST [14] { + elements: { + CALL [15] { + function: _+_ + args: { + IDENT [16] { + name: @it:1:0 + } + IDENT [17] { + name: @it:1:0 + } + } + } + CALL [18] { + function: _+_ + args: { + IDENT [19] { + name: @it:1:0 + } + IDENT [20] { + name: @it:1:0 + } + } + } + } + } + } + } + } + } + } + result: { + IDENT [21] { + name: @ac:1:0 + } + } + } + } + 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: { + LIST [27] { + elements: { + CALL [28] { + function: _+_ + args: { + IDENT [29] { + name: @it:0:0 + } + IDENT [30] { + name: @it:0:0 + } + } + } + CALL [31] { + function: _+_ + args: { + IDENT [32] { + name: @it:0:0 + } + IDENT [33] { + 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] { + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.single_int64 + } + } + } + CALL [7] { + function: _==_ + args: { + CALL [8] { + function: _?_:_ + args: { + SELECT [9] { + SELECT [10] { + IDENT [11] { + name: msg + }.oneof_type + }.payload~presence_test + } + IDENT [12] { + name: @index0 + } + CALL [13] { + function: _*_ + args: { + IDENT [14] { + name: @index0 + } + 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] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.single_int64 + } + CALL [7] { + function: _?_:_ + args: { + SELECT [8] { + SELECT [9] { + SELECT [10] { + IDENT [11] { + name: msg + }.oneof_type + }.payload + }.single_int64~presence_test + } + IDENT [12] { + name: @index0 + } + CALL [13] { + function: _*_ + args: { + IDENT [14] { + name: @index0 + } + CONSTANT [15] { value: 0 } + } + } + } + } + } + } + CALL [16] { + function: _==_ + args: { + IDENT [17] { + name: @index1 + } + CONSTANT [18] { 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] { + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.map_string_string + } + SELECT [7] { + SELECT [8] { + IDENT [9] { + name: msg + }.oneof_type + }.payload + } + CALL [10] { + function: _&&_ + args: { + CALL [11] { + function: _&&_ + args: { + SELECT [12] { + IDENT [13] { + name: msg + }.oneof_type~presence_test + } + SELECT [14] { + SELECT [15] { + IDENT [16] { + name: msg + }.oneof_type + }.payload~presence_test + } + } + } + SELECT [17] { + IDENT [18] { + name: @index1 + }.single_int64~presence_test + } + } + } + } + } + CALL [19] { + function: _?_:_ + args: { + IDENT [20] { + name: @index2 + } + 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 [32] { 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: { + LIST [3] { + elements: { + CALL [4] { + function: optional.none + args: { + } + } + IDENT [5] { + name: opt_x + } + } + optional_indices: [0, 1] + } + LIST [6] { + elements: { + CONSTANT [7] { value: 5 } + } + } + } + } + CALL [8] { + function: _==_ + args: { + LIST [9] { + elements: { + CONSTANT [10] { value: 10 } + CALL [11] { + function: optional.none + args: { + } + } + IDENT [12] { + name: @index0 + } + IDENT [13] { + name: @index0 + } + } + optional_indices: [0] + } + LIST [14] { + elements: { + CONSTANT [15] { value: 10 } + IDENT [16] { + name: @index1 + } + IDENT [17] { + name: @index1 + } + } + } + } + } + } +} +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: or + target: { + CALL [8] { + function: _[?_] + args: { + MAP [9] { + MAP_ENTRY [10] { + key: { + CONSTANT [11] { value: "key" } + } + optional_entry: true + value: { + CALL [12] { + function: optional.of + args: { + CONSTANT [13] { value: "test" } + } + } + } + } + } + CONSTANT [14] { value: "bogus" } + } + } + } + args: { + CALL [15] { + function: _[?_] + args: { + IDENT [16] { + name: @index0 + } + CONSTANT [17] { value: "bogus" } + } + } + } + } + } + } + CALL [18] { + function: _==_ + args: { + CALL [19] { + function: orValue + target: { + IDENT [20] { + name: @index1 + } + } + args: { + CALL [21] { + function: _[_] + args: { + IDENT [22] { + name: @index0 + } + CONSTANT [23] { value: "key" } + } + } + } + } + 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: { + 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 [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: { + CONSTANT [13] { value: "hello world" } + } + args: { + IDENT [14] { + name: @index0 + } + } + } + } +} +Test case: CALL_TARGET_NESTED_NO_COMMON_SUBEXPR +Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('hello') +=====> +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: { + CONSTANT [16] { 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 [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: _+_ + args: { + CALL [13] { + function: _+_ + args: { + CALL [14] { + function: _+_ + args: { + CALL [15] { + function: _+_ + args: { + CONSTANT [16] { value: "w" } + CONSTANT [17] { value: "o" } + } + } + CONSTANT [18] { value: "r" } + } + } + CONSTANT [19] { value: "l" } + } + } + CONSTANT [20] { value: "d" } + } + } + } + } + CALL [21] { + function: matches + target: { + CALL [22] { + function: _+_ + args: { + IDENT [23] { + name: @index0 + } + CONSTANT [24] { value: " world" } + } + } + } + args: { + IDENT [25] { + name: @index1 + } + } + } + } +} +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] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.single_int64 + } + } + } + CALL [7] { + function: _+_ + args: { + CALL [8] { + function: _+_ + args: { + CALL [9] { + function: _+_ + args: { + CALL [10] { + function: non_pure_custom_func + args: { + IDENT [11] { + name: @index0 + } + } + } + CALL [12] { + function: non_pure_custom_func + args: { + SELECT [13] { + SELECT [14] { + SELECT [15] { + IDENT [16] { + name: msg + }.oneof_type + }.payload + }.single_int32 + } + } + } + } + } + CALL [17] { + function: non_pure_custom_func + args: { + IDENT [18] { + name: @index0 + } + } + } + } + } + CALL [19] { + function: non_pure_custom_func + args: { + SELECT [20] { + IDENT [21] { + 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: { + CALL [3] { + function: pure_custom_func + args: { + SELECT [4] { + SELECT [5] { + SELECT [6] { + IDENT [7] { + name: msg + }.oneof_type + }.payload + }.single_int64 + } + } + } + CALL [8] { + function: pure_custom_func + args: { + SELECT [9] { + SELECT [10] { + SELECT [11] { + IDENT [12] { + name: msg + }.oneof_type + }.payload + }.single_int32 + } + } + } + } + } + CALL [13] { + function: _+_ + args: { + CALL [14] { + function: _+_ + args: { + CALL [15] { + function: _+_ + args: { + IDENT [16] { + name: @index0 + } + IDENT [17] { + name: @index1 + } + } + } + IDENT [18] { + name: @index0 + } + } + } + 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 new file mode 100644 index 000000000..0eadf7a83 --- /dev/null +++ b/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_5.baseline @@ -0,0 +1,3535 @@ +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: { + CONSTANT [20] { value: 5 } + IDENT [21] { + name: @index0 + } + } + } + IDENT [22] { + name: @index0 + } + } + } + IDENT [23] { + name: @index1 + } + } + } + IDENT [24] { + name: @index1 + } + } + } + IDENT [25] { + name: @index2 + } + } + } + } + } + CALL [26] { + function: _==_ + args: { + CALL [27] { + function: _+_ + args: { + IDENT [28] { + name: @index3 + } + IDENT [29] { + name: @index2 + } + } + } + CONSTANT [30] { 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: getFullYear + target: { + CALL [9] { + function: timestamp + args: { + CALL [10] { + function: int + args: { + CALL [11] { + function: timestamp + args: { + CONSTANT [12] { value: 200 } + } + } + } + } + } + } + } + args: { + } + } + CALL [13] { + function: timestamp + args: { + CALL [14] { + function: int + args: { + CALL [15] { + function: timestamp + args: { + CONSTANT [16] { value: 50 } + } + } + } + } + } + } + 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: { + IDENT [25] { + name: @index0 + } + CALL [26] { + function: getFullYear + target: { + IDENT [27] { + name: @index3 + } + } + args: { + } + } + } + } + CALL [28] { + function: getFullYear + target: { + IDENT [29] { + name: @index2 + } + } + args: { + } + } + } + } + IDENT [30] { + name: @index0 + } + } + } + CALL [31] { + function: getSeconds + target: { + IDENT [32] { + name: @index2 + } + } + args: { + } + } + } + } + } + } + CALL [33] { + function: _==_ + args: { + CALL [34] { + function: _+_ + args: { + CALL [35] { + function: _+_ + args: { + CALL [36] { + function: _+_ + args: { + CALL [37] { + function: _+_ + args: { + IDENT [38] { + name: @index4 + } + IDENT [39] { + name: @index1 + } + } + } + IDENT [40] { + name: @index1 + } + } + } + CALL [41] { + function: getMinutes + target: { + IDENT [42] { + name: @index3 + } + } + args: { + } + } + } + } + IDENT [43] { + name: @index0 + } + } + } + CONSTANT [44] { 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: "e" } + } + value: { + MAP [6] { + MAP_ENTRY [7] { + key: { + CONSTANT [8] { value: "b" } + } + value: { + CONSTANT [9] { value: 1 } + } + } + } + } + } + } + MAP [10] { + MAP_ENTRY [11] { + key: { + CONSTANT [12] { value: "b" } + } + value: { + CONSTANT [13] { value: 1 } + } + } + } + } + } + MAP [14] { + MAP_ENTRY [15] { + key: { + CONSTANT [16] { value: "a" } + } + value: { + IDENT [17] { + name: @index1 + } + } + } + MAP_ENTRY [18] { + key: { + CONSTANT [19] { value: "c" } + } + value: { + IDENT [20] { + name: @index1 + } + } + } + MAP_ENTRY [21] { + key: { + CONSTANT [22] { value: "d" } + } + value: { + IDENT [23] { + name: @index0 + } + } + } + MAP_ENTRY [24] { + key: { + CONSTANT [25] { value: "e" } + } + value: { + IDENT [26] { + name: @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]] +=====> +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] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.single_int64 + } + SELECT [7] { + SELECT [8] { + IDENT [9] { + name: msg + }.oneof_type + }.payload + } + CALL [10] { + function: _+_ + args: { + CALL [11] { + function: _+_ + args: { + CALL [12] { + function: _+_ + args: { + CALL [13] { + function: _+_ + args: { + IDENT [14] { + name: @index0 + } + SELECT [15] { + IDENT [16] { + name: @index1 + }.single_int32 + } + } + } + IDENT [17] { + name: @index0 + } + } + } + SELECT [18] { + IDENT [19] { + name: msg + }.single_int64 + } + } + } + SELECT [20] { + SELECT [21] { + SELECT [22] { + IDENT [23] { + name: @index1 + }.oneof_type + }.payload + }.single_int64 + } + } + } + } + } + CALL [24] { + function: _==_ + args: { + IDENT [25] { + name: @index2 + } + CONSTANT [26] { 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: { + CONSTANT [10] { value: true } + SELECT [11] { + SELECT [12] { + SELECT [13] { + SELECT [14] { + IDENT [15] { + name: @index0 + }.payload + }.oneof_type + }.payload + }.single_bool + } + } + } + } + } + CALL [16] { + function: _||_ + args: { + IDENT [17] { + name: @index1 + } + SELECT [18] { + SELECT [19] { + SELECT [20] { + SELECT [21] { + IDENT [22] { + 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 +=====> +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 + } + } + } + SELECT [9] { + SELECT [10] { + SELECT [11] { + SELECT [12] { + IDENT [13] { + name: @index0 + }.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 + } + } + } + COMPREHENSION [16] { + iter_var: @it:0:0 + iter_range: { + LIST [17] { + elements: { + CONSTANT [18] { value: 2 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [19] { value: false } + } + loop_condition: { + CALL [20] { + function: @not_strictly_false + args: { + CALL [21] { + function: !_ + args: { + IDENT [22] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + 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 [28] { + name: @ac:0:0 + } + } + } + CALL [29] { + function: size + args: { + LIST [30] { + elements: { + IDENT [31] { + name: @index0 + } + } + } + } + } + CALL [32] { + function: size + args: { + LIST [33] { + elements: { + IDENT [34] { + name: @index1 + } + } + } + } + } + } + } + CALL [35] { + function: _==_ + args: { + CALL [36] { + function: _+_ + args: { + CALL [37] { + function: _+_ + args: { + CALL [38] { + function: _+_ + args: { + IDENT [39] { + name: @index2 + } + IDENT [40] { + name: @index2 + } + } + } + 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 + } + } + } + COMPREHENSION [16] { + iter_var: @it:0:1 + iter_range: { + LIST [17] { + elements: { + CONSTANT [18] { value: "a" } + } + } + } + accu_var: @ac:0:1 + accu_init: { + CONSTANT [19] { value: false } + } + loop_condition: { + CALL [20] { + function: @not_strictly_false + args: { + CALL [21] { + function: !_ + args: { + IDENT [22] { + name: @ac:0:1 + } + } + } + } + } + } + loop_step: { + 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 [28] { + name: @ac:0:1 + } + } + } + LIST [29] { + elements: { + IDENT [30] { + name: @index0 + } + } + } + LIST [31] { + elements: { + IDENT [32] { + name: @index1 + } + } + } + } + } + CALL [33] { + function: _==_ + args: { + CALL [34] { + function: _+_ + args: { + CALL [35] { + function: _+_ + args: { + CALL [36] { + function: _+_ + args: { + IDENT [37] { + name: @index2 + } + IDENT [38] { + name: @index2 + } + } + } + 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 } + } + } + COMPREHENSION [11] { + iter_var: @it:1:0 + iter_range: { + IDENT [12] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [13] { + elements: { + } + } + } + loop_condition: { + CONSTANT [14] { value: true } + } + loop_step: { + CALL [15] { + function: _+_ + args: { + IDENT [16] { + name: @ac:1:0 + } + LIST [17] { + elements: { + CALL [18] { + function: _+_ + args: { + IDENT [19] { + name: @it:1:0 + } + CONSTANT [20] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [21] { + name: @ac:1:0 + } + } + } + } + } + CALL [22] { + function: _==_ + args: { + 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: { + IDENT [30] { + name: @index2 + } + } + } + } + } + } + result: { + IDENT [31] { + name: @ac:0: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: { + 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: { + CALL [11] { + function: _==_ + args: { + IDENT [12] { + name: @it:1:0 + } + IDENT [13] { + name: @it:0:0 + } + } + } + CALL [14] { + function: _+_ + args: { + IDENT [15] { + name: @ac:1:0 + } + LIST [16] { + elements: { + IDENT [17] { + name: @it:1:0 + } + } + } + } + } + IDENT [18] { + name: @ac:1:0 + } + } + } + } + result: { + IDENT [19] { + name: @ac:1:0 + } + } + } + } + } + CALL [20] { + function: _==_ + args: { + COMPREHENSION [21] { + iter_var: @it:0:0 + iter_range: { + LIST [22] { + elements: { + CONSTANT [23] { value: 1 } + CONSTANT [24] { value: 2 } + } + } + } + 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: { + IDENT [30] { + name: @index0 + } + } + } + } + } + } + result: { + IDENT [31] { + 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: { + 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: { + CALL [13] { + function: _+_ + args: { + IDENT [14] { + name: @it:1:0 + } + CONSTANT [15] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [16] { + name: @ac:1:0 + } + } + } + COMPREHENSION [17] { + iter_var: @it:0:0 + iter_range: { + LIST [18] { + elements: { + CONSTANT [19] { value: 1 } + CONSTANT [20] { value: 2 } + CONSTANT [21] { value: 3 } + } + } + } + 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: @index0 + } + } + } + } + } + } + result: { + IDENT [28] { + name: @ac:0:0 + } + } + } + } + } + CALL [29] { + function: _==_ + args: { + IDENT [30] { + name: @index1 + } + IDENT [31] { + 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:1:0 + iter_range: { + IDENT [14] { + name: @index1 + } + } + 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: { + LIST [20] { + elements: { + CONSTANT [21] { value: 3 } + CONSTANT [22] { value: 4 } + } + } + } + } + } + } + } + result: { + IDENT [23] { + name: @ac:1:0 + } + } + } + } + } + CALL [24] { + function: _==_ + args: { + 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: { + IDENT [30] { + name: @ac:0:0 + } + LIST [31] { + elements: { + IDENT [32] { + name: @index2 + } + } + } + } + } + } + result: { + IDENT [33] { + name: @ac:0: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: { + COMPREHENSION [3] { + iter_var: @it:1:0 + iter_range: { + LIST [4] { + elements: { + CONSTANT [5] { value: "foo" } + CONSTANT [6] { value: "bar" } + } + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [7] { + elements: { + } + } + } + loop_condition: { + CONSTANT [8] { value: true } + } + loop_step: { + CALL [9] { + function: _+_ + args: { + IDENT [10] { + name: @ac:1:0 + } + LIST [11] { + elements: { + LIST [12] { + elements: { + CALL [13] { + function: _+_ + args: { + IDENT [14] { + name: @it:1:0 + } + IDENT [15] { + name: @it:1:0 + } + } + } + CALL [16] { + function: _+_ + args: { + IDENT [17] { + name: @it:1:0 + } + IDENT [18] { + name: @it:1:0 + } + } + } + } + } + } + } + } + } + } + result: { + IDENT [19] { + name: @ac:1:0 + } + } + } + } + } + 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: { + LIST [27] { + elements: { + CALL [28] { + function: _+_ + args: { + IDENT [29] { + name: @it:0:0 + } + IDENT [30] { + name: @it:0:0 + } + } + } + CALL [31] { + function: _+_ + args: { + IDENT [32] { + name: @it:0:0 + } + IDENT [33] { + 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] { + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.single_int64 + } + } + } + CALL [7] { + function: _==_ + args: { + CALL [8] { + function: _?_:_ + args: { + SELECT [9] { + SELECT [10] { + IDENT [11] { + name: msg + }.oneof_type + }.payload~presence_test + } + IDENT [12] { + name: @index0 + } + CALL [13] { + function: _*_ + args: { + IDENT [14] { + name: @index0 + } + 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] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.single_int64 + } + } + } + CALL [7] { + function: _==_ + args: { + CALL [8] { + function: _?_:_ + args: { + SELECT [9] { + SELECT [10] { + SELECT [11] { + IDENT [12] { + name: msg + }.oneof_type + }.payload + }.single_int64~presence_test + } + IDENT [13] { + name: @index0 + } + CALL [14] { + function: _*_ + args: { + IDENT [15] { + name: @index0 + } + CONSTANT [16] { value: 0 } + } + } + } + } + CONSTANT [17] { 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] { + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.map_string_string + } + SELECT [7] { + SELECT [8] { + IDENT [9] { + name: msg + }.oneof_type + }.payload + } + } + } + CALL [10] { + function: _?_:_ + args: { + CALL [11] { + function: _&&_ + args: { + CALL [12] { + function: _&&_ + args: { + SELECT [13] { + IDENT [14] { + name: msg + }.oneof_type~presence_test + } + SELECT [15] { + SELECT [16] { + IDENT [17] { + name: msg + }.oneof_type + }.payload~presence_test + } + } + } + SELECT [18] { + IDENT [19] { + name: @index1 + }.single_int64~presence_test + } + } + } + CALL [20] { + function: _?_:_ + args: { + CALL [21] { + function: _&&_ + args: { + SELECT [22] { + IDENT [23] { + name: @index1 + }.map_string_string~presence_test + } + SELECT [24] { + IDENT [25] { + name: @index0 + }.key~presence_test + } + } + } + CALL [26] { + function: _==_ + args: { + SELECT [27] { + IDENT [28] { + name: @index0 + }.key + } + CONSTANT [29] { value: "A" } + } + } + CONSTANT [30] { value: false } + } + } + CONSTANT [31] { 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: { + LIST [3] { + elements: { + CALL [4] { + function: optional.none + args: { + } + } + IDENT [5] { + name: opt_x + } + } + optional_indices: [0, 1] + } + LIST [6] { + elements: { + CONSTANT [7] { value: 5 } + } + } + } + } + CALL [8] { + function: _==_ + args: { + LIST [9] { + elements: { + CONSTANT [10] { value: 10 } + CALL [11] { + function: optional.none + args: { + } + } + IDENT [12] { + name: @index0 + } + IDENT [13] { + name: @index0 + } + } + optional_indices: [0] + } + LIST [14] { + elements: { + CONSTANT [15] { value: 10 } + IDENT [16] { + name: @index1 + } + IDENT [17] { + name: @index1 + } + } + } + } + } + } +} +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: orValue + target: { + CALL [8] { + function: or + target: { + CALL [9] { + function: _[?_] + args: { + MAP [10] { + MAP_ENTRY [11] { + key: { + CONSTANT [12] { value: "key" } + } + optional_entry: true + value: { + CALL [13] { + function: optional.of + args: { + CONSTANT [14] { value: "test" } + } + } + } + } + } + CONSTANT [15] { value: "bogus" } + } + } + } + args: { + CALL [16] { + function: _[?_] + args: { + IDENT [17] { + name: @index0 + } + CONSTANT [18] { value: "bogus" } + } + } + } + } + } + args: { + CALL [19] { + function: _[_] + args: { + IDENT [20] { + name: @index0 + } + CONSTANT [21] { value: "key" } + } + } + } + } + } + } + CALL [22] { + function: _==_ + args: { + IDENT [23] { + name: @index1 + } + 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: { + 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 [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + CALL [3] { + function: _+_ + args: { + CALL [4] { + function: _+_ + args: { + CALL [5] { + 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 [12] { value: "o" } + } + } + CONSTANT [13] { value: " world" } + } + } + } + } + CALL [14] { + function: matches + target: { + IDENT [15] { + name: @index0 + } + } + args: { + CONSTANT [16] { 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 [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + CALL [3] { + function: _+_ + args: { + CALL [4] { + function: _+_ + args: { + CALL [5] { + 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 [12] { value: "o" } + } + } + CONSTANT [13] { value: " world" } + } + } + } + } + CALL [14] { + function: matches + target: { + IDENT [15] { + name: @index0 + } + } + args: { + CALL [16] { + function: _+_ + args: { + CALL [17] { + function: _+_ + args: { + CALL [18] { + function: _+_ + args: { + CALL [19] { + function: _+_ + args: { + CONSTANT [20] { value: "w" } + CONSTANT [21] { value: "o" } + } + } + CONSTANT [22] { value: "r" } + } + } + CONSTANT [23] { value: "l" } + } + } + CONSTANT [24] { 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] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.single_int64 + } + } + } + CALL [7] { + function: _+_ + args: { + CALL [8] { + function: _+_ + args: { + CALL [9] { + function: _+_ + args: { + CALL [10] { + function: non_pure_custom_func + args: { + IDENT [11] { + name: @index0 + } + } + } + CALL [12] { + function: non_pure_custom_func + args: { + SELECT [13] { + SELECT [14] { + SELECT [15] { + IDENT [16] { + name: msg + }.oneof_type + }.payload + }.single_int32 + } + } + } + } + } + CALL [17] { + function: non_pure_custom_func + args: { + IDENT [18] { + name: @index0 + } + } + } + } + } + CALL [19] { + function: non_pure_custom_func + args: { + SELECT [20] { + IDENT [21] { + 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: { + CALL [3] { + function: pure_custom_func + args: { + SELECT [4] { + SELECT [5] { + SELECT [6] { + IDENT [7] { + name: msg + }.oneof_type + }.payload + }.single_int64 + } + } + } + CALL [8] { + function: _+_ + args: { + IDENT [9] { + name: @index0 + } + CALL [10] { + function: pure_custom_func + args: { + SELECT [11] { + SELECT [12] { + SELECT [13] { + IDENT [14] { + name: msg + }.oneof_type + }.payload + }.single_int32 + } + } + } + } + } + } + } + CALL [15] { + function: _+_ + args: { + CALL [16] { + function: _+_ + args: { + IDENT [17] { + name: @index1 + } + IDENT [18] { + name: @index0 + } + } + } + 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 new file mode 100644 index 000000000..7eadf9498 --- /dev/null +++ b/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_6.baseline @@ -0,0 +1,3490 @@ +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: { + CONSTANT [21] { value: 5 } + IDENT [22] { + name: @index0 + } + } + } + IDENT [23] { + name: @index0 + } + } + } + IDENT [24] { + name: @index1 + } + } + } + IDENT [25] { + name: @index1 + } + } + } + IDENT [26] { + name: @index2 + } + } + } + IDENT [27] { + name: @index2 + } + } + } + } + } + CALL [28] { + function: _==_ + args: { + IDENT [29] { + name: @index3 + } + CONSTANT [30] { 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: getFullYear + target: { + CALL [9] { + function: timestamp + args: { + CALL [10] { + function: int + args: { + CALL [11] { + function: timestamp + args: { + CONSTANT [12] { value: 200 } + } + } + } + } + } + } + } + args: { + } + } + CALL [13] { + function: timestamp + args: { + CALL [14] { + function: int + args: { + CALL [15] { + function: timestamp + args: { + CONSTANT [16] { value: 50 } + } + } + } + } + } + } + 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: { + IDENT [26] { + name: @index0 + } + CALL [27] { + function: getFullYear + target: { + IDENT [28] { + name: @index3 + } + } + args: { + } + } + } + } + CALL [29] { + function: getFullYear + target: { + IDENT [30] { + name: @index2 + } + } + args: { + } + } + } + } + IDENT [31] { + name: @index0 + } + } + } + CALL [32] { + function: getSeconds + target: { + IDENT [33] { + name: @index2 + } + } + args: { + } + } + } + } + IDENT [34] { + name: @index1 + } + } + } + } + } + CALL [35] { + function: _==_ + args: { + CALL [36] { + function: _+_ + args: { + CALL [37] { + function: _+_ + args: { + CALL [38] { + function: _+_ + args: { + IDENT [39] { + name: @index4 + } + IDENT [40] { + name: @index1 + } + } + } + CALL [41] { + function: getMinutes + target: { + IDENT [42] { + name: @index3 + } + } + args: { + } + } + } + } + IDENT [43] { + name: @index0 + } + } + } + CONSTANT [44] { 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: "e" } + } + value: { + MAP [6] { + MAP_ENTRY [7] { + key: { + CONSTANT [8] { value: "b" } + } + value: { + CONSTANT [9] { value: 1 } + } + } + } + } + } + } + MAP [10] { + MAP_ENTRY [11] { + key: { + CONSTANT [12] { value: "b" } + } + value: { + CONSTANT [13] { value: 1 } + } + } + } + } + } + MAP [14] { + MAP_ENTRY [15] { + key: { + CONSTANT [16] { value: "a" } + } + value: { + IDENT [17] { + name: @index1 + } + } + } + MAP_ENTRY [18] { + key: { + CONSTANT [19] { value: "c" } + } + value: { + IDENT [20] { + name: @index1 + } + } + } + MAP_ENTRY [21] { + key: { + CONSTANT [22] { value: "d" } + } + value: { + IDENT [23] { + name: @index0 + } + } + } + MAP_ENTRY [24] { + key: { + CONSTANT [25] { value: "e" } + } + value: { + IDENT [26] { + name: @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]] +=====> +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] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.single_int64 + } + SELECT [7] { + SELECT [8] { + IDENT [9] { + name: msg + }.oneof_type + }.payload + } + } + } + CALL [10] { + function: _==_ + args: { + CALL [11] { + function: _+_ + args: { + CALL [12] { + function: _+_ + args: { + CALL [13] { + function: _+_ + args: { + CALL [14] { + function: _+_ + args: { + IDENT [15] { + name: @index0 + } + SELECT [16] { + IDENT [17] { + name: @index1 + }.single_int32 + } + } + } + IDENT [18] { + name: @index0 + } + } + } + SELECT [19] { + IDENT [20] { + name: msg + }.single_int64 + } + } + } + SELECT [21] { + SELECT [22] { + SELECT [23] { + IDENT [24] { + name: @index1 + }.oneof_type + }.payload + }.single_int64 + } + } + } + CONSTANT [25] { 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 +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + SELECT [3] { + SELECT [4] { + SELECT [5] { + SELECT [6] { + SELECT [7] { + SELECT [8] { + IDENT [9] { + name: msg + }.oneof_type + }.payload + }.oneof_type + }.payload + }.oneof_type + }.payload + } + } + } + SELECT [10] { + SELECT [11] { + SELECT [12] { + IDENT [13] { + name: @index0 + }.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 + } + } + } + COMPREHENSION [16] { + iter_var: @it:0:0 + iter_range: { + LIST [17] { + elements: { + CONSTANT [18] { value: 2 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [19] { value: false } + } + loop_condition: { + CALL [20] { + function: @not_strictly_false + args: { + CALL [21] { + function: !_ + args: { + IDENT [22] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + 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 [28] { + name: @ac:0:0 + } + } + } + CALL [29] { + function: size + args: { + LIST [30] { + elements: { + IDENT [31] { + name: @index0 + } + } + } + } + } + CALL [32] { + function: size + args: { + LIST [33] { + elements: { + IDENT [34] { + name: @index1 + } + } + } + } + } + } + } + CALL [35] { + function: _==_ + args: { + CALL [36] { + function: _+_ + args: { + CALL [37] { + function: _+_ + args: { + CALL [38] { + function: _+_ + args: { + IDENT [39] { + name: @index2 + } + IDENT [40] { + name: @index2 + } + } + } + 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 + } + } + } + COMPREHENSION [16] { + iter_var: @it:0:1 + iter_range: { + LIST [17] { + elements: { + CONSTANT [18] { value: "a" } + } + } + } + accu_var: @ac:0:1 + accu_init: { + CONSTANT [19] { value: false } + } + loop_condition: { + CALL [20] { + function: @not_strictly_false + args: { + CALL [21] { + function: !_ + args: { + IDENT [22] { + name: @ac:0:1 + } + } + } + } + } + } + loop_step: { + 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 [28] { + name: @ac:0:1 + } + } + } + LIST [29] { + elements: { + IDENT [30] { + name: @index0 + } + } + } + LIST [31] { + elements: { + IDENT [32] { + name: @index1 + } + } + } + } + } + CALL [33] { + function: _==_ + args: { + CALL [34] { + function: _+_ + args: { + CALL [35] { + function: _+_ + args: { + CALL [36] { + function: _+_ + args: { + IDENT [37] { + name: @index2 + } + IDENT [38] { + name: @index2 + } + } + } + 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 } + } + } + COMPREHENSION [11] { + iter_var: @it:1:0 + iter_range: { + IDENT [12] { + name: @index0 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [13] { + elements: { + } + } + } + loop_condition: { + CONSTANT [14] { value: true } + } + loop_step: { + CALL [15] { + function: _+_ + args: { + IDENT [16] { + name: @ac:1:0 + } + LIST [17] { + elements: { + CALL [18] { + function: _+_ + args: { + IDENT [19] { + name: @it:1:0 + } + CONSTANT [20] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [21] { + name: @ac:1:0 + } + } + } + } + } + CALL [22] { + function: _==_ + args: { + 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: { + IDENT [30] { + name: @index2 + } + } + } + } + } + } + result: { + IDENT [31] { + name: @ac:0: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: { + 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: { + CALL [11] { + function: _==_ + args: { + IDENT [12] { + name: @it:1:0 + } + IDENT [13] { + name: @it:0:0 + } + } + } + CALL [14] { + function: _+_ + args: { + IDENT [15] { + name: @ac:1:0 + } + LIST [16] { + elements: { + IDENT [17] { + name: @it:1:0 + } + } + } + } + } + IDENT [18] { + name: @ac:1:0 + } + } + } + } + result: { + IDENT [19] { + name: @ac:1:0 + } + } + } + } + } + CALL [20] { + function: _==_ + args: { + COMPREHENSION [21] { + iter_var: @it:0:0 + iter_range: { + LIST [22] { + elements: { + CONSTANT [23] { value: 1 } + CONSTANT [24] { value: 2 } + } + } + } + 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: { + IDENT [30] { + name: @index0 + } + } + } + } + } + } + result: { + IDENT [31] { + 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: { + 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: { + CALL [13] { + function: _+_ + args: { + IDENT [14] { + name: @it:1:0 + } + CONSTANT [15] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [16] { + name: @ac:1:0 + } + } + } + COMPREHENSION [17] { + iter_var: @it:0:0 + iter_range: { + LIST [18] { + elements: { + CONSTANT [19] { value: 1 } + CONSTANT [20] { value: 2 } + CONSTANT [21] { value: 3 } + } + } + } + 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: @index0 + } + } + } + } + } + } + result: { + IDENT [28] { + name: @ac:0:0 + } + } + } + } + } + CALL [29] { + function: _==_ + args: { + IDENT [30] { + name: @index1 + } + IDENT [31] { + 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:1:0 + iter_range: { + IDENT [14] { + name: @index1 + } + } + 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: { + LIST [20] { + elements: { + CONSTANT [21] { value: 3 } + CONSTANT [22] { value: 4 } + } + } + } + } + } + } + } + result: { + IDENT [23] { + name: @ac:1:0 + } + } + } + } + } + CALL [24] { + function: _==_ + args: { + 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: { + IDENT [30] { + name: @ac:0:0 + } + LIST [31] { + elements: { + IDENT [32] { + name: @index2 + } + } + } + } + } + } + result: { + IDENT [33] { + name: @ac:0: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]) +=====> +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: 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] { + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.single_int64 + } + } + } + CALL [7] { + function: _==_ + args: { + CALL [8] { + function: _?_:_ + args: { + SELECT [9] { + SELECT [10] { + IDENT [11] { + name: msg + }.oneof_type + }.payload~presence_test + } + IDENT [12] { + name: @index0 + } + CALL [13] { + function: _*_ + args: { + IDENT [14] { + name: @index0 + } + 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] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.single_int64 + } + } + } + CALL [7] { + function: _==_ + args: { + CALL [8] { + function: _?_:_ + args: { + SELECT [9] { + SELECT [10] { + SELECT [11] { + IDENT [12] { + name: msg + }.oneof_type + }.payload + }.single_int64~presence_test + } + IDENT [13] { + name: @index0 + } + CALL [14] { + function: _*_ + args: { + IDENT [15] { + name: @index0 + } + CONSTANT [16] { value: 0 } + } + } + } + } + CONSTANT [17] { 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] { + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.map_string_string + } + SELECT [7] { + SELECT [8] { + IDENT [9] { + name: msg + }.oneof_type + }.payload + } + } + } + CALL [10] { + function: _?_:_ + args: { + CALL [11] { + function: _&&_ + args: { + CALL [12] { + function: _&&_ + args: { + SELECT [13] { + IDENT [14] { + name: msg + }.oneof_type~presence_test + } + SELECT [15] { + SELECT [16] { + IDENT [17] { + name: msg + }.oneof_type + }.payload~presence_test + } + } + } + SELECT [18] { + IDENT [19] { + name: @index1 + }.single_int64~presence_test + } + } + } + CALL [20] { + function: _?_:_ + args: { + CALL [21] { + function: _&&_ + args: { + SELECT [22] { + IDENT [23] { + name: @index1 + }.map_string_string~presence_test + } + SELECT [24] { + IDENT [25] { + name: @index0 + }.key~presence_test + } + } + } + CALL [26] { + function: _==_ + args: { + SELECT [27] { + IDENT [28] { + name: @index0 + }.key + } + CONSTANT [29] { value: "A" } + } + } + CONSTANT [30] { value: false } + } + } + CONSTANT [31] { 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: { + LIST [3] { + elements: { + CALL [4] { + function: optional.none + args: { + } + } + IDENT [5] { + name: opt_x + } + } + optional_indices: [0, 1] + } + LIST [6] { + elements: { + CONSTANT [7] { value: 5 } + } + } + } + } + CALL [8] { + function: _==_ + args: { + LIST [9] { + elements: { + CONSTANT [10] { value: 10 } + CALL [11] { + function: optional.none + args: { + } + } + IDENT [12] { + name: @index0 + } + IDENT [13] { + name: @index0 + } + } + optional_indices: [0] + } + LIST [14] { + elements: { + CONSTANT [15] { value: 10 } + IDENT [16] { + name: @index1 + } + IDENT [17] { + name: @index1 + } + } + } + } + } + } +} +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] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.single_int64 + } + } + } + CALL [7] { + function: _+_ + args: { + CALL [8] { + function: _+_ + args: { + CALL [9] { + function: _+_ + args: { + CALL [10] { + function: non_pure_custom_func + args: { + IDENT [11] { + name: @index0 + } + } + } + CALL [12] { + function: non_pure_custom_func + args: { + SELECT [13] { + SELECT [14] { + SELECT [15] { + IDENT [16] { + name: msg + }.oneof_type + }.payload + }.single_int32 + } + } + } + } + } + CALL [17] { + function: non_pure_custom_func + args: { + IDENT [18] { + name: @index0 + } + } + } + } + } + CALL [19] { + function: non_pure_custom_func + args: { + SELECT [20] { + IDENT [21] { + 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: { + CALL [3] { + function: pure_custom_func + args: { + SELECT [4] { + SELECT [5] { + SELECT [6] { + IDENT [7] { + name: msg + }.oneof_type + }.payload + }.single_int64 + } + } + } + CALL [8] { + function: _+_ + args: { + CALL [9] { + function: _+_ + args: { + IDENT [10] { + name: @index0 + } + CALL [11] { + function: pure_custom_func + args: { + SELECT [12] { + SELECT [13] { + SELECT [14] { + IDENT [15] { + name: msg + }.oneof_type + }.payload + }.single_int32 + } + } + } + } + } + IDENT [16] { + name: @index0 + } + } + } + } + } + CALL [17] { + function: _+_ + args: { + IDENT [18] { + name: @index1 + } + 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 new file mode 100644 index 000000000..8bba9777e --- /dev/null +++ b/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_7.baseline @@ -0,0 +1,3463 @@ +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: getFullYear + target: { + CALL [9] { + function: timestamp + args: { + CALL [10] { + function: int + args: { + CALL [11] { + function: timestamp + args: { + CONSTANT [12] { value: 200 } + } + } + } + } + } + } + } + args: { + } + } + CALL [13] { + function: timestamp + args: { + CALL [14] { + function: int + args: { + CALL [15] { + function: timestamp + args: { + CONSTANT [16] { value: 50 } + } + } + } + } + } + } + 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: { + IDENT [27] { + name: @index0 + } + CALL [28] { + function: getFullYear + target: { + IDENT [29] { + name: @index3 + } + } + args: { + } + } + } + } + CALL [30] { + function: getFullYear + target: { + IDENT [31] { + name: @index2 + } + } + args: { + } + } + } + } + IDENT [32] { + name: @index0 + } + } + } + CALL [33] { + function: getSeconds + target: { + IDENT [34] { + name: @index2 + } + } + args: { + } + } + } + } + IDENT [35] { + name: @index1 + } + } + } + IDENT [36] { + name: @index1 + } + } + } + } + } + CALL [37] { + function: _==_ + args: { + CALL [38] { + function: _+_ + args: { + CALL [39] { + function: _+_ + args: { + IDENT [40] { + name: @index4 + } + CALL [41] { + function: getMinutes + target: { + IDENT [42] { + name: @index3 + } + } + args: { + } + } + } + } + IDENT [43] { + name: @index0 + } + } + } + CONSTANT [44] { 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: "e" } + } + value: { + MAP [6] { + MAP_ENTRY [7] { + key: { + CONSTANT [8] { value: "b" } + } + value: { + CONSTANT [9] { value: 1 } + } + } + } + } + } + } + MAP [10] { + MAP_ENTRY [11] { + key: { + CONSTANT [12] { value: "b" } + } + value: { + CONSTANT [13] { value: 1 } + } + } + } + } + } + MAP [14] { + MAP_ENTRY [15] { + key: { + CONSTANT [16] { value: "a" } + } + value: { + IDENT [17] { + name: @index1 + } + } + } + MAP_ENTRY [18] { + key: { + CONSTANT [19] { value: "c" } + } + value: { + IDENT [20] { + name: @index1 + } + } + } + MAP_ENTRY [21] { + key: { + CONSTANT [22] { value: "d" } + } + value: { + IDENT [23] { + name: @index0 + } + } + } + MAP_ENTRY [24] { + key: { + CONSTANT [25] { value: "e" } + } + value: { + IDENT [26] { + name: @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]] +=====> +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] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.single_int64 + } + SELECT [7] { + SELECT [8] { + IDENT [9] { + name: msg + }.oneof_type + }.payload + } + } + } + CALL [10] { + function: _==_ + args: { + CALL [11] { + function: _+_ + args: { + CALL [12] { + function: _+_ + args: { + CALL [13] { + function: _+_ + args: { + CALL [14] { + function: _+_ + args: { + IDENT [15] { + name: @index0 + } + SELECT [16] { + IDENT [17] { + name: @index1 + }.single_int32 + } + } + } + IDENT [18] { + name: @index0 + } + } + } + SELECT [19] { + IDENT [20] { + name: msg + }.single_int64 + } + } + } + SELECT [21] { + SELECT [22] { + SELECT [23] { + IDENT [24] { + name: @index1 + }.oneof_type + }.payload + }.single_int64 + } + } + } + CONSTANT [25] { 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 +=====> +CALL [1] { + function: cel.@block + args: { + LIST [2] { + elements: { + SELECT [3] { + SELECT [4] { + SELECT [5] { + SELECT [6] { + SELECT [7] { + SELECT [8] { + SELECT [9] { + IDENT [10] { + name: msg + }.oneof_type + }.payload + }.oneof_type + }.payload + }.oneof_type + }.payload + }.oneof_type + } + } + } + SELECT [11] { + SELECT [12] { + IDENT [13] { + name: @index0 + }.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 + } + } + } + COMPREHENSION [16] { + iter_var: @it:0:0 + iter_range: { + LIST [17] { + elements: { + CONSTANT [18] { value: 2 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [19] { value: false } + } + loop_condition: { + CALL [20] { + function: @not_strictly_false + args: { + CALL [21] { + function: !_ + args: { + IDENT [22] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + 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 [28] { + name: @ac:0:0 + } + } + } + CALL [29] { + function: size + args: { + LIST [30] { + elements: { + IDENT [31] { + name: @index0 + } + } + } + } + } + CALL [32] { + function: size + args: { + LIST [33] { + elements: { + IDENT [34] { + name: @index1 + } + } + } + } + } + } + } + CALL [35] { + function: _==_ + args: { + CALL [36] { + function: _+_ + args: { + CALL [37] { + function: _+_ + args: { + CALL [38] { + function: _+_ + args: { + IDENT [39] { + name: @index2 + } + IDENT [40] { + name: @index2 + } + } + } + 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 + } + } + } + COMPREHENSION [16] { + iter_var: @it:0:1 + iter_range: { + LIST [17] { + elements: { + CONSTANT [18] { value: "a" } + } + } + } + accu_var: @ac:0:1 + accu_init: { + CONSTANT [19] { value: false } + } + loop_condition: { + CALL [20] { + function: @not_strictly_false + args: { + CALL [21] { + function: !_ + args: { + IDENT [22] { + name: @ac:0:1 + } + } + } + } + } + } + loop_step: { + 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 [28] { + name: @ac:0:1 + } + } + } + LIST [29] { + elements: { + IDENT [30] { + name: @index0 + } + } + } + LIST [31] { + elements: { + IDENT [32] { + name: @index1 + } + } + } + } + } + CALL [33] { + function: _==_ + args: { + CALL [34] { + function: _+_ + args: { + CALL [35] { + function: _+_ + args: { + CALL [36] { + function: _+_ + args: { + IDENT [37] { + name: @index2 + } + IDENT [38] { + name: @index2 + } + } + } + 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: 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: 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: { + COMPREHENSION [13] { + iter_var: @it:1:0 + iter_range: { + LIST [14] { + elements: { + CONSTANT [15] { value: 1 } + CONSTANT [16] { value: 2 } + CONSTANT [17] { value: 3 } + } + } + } + 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: { + CALL [23] { + function: _+_ + args: { + IDENT [24] { + name: @it:1:0 + } + CONSTANT [25] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [26] { + name: @ac:1:0 + } + } + } + } + } + } + } + } + result: { + IDENT [27] { + name: @ac:0:0 + } + } + } + } + } + CALL [28] { + function: _==_ + args: { + IDENT [29] { + name: @index0 + } + 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] +=====> +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 } + } + } + } + } + CALL [13] { + function: _==_ + args: { + COMPREHENSION [14] { + iter_var: @it:0:0 + iter_range: { + IDENT [15] { + name: @index1 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [16] { + elements: { + } + } + } + loop_condition: { + CONSTANT [17] { value: true } + } + loop_step: { + CALL [18] { + function: _+_ + args: { + IDENT [19] { + name: @ac:0:0 + } + LIST [20] { + elements: { + COMPREHENSION [21] { + iter_var: @it:1:0 + iter_range: { + IDENT [22] { + name: @index1 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [23] { + elements: { + } + } + } + loop_condition: { + CONSTANT [24] { value: true } + } + loop_step: { + CALL [25] { + function: _+_ + args: { + IDENT [26] { + name: @ac:1:0 + } + LIST [27] { + elements: { + LIST [28] { + elements: { + CONSTANT [29] { value: 3 } + CONSTANT [30] { value: 4 } + } + } + } + } + } + } + } + result: { + IDENT [31] { + name: @ac:1:0 + } + } + } + } + } + } + } + } + result: { + IDENT [32] { + name: @ac:0:0 + } + } + } + LIST [33] { + elements: { + IDENT [34] { + name: @index0 + } + IDENT [35] { + 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]) +=====> +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: 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] { + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.single_int64 + } + } + } + CALL [7] { + function: _==_ + args: { + CALL [8] { + function: _?_:_ + args: { + SELECT [9] { + SELECT [10] { + IDENT [11] { + name: msg + }.oneof_type + }.payload~presence_test + } + IDENT [12] { + name: @index0 + } + CALL [13] { + function: _*_ + args: { + IDENT [14] { + name: @index0 + } + 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] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.single_int64 + } + } + } + CALL [7] { + function: _==_ + args: { + CALL [8] { + function: _?_:_ + args: { + SELECT [9] { + SELECT [10] { + SELECT [11] { + IDENT [12] { + name: msg + }.oneof_type + }.payload + }.single_int64~presence_test + } + IDENT [13] { + name: @index0 + } + CALL [14] { + function: _*_ + args: { + IDENT [15] { + name: @index0 + } + CONSTANT [16] { value: 0 } + } + } + } + } + CONSTANT [17] { 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] { + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.map_string_string + } + SELECT [7] { + SELECT [8] { + IDENT [9] { + name: msg + }.oneof_type + }.payload + } + } + } + CALL [10] { + function: _?_:_ + args: { + CALL [11] { + function: _&&_ + args: { + CALL [12] { + function: _&&_ + args: { + SELECT [13] { + IDENT [14] { + name: msg + }.oneof_type~presence_test + } + SELECT [15] { + SELECT [16] { + IDENT [17] { + name: msg + }.oneof_type + }.payload~presence_test + } + } + } + SELECT [18] { + IDENT [19] { + name: @index1 + }.single_int64~presence_test + } + } + } + CALL [20] { + function: _?_:_ + args: { + CALL [21] { + function: _&&_ + args: { + SELECT [22] { + IDENT [23] { + name: @index1 + }.map_string_string~presence_test + } + SELECT [24] { + IDENT [25] { + name: @index0 + }.key~presence_test + } + } + } + CALL [26] { + function: _==_ + args: { + SELECT [27] { + IDENT [28] { + name: @index0 + }.key + } + CONSTANT [29] { value: "A" } + } + } + CONSTANT [30] { value: false } + } + } + CONSTANT [31] { 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: { + LIST [3] { + elements: { + CALL [4] { + function: optional.none + args: { + } + } + IDENT [5] { + name: opt_x + } + } + optional_indices: [0, 1] + } + LIST [6] { + elements: { + CONSTANT [7] { value: 5 } + } + } + } + } + CALL [8] { + function: _==_ + args: { + LIST [9] { + elements: { + CONSTANT [10] { value: 10 } + CALL [11] { + function: optional.none + args: { + } + } + IDENT [12] { + name: @index0 + } + IDENT [13] { + name: @index0 + } + } + optional_indices: [0] + } + LIST [14] { + elements: { + CONSTANT [15] { value: 10 } + IDENT [16] { + name: @index1 + } + IDENT [17] { + name: @index1 + } + } + } + } + } + } +} +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] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.single_int64 + } + } + } + CALL [7] { + function: _+_ + args: { + CALL [8] { + function: _+_ + args: { + CALL [9] { + function: _+_ + args: { + CALL [10] { + function: non_pure_custom_func + args: { + IDENT [11] { + name: @index0 + } + } + } + CALL [12] { + function: non_pure_custom_func + args: { + SELECT [13] { + SELECT [14] { + SELECT [15] { + IDENT [16] { + name: msg + }.oneof_type + }.payload + }.single_int32 + } + } + } + } + } + CALL [17] { + function: non_pure_custom_func + args: { + IDENT [18] { + name: @index0 + } + } + } + } + } + CALL [19] { + function: non_pure_custom_func + args: { + SELECT [20] { + IDENT [21] { + 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: { + CALL [3] { + function: pure_custom_func + args: { + SELECT [4] { + SELECT [5] { + SELECT [6] { + IDENT [7] { + name: msg + }.oneof_type + }.payload + }.single_int64 + } + } + } + } + } + CALL [8] { + function: _+_ + args: { + CALL [9] { + function: _+_ + args: { + CALL [10] { + function: _+_ + args: { + IDENT [11] { + name: @index0 + } + CALL [12] { + function: pure_custom_func + args: { + SELECT [13] { + SELECT [14] { + SELECT [15] { + IDENT [16] { + name: msg + }.oneof_type + }.payload + }.single_int32 + } + } + } + } + } + IDENT [17] { + name: @index0 + } + } + } + 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 new file mode 100644 index 000000000..2bee1cefa --- /dev/null +++ b/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_8.baseline @@ -0,0 +1,3463 @@ +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: getFullYear + target: { + CALL [9] { + function: timestamp + args: { + CALL [10] { + function: int + args: { + CALL [11] { + function: timestamp + args: { + CONSTANT [12] { value: 200 } + } + } + } + } + } + } + } + args: { + } + } + CALL [13] { + function: timestamp + args: { + CALL [14] { + function: int + args: { + CALL [15] { + function: timestamp + args: { + CONSTANT [16] { value: 50 } + } + } + } + } + } + } + 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: { + IDENT [28] { + name: @index0 + } + CALL [29] { + function: getFullYear + target: { + IDENT [30] { + name: @index3 + } + } + args: { + } + } + } + } + CALL [31] { + function: getFullYear + target: { + IDENT [32] { + name: @index2 + } + } + args: { + } + } + } + } + IDENT [33] { + name: @index0 + } + } + } + CALL [34] { + function: getSeconds + target: { + IDENT [35] { + name: @index2 + } + } + args: { + } + } + } + } + IDENT [36] { + name: @index1 + } + } + } + IDENT [37] { + name: @index1 + } + } + } + CALL [38] { + function: getMinutes + target: { + IDENT [39] { + name: @index3 + } + } + args: { + } + } + } + } + } + } + CALL [40] { + function: _==_ + args: { + CALL [41] { + function: _+_ + args: { + IDENT [42] { + name: @index4 + } + IDENT [43] { + name: @index0 + } + } + } + CONSTANT [44] { 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: "e" } + } + value: { + MAP [6] { + MAP_ENTRY [7] { + key: { + CONSTANT [8] { value: "b" } + } + value: { + CONSTANT [9] { value: 1 } + } + } + } + } + } + } + MAP [10] { + MAP_ENTRY [11] { + key: { + CONSTANT [12] { value: "b" } + } + value: { + CONSTANT [13] { value: 1 } + } + } + } + } + } + MAP [14] { + MAP_ENTRY [15] { + key: { + CONSTANT [16] { value: "a" } + } + value: { + IDENT [17] { + name: @index1 + } + } + } + MAP_ENTRY [18] { + key: { + CONSTANT [19] { value: "c" } + } + value: { + IDENT [20] { + name: @index1 + } + } + } + MAP_ENTRY [21] { + key: { + CONSTANT [22] { value: "d" } + } + value: { + IDENT [23] { + name: @index0 + } + } + } + MAP_ENTRY [24] { + key: { + CONSTANT [25] { value: "e" } + } + value: { + IDENT [26] { + name: @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]] +=====> +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] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.single_int64 + } + SELECT [7] { + SELECT [8] { + IDENT [9] { + name: msg + }.oneof_type + }.payload + } + } + } + CALL [10] { + function: _==_ + args: { + CALL [11] { + function: _+_ + args: { + CALL [12] { + function: _+_ + args: { + CALL [13] { + function: _+_ + args: { + CALL [14] { + function: _+_ + args: { + IDENT [15] { + name: @index0 + } + SELECT [16] { + IDENT [17] { + name: @index1 + }.single_int32 + } + } + } + IDENT [18] { + name: @index0 + } + } + } + SELECT [19] { + IDENT [20] { + name: msg + }.single_int64 + } + } + } + SELECT [21] { + SELECT [22] { + SELECT [23] { + IDENT [24] { + name: @index1 + }.oneof_type + }.payload + }.single_int64 + } + } + } + CONSTANT [25] { 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 +=====> +CALL [1] { + function: cel.@block + args: { + 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 + } + } +} +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 + } + } + } + COMPREHENSION [16] { + iter_var: @it:0:0 + iter_range: { + LIST [17] { + elements: { + CONSTANT [18] { value: 2 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [19] { value: false } + } + loop_condition: { + CALL [20] { + function: @not_strictly_false + args: { + CALL [21] { + function: !_ + args: { + IDENT [22] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + 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 [28] { + name: @ac:0:0 + } + } + } + CALL [29] { + function: size + args: { + LIST [30] { + elements: { + IDENT [31] { + name: @index0 + } + } + } + } + } + CALL [32] { + function: size + args: { + LIST [33] { + elements: { + IDENT [34] { + name: @index1 + } + } + } + } + } + } + } + CALL [35] { + function: _==_ + args: { + CALL [36] { + function: _+_ + args: { + CALL [37] { + function: _+_ + args: { + CALL [38] { + function: _+_ + args: { + IDENT [39] { + name: @index2 + } + IDENT [40] { + name: @index2 + } + } + } + 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 + } + } + } + COMPREHENSION [16] { + iter_var: @it:0:1 + iter_range: { + LIST [17] { + elements: { + CONSTANT [18] { value: "a" } + } + } + } + accu_var: @ac:0:1 + accu_init: { + CONSTANT [19] { value: false } + } + loop_condition: { + CALL [20] { + function: @not_strictly_false + args: { + CALL [21] { + function: !_ + args: { + IDENT [22] { + name: @ac:0:1 + } + } + } + } + } + } + loop_step: { + 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 [28] { + name: @ac:0:1 + } + } + } + LIST [29] { + elements: { + IDENT [30] { + name: @index0 + } + } + } + LIST [31] { + elements: { + IDENT [32] { + name: @index1 + } + } + } + } + } + CALL [33] { + function: _==_ + args: { + CALL [34] { + function: _+_ + args: { + CALL [35] { + function: _+_ + args: { + CALL [36] { + function: _+_ + args: { + IDENT [37] { + name: @index2 + } + IDENT [38] { + name: @index2 + } + } + } + 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: 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: 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: { + COMPREHENSION [13] { + iter_var: @it:1:0 + iter_range: { + LIST [14] { + elements: { + CONSTANT [15] { value: 1 } + CONSTANT [16] { value: 2 } + CONSTANT [17] { value: 3 } + } + } + } + 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: { + CALL [23] { + function: _+_ + args: { + IDENT [24] { + name: @it:1:0 + } + CONSTANT [25] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [26] { + name: @ac:1:0 + } + } + } + } + } + } + } + } + result: { + IDENT [27] { + name: @ac:0:0 + } + } + } + } + } + CALL [28] { + function: _==_ + args: { + IDENT [29] { + name: @index0 + } + 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] +=====> +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 } + } + } + } + } + CALL [13] { + function: _==_ + args: { + COMPREHENSION [14] { + iter_var: @it:0:0 + iter_range: { + IDENT [15] { + name: @index1 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [16] { + elements: { + } + } + } + loop_condition: { + CONSTANT [17] { value: true } + } + loop_step: { + CALL [18] { + function: _+_ + args: { + IDENT [19] { + name: @ac:0:0 + } + LIST [20] { + elements: { + COMPREHENSION [21] { + iter_var: @it:1:0 + iter_range: { + IDENT [22] { + name: @index1 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [23] { + elements: { + } + } + } + loop_condition: { + CONSTANT [24] { value: true } + } + loop_step: { + CALL [25] { + function: _+_ + args: { + IDENT [26] { + name: @ac:1:0 + } + LIST [27] { + elements: { + LIST [28] { + elements: { + CONSTANT [29] { value: 3 } + CONSTANT [30] { value: 4 } + } + } + } + } + } + } + } + result: { + IDENT [31] { + name: @ac:1:0 + } + } + } + } + } + } + } + } + result: { + IDENT [32] { + name: @ac:0:0 + } + } + } + LIST [33] { + elements: { + IDENT [34] { + name: @index0 + } + IDENT [35] { + 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]) +=====> +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: 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] { + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.single_int64 + } + } + } + CALL [7] { + function: _==_ + args: { + CALL [8] { + function: _?_:_ + args: { + SELECT [9] { + SELECT [10] { + IDENT [11] { + name: msg + }.oneof_type + }.payload~presence_test + } + IDENT [12] { + name: @index0 + } + CALL [13] { + function: _*_ + args: { + IDENT [14] { + name: @index0 + } + 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] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.single_int64 + } + } + } + CALL [7] { + function: _==_ + args: { + CALL [8] { + function: _?_:_ + args: { + SELECT [9] { + SELECT [10] { + SELECT [11] { + IDENT [12] { + name: msg + }.oneof_type + }.payload + }.single_int64~presence_test + } + IDENT [13] { + name: @index0 + } + CALL [14] { + function: _*_ + args: { + IDENT [15] { + name: @index0 + } + CONSTANT [16] { value: 0 } + } + } + } + } + CONSTANT [17] { 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] { + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.map_string_string + } + SELECT [7] { + SELECT [8] { + IDENT [9] { + name: msg + }.oneof_type + }.payload + } + } + } + CALL [10] { + function: _?_:_ + args: { + CALL [11] { + function: _&&_ + args: { + CALL [12] { + function: _&&_ + args: { + SELECT [13] { + IDENT [14] { + name: msg + }.oneof_type~presence_test + } + SELECT [15] { + SELECT [16] { + IDENT [17] { + name: msg + }.oneof_type + }.payload~presence_test + } + } + } + SELECT [18] { + IDENT [19] { + name: @index1 + }.single_int64~presence_test + } + } + } + CALL [20] { + function: _?_:_ + args: { + CALL [21] { + function: _&&_ + args: { + SELECT [22] { + IDENT [23] { + name: @index1 + }.map_string_string~presence_test + } + SELECT [24] { + IDENT [25] { + name: @index0 + }.key~presence_test + } + } + } + CALL [26] { + function: _==_ + args: { + SELECT [27] { + IDENT [28] { + name: @index0 + }.key + } + CONSTANT [29] { value: "A" } + } + } + CONSTANT [30] { value: false } + } + } + CONSTANT [31] { 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: { + LIST [3] { + elements: { + CALL [4] { + function: optional.none + args: { + } + } + IDENT [5] { + name: opt_x + } + } + optional_indices: [0, 1] + } + LIST [6] { + elements: { + CONSTANT [7] { value: 5 } + } + } + } + } + CALL [8] { + function: _==_ + args: { + LIST [9] { + elements: { + CONSTANT [10] { value: 10 } + CALL [11] { + function: optional.none + args: { + } + } + IDENT [12] { + name: @index0 + } + IDENT [13] { + name: @index0 + } + } + optional_indices: [0] + } + LIST [14] { + elements: { + CONSTANT [15] { value: 10 } + IDENT [16] { + name: @index1 + } + IDENT [17] { + name: @index1 + } + } + } + } + } + } +} +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] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.single_int64 + } + } + } + CALL [7] { + function: _+_ + args: { + CALL [8] { + function: _+_ + args: { + CALL [9] { + function: _+_ + args: { + CALL [10] { + function: non_pure_custom_func + args: { + IDENT [11] { + name: @index0 + } + } + } + CALL [12] { + function: non_pure_custom_func + args: { + SELECT [13] { + SELECT [14] { + SELECT [15] { + IDENT [16] { + name: msg + }.oneof_type + }.payload + }.single_int32 + } + } + } + } + } + CALL [17] { + function: non_pure_custom_func + args: { + IDENT [18] { + name: @index0 + } + } + } + } + } + CALL [19] { + function: non_pure_custom_func + args: { + SELECT [20] { + IDENT [21] { + 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: { + CALL [3] { + function: pure_custom_func + args: { + SELECT [4] { + SELECT [5] { + SELECT [6] { + IDENT [7] { + name: msg + }.oneof_type + }.payload + }.single_int64 + } + } + } + } + } + CALL [8] { + function: _+_ + args: { + CALL [9] { + function: _+_ + args: { + CALL [10] { + function: _+_ + args: { + IDENT [11] { + name: @index0 + } + CALL [12] { + function: pure_custom_func + args: { + SELECT [13] { + SELECT [14] { + SELECT [15] { + IDENT [16] { + name: msg + }.oneof_type + }.payload + }.single_int32 + } + } + } + } + } + IDENT [17] { + name: @index0 + } + } + } + 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 new file mode 100644 index 000000000..740b3e04f --- /dev/null +++ b/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_9.baseline @@ -0,0 +1,3451 @@ +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: getFullYear + target: { + CALL [9] { + function: timestamp + args: { + CALL [10] { + function: int + args: { + CALL [11] { + function: timestamp + args: { + CONSTANT [12] { value: 200 } + } + } + } + } + } + } + } + args: { + } + } + CALL [13] { + function: timestamp + args: { + CALL [14] { + function: int + args: { + CALL [15] { + function: timestamp + args: { + CONSTANT [16] { value: 50 } + } + } + } + } + } + } + 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: { + IDENT [29] { + name: @index0 + } + CALL [30] { + function: getFullYear + target: { + IDENT [31] { + name: @index3 + } + } + args: { + } + } + } + } + CALL [32] { + function: getFullYear + target: { + IDENT [33] { + name: @index2 + } + } + args: { + } + } + } + } + IDENT [34] { + name: @index0 + } + } + } + CALL [35] { + function: getSeconds + target: { + IDENT [36] { + name: @index2 + } + } + args: { + } + } + } + } + IDENT [37] { + name: @index1 + } + } + } + IDENT [38] { + name: @index1 + } + } + } + CALL [39] { + function: getMinutes + target: { + IDENT [40] { + name: @index3 + } + } + args: { + } + } + } + } + IDENT [41] { + name: @index0 + } + } + } + } + } + CALL [42] { + function: _==_ + args: { + IDENT [43] { + name: @index4 + } + CONSTANT [44] { 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: "e" } + } + value: { + MAP [6] { + MAP_ENTRY [7] { + key: { + CONSTANT [8] { value: "b" } + } + value: { + CONSTANT [9] { value: 1 } + } + } + } + } + } + } + MAP [10] { + MAP_ENTRY [11] { + key: { + CONSTANT [12] { value: "b" } + } + value: { + CONSTANT [13] { value: 1 } + } + } + } + } + } + MAP [14] { + MAP_ENTRY [15] { + key: { + CONSTANT [16] { value: "a" } + } + value: { + IDENT [17] { + name: @index1 + } + } + } + MAP_ENTRY [18] { + key: { + CONSTANT [19] { value: "c" } + } + value: { + IDENT [20] { + name: @index1 + } + } + } + MAP_ENTRY [21] { + key: { + CONSTANT [22] { value: "d" } + } + value: { + IDENT [23] { + name: @index0 + } + } + } + MAP_ENTRY [24] { + key: { + CONSTANT [25] { value: "e" } + } + value: { + IDENT [26] { + name: @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]] +=====> +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] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.single_int64 + } + SELECT [7] { + SELECT [8] { + IDENT [9] { + name: msg + }.oneof_type + }.payload + } + } + } + CALL [10] { + function: _==_ + args: { + CALL [11] { + function: _+_ + args: { + CALL [12] { + function: _+_ + args: { + CALL [13] { + function: _+_ + args: { + CALL [14] { + function: _+_ + args: { + IDENT [15] { + name: @index0 + } + SELECT [16] { + IDENT [17] { + name: @index1 + }.single_int32 + } + } + } + IDENT [18] { + name: @index0 + } + } + } + SELECT [19] { + IDENT [20] { + name: msg + }.single_int64 + } + } + } + SELECT [21] { + SELECT [22] { + SELECT [23] { + IDENT [24] { + name: @index1 + }.oneof_type + }.payload + }.single_int64 + } + } + } + CONSTANT [25] { 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 + } + } + } + COMPREHENSION [16] { + iter_var: @it:0:0 + iter_range: { + LIST [17] { + elements: { + CONSTANT [18] { value: 2 } + } + } + } + accu_var: @ac:0:0 + accu_init: { + CONSTANT [19] { value: false } + } + loop_condition: { + CALL [20] { + function: @not_strictly_false + args: { + CALL [21] { + function: !_ + args: { + IDENT [22] { + name: @ac:0:0 + } + } + } + } + } + } + loop_step: { + 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 [28] { + name: @ac:0:0 + } + } + } + CALL [29] { + function: size + args: { + LIST [30] { + elements: { + IDENT [31] { + name: @index0 + } + } + } + } + } + CALL [32] { + function: size + args: { + LIST [33] { + elements: { + IDENT [34] { + name: @index1 + } + } + } + } + } + } + } + CALL [35] { + function: _==_ + args: { + CALL [36] { + function: _+_ + args: { + CALL [37] { + function: _+_ + args: { + CALL [38] { + function: _+_ + args: { + IDENT [39] { + name: @index2 + } + IDENT [40] { + name: @index2 + } + } + } + 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 + } + } + } + COMPREHENSION [16] { + iter_var: @it:0:1 + iter_range: { + LIST [17] { + elements: { + CONSTANT [18] { value: "a" } + } + } + } + accu_var: @ac:0:1 + accu_init: { + CONSTANT [19] { value: false } + } + loop_condition: { + CALL [20] { + function: @not_strictly_false + args: { + CALL [21] { + function: !_ + args: { + IDENT [22] { + name: @ac:0:1 + } + } + } + } + } + } + loop_step: { + 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 [28] { + name: @ac:0:1 + } + } + } + LIST [29] { + elements: { + IDENT [30] { + name: @index0 + } + } + } + LIST [31] { + elements: { + IDENT [32] { + name: @index1 + } + } + } + } + } + CALL [33] { + function: _==_ + args: { + CALL [34] { + function: _+_ + args: { + CALL [35] { + function: _+_ + args: { + CALL [36] { + function: _+_ + args: { + IDENT [37] { + name: @index2 + } + IDENT [38] { + name: @index2 + } + } + } + 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: 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: 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: { + COMPREHENSION [13] { + iter_var: @it:1:0 + iter_range: { + LIST [14] { + elements: { + CONSTANT [15] { value: 1 } + CONSTANT [16] { value: 2 } + CONSTANT [17] { value: 3 } + } + } + } + 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: { + CALL [23] { + function: _+_ + args: { + IDENT [24] { + name: @it:1:0 + } + CONSTANT [25] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [26] { + name: @ac:1:0 + } + } + } + } + } + } + } + } + result: { + IDENT [27] { + name: @ac:0:0 + } + } + } + } + } + CALL [28] { + function: _==_ + args: { + IDENT [29] { + name: @index0 + } + 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] +=====> +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 } + } + } + } + } + CALL [13] { + function: _==_ + args: { + COMPREHENSION [14] { + iter_var: @it:0:0 + iter_range: { + IDENT [15] { + name: @index1 + } + } + accu_var: @ac:0:0 + accu_init: { + LIST [16] { + elements: { + } + } + } + loop_condition: { + CONSTANT [17] { value: true } + } + loop_step: { + CALL [18] { + function: _+_ + args: { + IDENT [19] { + name: @ac:0:0 + } + LIST [20] { + elements: { + COMPREHENSION [21] { + iter_var: @it:1:0 + iter_range: { + IDENT [22] { + name: @index1 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [23] { + elements: { + } + } + } + loop_condition: { + CONSTANT [24] { value: true } + } + loop_step: { + CALL [25] { + function: _+_ + args: { + IDENT [26] { + name: @ac:1:0 + } + LIST [27] { + elements: { + LIST [28] { + elements: { + CONSTANT [29] { value: 3 } + CONSTANT [30] { value: 4 } + } + } + } + } + } + } + } + result: { + IDENT [31] { + name: @ac:1:0 + } + } + } + } + } + } + } + } + result: { + IDENT [32] { + name: @ac:0:0 + } + } + } + LIST [33] { + elements: { + IDENT [34] { + name: @index0 + } + IDENT [35] { + 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]) +=====> +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: 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] { + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.single_int64 + } + } + } + CALL [7] { + function: _==_ + args: { + CALL [8] { + function: _?_:_ + args: { + SELECT [9] { + SELECT [10] { + IDENT [11] { + name: msg + }.oneof_type + }.payload~presence_test + } + IDENT [12] { + name: @index0 + } + CALL [13] { + function: _*_ + args: { + IDENT [14] { + name: @index0 + } + 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] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.single_int64 + } + } + } + CALL [7] { + function: _==_ + args: { + CALL [8] { + function: _?_:_ + args: { + SELECT [9] { + SELECT [10] { + SELECT [11] { + IDENT [12] { + name: msg + }.oneof_type + }.payload + }.single_int64~presence_test + } + IDENT [13] { + name: @index0 + } + CALL [14] { + function: _*_ + args: { + IDENT [15] { + name: @index0 + } + CONSTANT [16] { value: 0 } + } + } + } + } + CONSTANT [17] { 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] { + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.map_string_string + } + SELECT [7] { + SELECT [8] { + IDENT [9] { + name: msg + }.oneof_type + }.payload + } + } + } + CALL [10] { + function: _?_:_ + args: { + CALL [11] { + function: _&&_ + args: { + CALL [12] { + function: _&&_ + args: { + SELECT [13] { + IDENT [14] { + name: msg + }.oneof_type~presence_test + } + SELECT [15] { + SELECT [16] { + IDENT [17] { + name: msg + }.oneof_type + }.payload~presence_test + } + } + } + SELECT [18] { + IDENT [19] { + name: @index1 + }.single_int64~presence_test + } + } + } + CALL [20] { + function: _?_:_ + args: { + CALL [21] { + function: _&&_ + args: { + SELECT [22] { + IDENT [23] { + name: @index1 + }.map_string_string~presence_test + } + SELECT [24] { + IDENT [25] { + name: @index0 + }.key~presence_test + } + } + } + CALL [26] { + function: _==_ + args: { + SELECT [27] { + IDENT [28] { + name: @index0 + }.key + } + CONSTANT [29] { value: "A" } + } + } + CONSTANT [30] { value: false } + } + } + CONSTANT [31] { 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: { + LIST [3] { + elements: { + CALL [4] { + function: optional.none + args: { + } + } + IDENT [5] { + name: opt_x + } + } + optional_indices: [0, 1] + } + LIST [6] { + elements: { + CONSTANT [7] { value: 5 } + } + } + } + } + CALL [8] { + function: _==_ + args: { + LIST [9] { + elements: { + CONSTANT [10] { value: 10 } + CALL [11] { + function: optional.none + args: { + } + } + IDENT [12] { + name: @index0 + } + IDENT [13] { + name: @index0 + } + } + optional_indices: [0] + } + LIST [14] { + elements: { + CONSTANT [15] { value: 10 } + IDENT [16] { + name: @index1 + } + IDENT [17] { + name: @index1 + } + } + } + } + } + } +} +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] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + }.single_int64 + } + } + } + CALL [7] { + function: _+_ + args: { + CALL [8] { + function: _+_ + args: { + CALL [9] { + function: _+_ + args: { + CALL [10] { + function: non_pure_custom_func + args: { + IDENT [11] { + name: @index0 + } + } + } + CALL [12] { + function: non_pure_custom_func + args: { + SELECT [13] { + SELECT [14] { + SELECT [15] { + IDENT [16] { + name: msg + }.oneof_type + }.payload + }.single_int32 + } + } + } + } + } + CALL [17] { + function: non_pure_custom_func + args: { + IDENT [18] { + name: @index0 + } + } + } + } + } + CALL [19] { + function: non_pure_custom_func + args: { + SELECT [20] { + IDENT [21] { + 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: { + CALL [3] { + function: pure_custom_func + args: { + SELECT [4] { + SELECT [5] { + SELECT [6] { + IDENT [7] { + name: msg + }.oneof_type + }.payload + }.single_int64 + } + } + } + } + } + CALL [8] { + function: _+_ + args: { + CALL [9] { + function: _+_ + args: { + CALL [10] { + function: _+_ + args: { + IDENT [11] { + name: @index0 + } + CALL [12] { + function: pure_custom_func + args: { + SELECT [13] { + SELECT [14] { + SELECT [15] { + IDENT [16] { + name: msg + }.oneof_type + }.payload + }.single_int32 + } + } + } + } + } + IDENT [17] { + name: @index0 + } + } + } + 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 new file mode 100644 index 000000000..016dc257d --- /dev/null +++ b/optimizer/src/test/resources/subexpression_ast_cascaded_binds.baseline @@ -0,0 +1,4795 @@ +Test case: SIZE_1 +Source: size([1,2]) + size([1,2]) + 1 == 5 +=====> +CALL [1] { + function: _==_ + args: { + CALL [2] { + function: _+_ + args: { + COMPREHENSION [3] { + iter_var: #unused + iter_range: { + LIST [4] { + elements: { + } + } + } + accu_var: @r0 + accu_init: { + CALL [5] { + function: size + args: { + LIST [6] { + elements: { + CONSTANT [7] { value: 1 } + CONSTANT [8] { value: 2 } + } + } + } + } + } + loop_condition: { + CONSTANT [9] { value: false } + } + loop_step: { + IDENT [10] { + name: @r0 + } + } + result: { + CALL [11] { + function: _+_ + args: { + IDENT [12] { + name: @r0 + } + IDENT [13] { + name: @r0 + } + } + } + } + } + CONSTANT [14] { value: 1 } + } + } + CONSTANT [15] { value: 5 } + } +} +Test case: SIZE_2 +Source: 2 + size([1,2]) + size([1,2]) + 1 == 7 +=====> +CALL [1] { + function: _==_ + args: { + CALL [2] { + function: _+_ + args: { + COMPREHENSION [3] { + iter_var: #unused + iter_range: { + LIST [4] { + elements: { + } + } + } + accu_var: @r0 + accu_init: { + CALL [5] { + function: size + args: { + LIST [6] { + elements: { + CONSTANT [7] { value: 1 } + CONSTANT [8] { value: 2 } + } + } + } + } + } + loop_condition: { + CONSTANT [9] { value: false } + } + loop_step: { + IDENT [10] { + name: @r0 + } + } + result: { + CALL [11] { + function: _+_ + args: { + CALL [12] { + function: _+_ + args: { + CONSTANT [13] { value: 2 } + IDENT [14] { + name: @r0 + } + } + } + IDENT [15] { + name: @r0 + } + } + } + } + } + CONSTANT [16] { value: 1 } + } + } + CONSTANT [17] { value: 7 } + } +} +Test case: SIZE_3 +Source: size([0]) + size([0]) + size([1,2]) + size([1,2]) == 6 +=====> +CALL [1] { + function: _==_ + args: { + COMPREHENSION [2] { + iter_var: #unused + iter_range: { + LIST [3] { + elements: { + } + } + } + accu_var: @r1 + accu_init: { + CALL [4] { + function: size + args: { + LIST [5] { + elements: { + CONSTANT [6] { value: 1 } + CONSTANT [7] { value: 2 } + } + } + } + } + } + loop_condition: { + CONSTANT [8] { value: false } + } + loop_step: { + IDENT [9] { + name: @r1 + } + } + result: { + CALL [10] { + function: _+_ + args: { + CALL [11] { + function: _+_ + args: { + COMPREHENSION [12] { + iter_var: #unused + iter_range: { + LIST [13] { + elements: { + } + } + } + accu_var: @r0 + accu_init: { + CALL [14] { + function: size + args: { + LIST [15] { + elements: { + CONSTANT [16] { value: 0 } + } + } + } + } + } + loop_condition: { + CONSTANT [17] { value: false } + } + loop_step: { + IDENT [18] { + name: @r0 + } + } + result: { + CALL [19] { + function: _+_ + args: { + IDENT [20] { + name: @r0 + } + IDENT [21] { + name: @r0 + } + } + } + } + } + IDENT [22] { + name: @r1 + } + } + } + IDENT [23] { + name: @r1 + } + } + } + } + } + CONSTANT [24] { 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: _==_ + args: { + COMPREHENSION [2] { + iter_var: #unused + iter_range: { + LIST [3] { + elements: { + } + } + } + accu_var: @r2 + accu_init: { + CALL [4] { + function: size + args: { + LIST [5] { + elements: { + CONSTANT [6] { value: 1 } + CONSTANT [7] { value: 2 } + CONSTANT [8] { value: 3 } + } + } + } + } + } + loop_condition: { + CONSTANT [9] { value: false } + } + loop_step: { + IDENT [10] { + name: @r2 + } + } + result: { + CALL [11] { + function: _+_ + args: { + CALL [12] { + function: _+_ + args: { + COMPREHENSION [13] { + iter_var: #unused + iter_range: { + LIST [14] { + elements: { + } + } + } + accu_var: @r1 + accu_init: { + CALL [15] { + function: size + args: { + LIST [16] { + elements: { + CONSTANT [17] { value: 1 } + CONSTANT [18] { value: 2 } + } + } + } + } + } + loop_condition: { + CONSTANT [19] { value: false } + } + loop_step: { + IDENT [20] { + name: @r1 + } + } + result: { + CALL [21] { + function: _+_ + args: { + CALL [22] { + function: _+_ + args: { + COMPREHENSION [23] { + iter_var: #unused + iter_range: { + LIST [24] { + elements: { + } + } + } + accu_var: @r0 + accu_init: { + CALL [25] { + function: size + args: { + LIST [26] { + elements: { + CONSTANT [27] { value: 0 } + } + } + } + } + } + loop_condition: { + CONSTANT [28] { value: false } + } + loop_step: { + IDENT [29] { + name: @r0 + } + } + result: { + CALL [30] { + function: _+_ + args: { + CALL [31] { + function: _+_ + args: { + CONSTANT [32] { value: 5 } + IDENT [33] { + name: @r0 + } + } + } + IDENT [34] { + name: @r0 + } + } + } + } + } + IDENT [35] { + name: @r1 + } + } + } + IDENT [36] { + name: @r1 + } + } + } + } + } + IDENT [37] { + name: @r2 + } + } + } + IDENT [38] { + name: @r2 + } + } + } + } + } + CONSTANT [39] { 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: _==_ + args: { + COMPREHENSION [2] { + iter_var: #unused + iter_range: { + LIST [3] { + elements: { + } + } + } + accu_var: @r0 + accu_init: { + CALL [4] { + function: getFullYear + target: { + CALL [5] { + function: timestamp + args: { + CALL [6] { + function: int + args: { + CALL [7] { + function: timestamp + args: { + CONSTANT [8] { value: 1000000000 } + } + } + } + } + } + } + } + args: { + } + } + } + loop_condition: { + CONSTANT [9] { value: false } + } + loop_step: { + IDENT [10] { + name: @r0 + } + } + result: { + CALL [11] { + function: _+_ + args: { + COMPREHENSION [12] { + iter_var: #unused + iter_range: { + LIST [13] { + elements: { + } + } + } + accu_var: @r3 + accu_init: { + CALL [14] { + function: timestamp + args: { + CALL [15] { + function: int + args: { + CALL [16] { + function: timestamp + args: { + CONSTANT [17] { value: 75 } + } + } + } + } + } + } + } + loop_condition: { + CONSTANT [18] { value: false } + } + loop_step: { + IDENT [19] { + name: @r3 + } + } + result: { + CALL [20] { + function: _+_ + args: { + COMPREHENSION [21] { + iter_var: #unused + iter_range: { + LIST [22] { + elements: { + } + } + } + accu_var: @r2 + accu_init: { + CALL [23] { + function: getFullYear + target: { + CALL [24] { + function: timestamp + args: { + CALL [25] { + function: int + args: { + CALL [26] { + function: timestamp + args: { + CONSTANT [27] { value: 200 } + } + } + } + } + } + } + } + args: { + } + } + } + loop_condition: { + CONSTANT [28] { value: false } + } + loop_step: { + IDENT [29] { + name: @r2 + } + } + result: { + CALL [30] { + function: _+_ + args: { + CALL [31] { + function: _+_ + args: { + COMPREHENSION [32] { + iter_var: #unused + iter_range: { + LIST [33] { + elements: { + } + } + } + accu_var: @r1 + accu_init: { + CALL [34] { + function: timestamp + args: { + CALL [35] { + function: int + args: { + CALL [36] { + function: timestamp + args: { + CONSTANT [37] { value: 50 } + } + } + } + } + } + } + } + loop_condition: { + CONSTANT [38] { value: false } + } + loop_step: { + IDENT [39] { + name: @r1 + } + } + result: { + CALL [40] { + function: _+_ + args: { + CALL [41] { + function: _+_ + args: { + CALL [42] { + function: _+_ + args: { + CALL [43] { + function: _+_ + args: { + IDENT [44] { + name: @r0 + } + CALL [45] { + function: getFullYear + target: { + IDENT [46] { + name: @r3 + } + } + args: { + } + } + } + } + CALL [47] { + function: getFullYear + target: { + IDENT [48] { + name: @r1 + } + } + args: { + } + } + } + } + IDENT [49] { + name: @r0 + } + } + } + CALL [50] { + function: getSeconds + target: { + IDENT [51] { + name: @r1 + } + } + args: { + } + } + } + } + } + } + IDENT [52] { + name: @r2 + } + } + } + IDENT [53] { + name: @r2 + } + } + } + } + } + CALL [54] { + function: getMinutes + target: { + IDENT [55] { + name: @r3 + } + } + args: { + } + } + } + } + } + } + IDENT [56] { + name: @r0 + } + } + } + } + } + CONSTANT [57] { value: 13934 } + } +} +Test case: MAP_INDEX +Source: {"a": 2}["a"] + {"a": 2}["a"] * {"a": 2}["a"] == 6 +=====> +CALL [1] { + function: _==_ + args: { + COMPREHENSION [2] { + iter_var: #unused + iter_range: { + LIST [3] { + elements: { + } + } + } + accu_var: @r0 + accu_init: { + CALL [4] { + function: _[_] + args: { + MAP [5] { + MAP_ENTRY [6] { + key: { + CONSTANT [7] { value: "a" } + } + value: { + CONSTANT [8] { value: 2 } + } + } + } + CONSTANT [9] { value: "a" } + } + } + } + loop_condition: { + CONSTANT [10] { value: false } + } + loop_step: { + IDENT [11] { + name: @r0 + } + } + result: { + CALL [12] { + function: _+_ + args: { + IDENT [13] { + name: @r0 + } + CALL [14] { + function: _*_ + args: { + IDENT [15] { + name: @r0 + } + IDENT [16] { + name: @r0 + } + } + } + } + } + } + } + CONSTANT [17] { value: 6 } + } +} +Test case: NESTED_MAP_CONSTRUCTION +Source: {'a': {'b': 1}, 'c': {'b': 1}, 'd': {'e': {'b': 1}}, 'e': {'e': {'b': 1}}} +=====> +COMPREHENSION [1] { + iter_var: #unused + iter_range: { + LIST [2] { + elements: { + } + } + } + accu_var: @r0 + accu_init: { + MAP [3] { + MAP_ENTRY [4] { + key: { + CONSTANT [5] { value: "b" } + } + value: { + CONSTANT [6] { value: 1 } + } + } + } + } + loop_condition: { + CONSTANT [7] { value: false } + } + loop_step: { + IDENT [8] { + name: @r0 + } + } + result: { + COMPREHENSION [9] { + iter_var: #unused + iter_range: { + LIST [10] { + elements: { + } + } + } + accu_var: @r1 + accu_init: { + MAP [11] { + MAP_ENTRY [12] { + key: { + CONSTANT [13] { value: "e" } + } + value: { + IDENT [14] { + name: @r0 + } + } + } + } + } + loop_condition: { + CONSTANT [15] { value: false } + } + loop_step: { + IDENT [16] { + name: @r1 + } + } + result: { + MAP [17] { + MAP_ENTRY [18] { + key: { + CONSTANT [19] { value: "a" } + } + value: { + IDENT [20] { + name: @r0 + } + } + } + MAP_ENTRY [21] { + key: { + CONSTANT [22] { value: "c" } + } + value: { + IDENT [23] { + name: @r0 + } + } + } + MAP_ENTRY [24] { + key: { + CONSTANT [25] { value: "d" } + } + value: { + IDENT [26] { + name: @r1 + } + } + } + MAP_ENTRY [27] { + key: { + CONSTANT [28] { value: "e" } + } + value: { + IDENT [29] { + name: @r1 + } + } + } + } + } + } + } +} +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]] +=====> +COMPREHENSION [1] { + iter_var: #unused + iter_range: { + LIST [2] { + elements: { + } + } + } + accu_var: @r0 + accu_init: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + CONSTANT [5] { value: 2 } + CONSTANT [6] { value: 3 } + CONSTANT [7] { value: 4 } + } + } + } + loop_condition: { + CONSTANT [8] { value: false } + } + loop_step: { + IDENT [9] { + name: @r0 + } + } + result: { + COMPREHENSION [10] { + iter_var: #unused + iter_range: { + LIST [11] { + elements: { + } + } + } + accu_var: @r1 + accu_init: { + LIST [12] { + elements: { + CONSTANT [13] { value: 1 } + CONSTANT [14] { value: 2 } + } + } + } + loop_condition: { + CONSTANT [15] { value: false } + } + loop_step: { + IDENT [16] { + name: @r1 + } + } + result: { + LIST [17] { + elements: { + CONSTANT [18] { value: 1 } + IDENT [19] { + name: @r0 + } + CONSTANT [20] { value: 2 } + IDENT [21] { + name: @r0 + } + CONSTANT [22] { value: 5 } + IDENT [23] { + name: @r0 + } + CONSTANT [24] { value: 7 } + LIST [25] { + elements: { + IDENT [26] { + name: @r1 + } + IDENT [27] { + name: @r0 + } + } + } + IDENT [28] { + name: @r1 + } + } + } + } + } + } +} +Test case: SELECT +Source: msg.single_int64 + msg.single_int64 == 6 +=====> +CALL [1] { + function: _==_ + args: { + COMPREHENSION [2] { + iter_var: #unused + iter_range: { + LIST [3] { + elements: { + } + } + } + accu_var: @r0 + accu_init: { + SELECT [4] { + IDENT [5] { + name: msg + }.single_int64 + } + } + loop_condition: { + CONSTANT [6] { value: false } + } + loop_step: { + IDENT [7] { + name: @r0 + } + } + result: { + CALL [8] { + function: _+_ + args: { + IDENT [9] { + name: @r0 + } + IDENT [10] { + name: @r0 + } + } + } + } + } + CONSTANT [11] { 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: _==_ + args: { + COMPREHENSION [2] { + iter_var: #unused + iter_range: { + LIST [3] { + elements: { + } + } + } + accu_var: @r0 + accu_init: { + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + } + } + loop_condition: { + CONSTANT [7] { value: false } + } + loop_step: { + IDENT [8] { + name: @r0 + } + } + result: { + CALL [9] { + function: _+_ + args: { + CALL [10] { + function: _+_ + args: { + COMPREHENSION [11] { + iter_var: #unused + iter_range: { + LIST [12] { + elements: { + } + } + } + accu_var: @r1 + accu_init: { + SELECT [13] { + IDENT [14] { + name: @r0 + }.single_int64 + } + } + loop_condition: { + CONSTANT [15] { value: false } + } + loop_step: { + IDENT [16] { + name: @r1 + } + } + result: { + CALL [17] { + function: _+_ + args: { + CALL [18] { + function: _+_ + args: { + IDENT [19] { + name: @r1 + } + SELECT [20] { + IDENT [21] { + name: @r0 + }.single_int32 + } + } + } + IDENT [22] { + name: @r1 + } + } + } + } + } + SELECT [23] { + IDENT [24] { + name: msg + }.single_int64 + } + } + } + SELECT [25] { + SELECT [26] { + SELECT [27] { + IDENT [28] { + name: @r0 + }.oneof_type + }.payload + }.single_int64 + } + } + } + } + } + CONSTANT [29] { 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 +=====> +COMPREHENSION [1] { + iter_var: #unused + iter_range: { + LIST [2] { + elements: { + } + } + } + accu_var: @r0 + accu_init: { + SELECT [3] { + SELECT [4] { + SELECT [5] { + SELECT [6] { + SELECT [7] { + IDENT [8] { + name: msg + }.oneof_type + }.payload + }.oneof_type + }.payload + }.oneof_type + } + } + loop_condition: { + CONSTANT [9] { value: false } + } + loop_step: { + IDENT [10] { + name: @r0 + } + } + result: { + CALL [11] { + function: _||_ + args: { + CALL [12] { + function: _||_ + args: { + CONSTANT [13] { value: true } + SELECT [14] { + SELECT [15] { + SELECT [16] { + SELECT [17] { + IDENT [18] { + name: @r0 + }.payload + }.oneof_type + }.payload + }.single_bool + } + } + } + SELECT [19] { + SELECT [20] { + SELECT [21] { + SELECT [22] { + IDENT [23] { + name: @r0 + }.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: _==_ + args: { + COMPREHENSION [2] { + iter_var: #unused + iter_range: { + LIST [3] { + elements: { + } + } + } + accu_var: @r0 + accu_init: { + CALL [4] { + function: _[_] + args: { + SELECT [5] { + SELECT [6] { + SELECT [7] { + IDENT [8] { + name: msg + }.oneof_type + }.payload + }.map_int32_int64 + } + CONSTANT [9] { value: 1 } + } + } + } + loop_condition: { + CONSTANT [10] { value: false } + } + loop_step: { + IDENT [11] { + name: @r0 + } + } + result: { + CALL [12] { + function: _+_ + args: { + CALL [13] { + function: _+_ + args: { + IDENT [14] { + name: @r0 + } + IDENT [15] { + name: @r0 + } + } + } + IDENT [16] { + name: @r0 + } + } + } + } + } + CONSTANT [17] { 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: _==_ + args: { + COMPREHENSION [2] { + iter_var: #unused + iter_range: { + LIST [3] { + elements: { + } + } + } + accu_var: @r0 + accu_init: { + SELECT [4] { + SELECT [5] { + SELECT [6] { + IDENT [7] { + name: msg + }.oneof_type + }.payload + }.map_int32_int64 + } + } + loop_condition: { + CONSTANT [8] { value: false } + } + loop_step: { + IDENT [9] { + name: @r0 + } + } + result: { + CALL [10] { + function: _+_ + args: { + CALL [11] { + function: _+_ + args: { + CALL [12] { + function: _[_] + args: { + IDENT [13] { + name: @r0 + } + CONSTANT [14] { value: 0 } + } + } + CALL [15] { + function: _[_] + args: { + IDENT [16] { + name: @r0 + } + CONSTANT [17] { value: 1 } + } + } + } + } + CALL [18] { + function: _[_] + args: { + IDENT [19] { + name: @r0 + } + CONSTANT [20] { value: 2 } + } + } + } + } + } + } + CONSTANT [21] { 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: _==_ + args: { + COMPREHENSION [2] { + iter_var: #unused + iter_range: { + LIST [3] { + elements: { + } + } + } + accu_var: @r0 + accu_init: { + SELECT [4] { + IDENT [5] { + name: msg + }.single_int64 + } + } + loop_condition: { + CONSTANT [6] { value: false } + } + loop_step: { + IDENT [7] { + name: @r0 + } + } + result: { + CALL [8] { + function: _?_:_ + args: { + CALL [9] { + function: _>_ + args: { + IDENT [10] { + name: @r0 + } + CONSTANT [11] { value: 0 } + } + } + IDENT [12] { + name: @r0 + } + CONSTANT [13] { value: 0 } + } + } + } + } + CONSTANT [14] { value: 3 } + } +} +Test case: TERNARY_BIND_RHS_ONLY +Source: false ? false : (msg.single_int64) + ((msg.single_int64 + 1) * 2) == 11 +=====> +CALL [1] { + function: _?_:_ + args: { + CONSTANT [2] { value: false } + CONSTANT [3] { value: false } + CALL [4] { + function: _==_ + args: { + COMPREHENSION [5] { + iter_var: #unused + iter_range: { + LIST [6] { + elements: { + } + } + } + accu_var: @r0 + accu_init: { + SELECT [7] { + IDENT [8] { + name: msg + }.single_int64 + } + } + loop_condition: { + CONSTANT [9] { value: false } + } + loop_step: { + IDENT [10] { + name: @r0 + } + } + result: { + CALL [11] { + function: _+_ + args: { + IDENT [12] { + name: @r0 + } + CALL [13] { + function: _*_ + args: { + CALL [14] { + function: _+_ + args: { + IDENT [15] { + name: @r0 + } + CONSTANT [16] { value: 1 } + } + } + CONSTANT [17] { value: 2 } + } + } + } + } + } + } + CONSTANT [18] { 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: _==_ + args: { + COMPREHENSION [2] { + iter_var: #unused + iter_range: { + LIST [3] { + elements: { + } + } + } + accu_var: @r0 + accu_init: { + SELECT [4] { + IDENT [5] { + name: msg + }.single_int64 + } + } + loop_condition: { + CONSTANT [6] { value: false } + } + loop_step: { + IDENT [7] { + name: @r0 + } + } + result: { + CALL [8] { + function: _?_:_ + args: { + CALL [9] { + function: _>_ + args: { + IDENT [10] { + name: @r0 + } + CONSTANT [11] { value: 0 } + } + } + COMPREHENSION [12] { + iter_var: #unused + iter_range: { + LIST [13] { + elements: { + } + } + } + accu_var: @r1 + accu_init: { + SELECT [14] { + IDENT [15] { + name: msg + }.single_int32 + } + } + loop_condition: { + CONSTANT [16] { value: false } + } + loop_step: { + IDENT [17] { + name: @r1 + } + } + result: { + CALL [18] { + function: _?_:_ + args: { + CALL [19] { + function: _>_ + args: { + IDENT [20] { + name: @r1 + } + CONSTANT [21] { value: 0 } + } + } + CALL [22] { + function: _+_ + args: { + IDENT [23] { + name: @r0 + } + IDENT [24] { + name: @r1 + } + } + } + CONSTANT [25] { value: 0 } + } + } + } + } + CONSTANT [26] { value: 0 } + } + } + } + } + CONSTANT [27] { 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: _==_ + args: { + COMPREHENSION [2] { + iter_var: #unused + iter_range: { + LIST [3] { + elements: { + } + } + } + accu_var: @r1 + accu_init: { + LIST [4] { + elements: { + CONSTANT [5] { value: 2 } + } + } + } + loop_condition: { + CONSTANT [6] { value: false } + } + loop_step: { + IDENT [7] { + name: @r1 + } + } + result: { + CALL [8] { + function: _+_ + args: { + CALL [9] { + function: _+_ + args: { + COMPREHENSION [10] { + iter_var: #unused + iter_range: { + LIST [11] { + elements: { + } + } + } + accu_var: @r0 + accu_init: { + LIST [12] { + elements: { + CONSTANT [13] { value: 1 } + } + } + } + loop_condition: { + CONSTANT [14] { value: false } + } + loop_step: { + IDENT [15] { + name: @r0 + } + } + result: { + CALL [16] { + function: _+_ + args: { + CALL [17] { + function: size + args: { + 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 + } + } + } + } + } + } + } + } + } + } + } + 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 + } + } + } + } + } + } + } + } + } + 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 + } + } + } + } + } + } + } + } + } + } + } + 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 [6] { value: false } + } + loop_step: { + IDENT [7] { + name: @r1 + } + } + result: { + CALL [8] { + function: _+_ + args: { + CALL [9] { + function: _+_ + args: { + COMPREHENSION [10] { + iter_var: #unused + iter_range: { + LIST [11] { + elements: { + } + } + } + accu_var: @r0 + accu_init: { + LIST [12] { + elements: { + CONSTANT [13] { value: 1 } + } + } + } + loop_condition: { + CONSTANT [14] { value: false } + } + loop_step: { + IDENT [15] { + name: @r0 + } + } + result: { + CALL [16] { + function: _+_ + args: { + LIST [17] { + elements: { + COMPREHENSION [18] { + iter_var: @it:0 + iter_range: { + IDENT [19] { + name: @r0 + } + } + accu_var: @ac:0 + accu_init: { + CONSTANT [20] { value: false } + } + loop_condition: { + CALL [21] { + function: @not_strictly_false + args: { + CALL [22] { + function: !_ + args: { + IDENT [23] { + name: @ac:0 + } + } + } + } + } + } + loop_step: { + CALL [24] { + function: _||_ + args: { + IDENT [25] { + name: @ac:0 + } + CALL [26] { + function: _>_ + args: { + IDENT [27] { + name: @it:0 + } + CONSTANT [28] { value: 0 } + } + } + } + } + } + result: { + IDENT [29] { + name: @ac:0 + } + } + } + } + } + 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 + } + } + } + } + } + } + } + } + result: { + IDENT [28] { + name: @ac:1 + } + } + } + } + } + COMPREHENSION [29] { + iter_var: #unused + iter_range: { + LIST [30] { + elements: { + } + } + } + accu_var: @r1 + accu_init: { + 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: { + 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: { + 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: 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: { + } + } + } + accu_var: @r0 + accu_init: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + CONSTANT [5] { value: 2 } + CONSTANT [6] { value: 3 } + } + } + } + loop_condition: { + CONSTANT [7] { value: false } + } + loop_step: { + IDENT [8] { + name: @r0 + } + } + result: { + CALL [9] { + function: _==_ + args: { + 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 + } + } + } + } + } + } + } + } + result: { + IDENT [28] { + name: @ac:1 + } + } + } + COMPREHENSION [29] { + iter_var: @it:3 + iter_range: { + IDENT [30] { + name: @r0 + } + } + accu_var: @ac:3 + accu_init: { + LIST [31] { + elements: { + } + } + } + loop_condition: { + CONSTANT [32] { value: true } + } + 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 + } + } + accu_var: @ac:2 + accu_init: { + LIST [38] { + elements: { + } + } + } + loop_condition: { + CONSTANT [39] { value: true } + } + loop_step: { + CALL [40] { + function: _+_ + args: { + IDENT [41] { + name: @ac:2 + } + LIST [42] { + elements: { + CALL [43] { + function: _+_ + args: { + IDENT [44] { + name: @it:2 + } + CONSTANT [45] { value: 1 } + } + } + } + } + } + } + } + result: { + IDENT [46] { + name: @ac:2 + } + } + } + } + } + } + } + } + result: { + IDENT [47] { + name: @ac:3 + } + } + } + } + } + } +} +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] +=====> +COMPREHENSION [1] { + iter_var: #unused + iter_range: { + LIST [2] { + elements: { + } + } + } + accu_var: @r0 + accu_init: { + LIST [3] { + elements: { + CONSTANT [4] { value: 1 } + CONSTANT [5] { value: 2 } + CONSTANT [6] { value: 3 } + } + } + } + loop_condition: { + CONSTANT [7] { value: false } + } + loop_step: { + IDENT [8] { + name: @r0 + } + } + result: { + COMPREHENSION [9] { + iter_var: #unused + iter_range: { + LIST [10] { + elements: { + } + } + } + accu_var: @r1 + accu_init: { + CALL [11] { + function: @in + args: { + CONSTANT [12] { value: 1 } + IDENT [13] { + name: @r0 + } + } + } + } + loop_condition: { + CONSTANT [14] { value: false } + } + loop_step: { + IDENT [15] { + name: @r1 + } + } + result: { + CALL [16] { + function: _&&_ + args: { + CALL [17] { + function: _&&_ + args: { + IDENT [18] { + name: @r1 + } + CALL [19] { + function: @in + args: { + CONSTANT [20] { value: 2 } + IDENT [21] { + name: @r0 + } + } + } + } + } + CALL [22] { + function: _&&_ + args: { + CALL [23] { + function: @in + args: { + CONSTANT [24] { value: 3 } + LIST [25] { + elements: { + CONSTANT [26] { value: 3 } + IDENT [27] { + name: @r0 + } + } + } + } + } + IDENT [28] { + name: @r1 + } + } + } + } + } + } + } + } +} +Test case: INCLUSION_MAP +Source: 2 in {'a': 1, 2: {true: false}, 3: {true: false}} +=====> +CALL [1] { + function: @in + args: { + CONSTANT [2] { value: 2 } + COMPREHENSION [3] { + iter_var: #unused + iter_range: { + LIST [4] { + elements: { + } + } + } + accu_var: @r0 + accu_init: { + MAP [5] { + MAP_ENTRY [6] { + key: { + CONSTANT [7] { value: true } + } + value: { + CONSTANT [8] { value: false } + } + } + } + } + loop_condition: { + CONSTANT [9] { value: false } + } + loop_step: { + IDENT [10] { + name: @r0 + } + } + result: { + MAP [11] { + MAP_ENTRY [12] { + key: { + CONSTANT [13] { value: "a" } + } + value: { + CONSTANT [14] { value: 1 } + } + } + MAP_ENTRY [15] { + key: { + CONSTANT [16] { value: 2 } + } + value: { + IDENT [17] { + name: @r0 + } + } + } + MAP_ENTRY [18] { + key: { + CONSTANT [19] { value: 3 } + } + value: { + IDENT [20] { + name: @r0 + } + } + } + } + } + } + } +} +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]]] +=====> +COMPREHENSION [1] { + iter_var: #unused + iter_range: { + LIST [2] { + elements: { + } + } + } + accu_var: @r1 + accu_init: { + LIST [3] { + elements: { + CONSTANT [4] { value: 3 } + CONSTANT [5] { value: 4 } + } + } + } + loop_condition: { + CONSTANT [6] { value: false } + } + loop_step: { + IDENT [7] { + name: @r1 + } + } + result: { + CALL [8] { + function: _==_ + args: { + COMPREHENSION [9] { + iter_var: #unused + iter_range: { + LIST [10] { + elements: { + } + } + } + accu_var: @r0 + accu_init: { + LIST [11] { + elements: { + CONSTANT [12] { value: 1 } + CONSTANT [13] { value: 2 } + } + } + } + loop_condition: { + CONSTANT [14] { value: false } + } + loop_step: { + IDENT [15] { + name: @r0 + } + } + result: { + COMPREHENSION [16] { + iter_var: @it:1 + iter_range: { + IDENT [17] { + name: @r0 + } + } + accu_var: @ac:1 + accu_init: { + LIST [18] { + elements: { + } + } + } + loop_condition: { + CONSTANT [19] { value: true } + } + loop_step: { + CALL [20] { + function: _+_ + args: { + IDENT [21] { + name: @ac:1 + } + LIST [22] { + elements: { + COMPREHENSION [23] { + iter_var: @it:0 + iter_range: { + IDENT [24] { + name: @r0 + } + } + accu_var: @ac:0 + accu_init: { + LIST [25] { + elements: { + } + } + } + loop_condition: { + CONSTANT [26] { value: true } + } + loop_step: { + CALL [27] { + function: _+_ + args: { + IDENT [28] { + name: @ac:0 + } + LIST [29] { + elements: { + IDENT [30] { + name: @r1 + } + } + } + } + } + } + result: { + IDENT [31] { + name: @ac:0 + } + } + } + } + } + } + } + } + result: { + IDENT [32] { + name: @ac:1 + } + } + } + } + } + COMPREHENSION [33] { + iter_var: #unused + iter_range: { + LIST [34] { + elements: { + } + } + } + accu_var: @r2 + accu_init: { + LIST [35] { + elements: { + IDENT [36] { + name: @r1 + } + IDENT [37] { + name: @r1 + } + } + } + } + loop_condition: { + CONSTANT [38] { value: false } + } + loop_step: { + IDENT [39] { + name: @r2 + } + } + result: { + LIST [40] { + elements: { + IDENT [41] { + name: @r2 + } + IDENT [42] { + name: @r2 + } + } + } + } + } + } + } + } +} +Test case: MACRO_SHADOWED_VARIABLE +Source: [x - 1 > 3 ? x - 1 : 5].exists(x, x - 1 > 3) || x - 1 > 3 +=====> +COMPREHENSION [1] { + iter_var: #unused + iter_range: { + LIST [2] { + elements: { + } + } + } + accu_var: @r0 + accu_init: { + CALL [3] { + function: _-_ + args: { + IDENT [4] { + name: x + } + CONSTANT [5] { value: 1 } + } + } + } + loop_condition: { + CONSTANT [6] { value: false } + } + loop_step: { + IDENT [7] { + name: @r0 + } + } + result: { + COMPREHENSION [8] { + iter_var: #unused + iter_range: { + LIST [9] { + elements: { + } + } + } + accu_var: @r1 + accu_init: { + CALL [10] { + function: _>_ + args: { + IDENT [11] { + name: @r0 + } + CONSTANT [12] { value: 3 } + } + } + } + loop_condition: { + CONSTANT [13] { value: false } + } + loop_step: { + IDENT [14] { + name: @r1 + } + } + result: { + CALL [15] { + function: _||_ + args: { + COMPREHENSION [16] { + iter_var: @it:0 + iter_range: { + LIST [17] { + elements: { + CALL [18] { + function: _?_:_ + args: { + IDENT [19] { + name: @r1 + } + IDENT [20] { + name: @r0 + } + CONSTANT [21] { value: 5 } + } + } + } + } + } + accu_var: @ac: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 + } + } + } + } + } + } + loop_step: { + CALL [26] { + function: _||_ + args: { + IDENT [27] { + name: @ac:0 + } + CALL [28] { + function: _>_ + args: { + CALL [29] { + function: _-_ + args: { + IDENT [30] { + name: @it:0 + } + CONSTANT [31] { value: 1 } + } + } + CONSTANT [32] { value: 3 } + } + } + } + } + } + result: { + IDENT [33] { + name: @ac:0 + } + } + } + IDENT [34] { + name: @r1 + } + } + } + } + } + } +} +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: PRESENCE_TEST +Source: has({'a': true}.a) && {'a':true}['a'] +=====> +COMPREHENSION [1] { + iter_var: #unused + iter_range: { + LIST [2] { + elements: { + } + } + } + accu_var: @r0 + accu_init: { + MAP [3] { + MAP_ENTRY [4] { + key: { + CONSTANT [5] { value: "a" } + } + value: { + CONSTANT [6] { value: true } + } + } + } + } + loop_condition: { + CONSTANT [7] { value: false } + } + loop_step: { + IDENT [8] { + name: @r0 + } + } + result: { + CALL [9] { + function: _&&_ + args: { + SELECT [10] { + IDENT [11] { + name: @r0 + }.a~presence_test + } + CALL [12] { + function: _[_] + args: { + IDENT [13] { + name: @r0 + } + CONSTANT [14] { value: "a" } + } + } + } + } + } +} +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 +=====> +CALL [1] { + function: _==_ + args: { + COMPREHENSION [2] { + iter_var: #unused + iter_range: { + LIST [3] { + elements: { + } + } + } + accu_var: @r0 + accu_init: { + SELECT [4] { + IDENT [5] { + name: msg + }.oneof_type + } + } + loop_condition: { + CONSTANT [6] { value: false } + } + loop_step: { + IDENT [7] { + name: @r0 + } + } + result: { + CALL [8] { + function: _?_:_ + args: { + SELECT [9] { + IDENT [10] { + name: @r0 + }.payload~presence_test + } + SELECT [11] { + SELECT [12] { + IDENT [13] { + name: @r0 + }.payload + }.single_int64 + } + CONSTANT [14] { value: 0 } + } + } + } + } + CONSTANT [15] { 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: _==_ + args: { + COMPREHENSION [2] { + iter_var: #unused + iter_range: { + LIST [3] { + elements: { + } + } + } + accu_var: @r0 + accu_init: { + SELECT [4] { + IDENT [5] { + name: msg + }.oneof_type + } + } + loop_condition: { + CONSTANT [6] { value: false } + } + loop_step: { + IDENT [7] { + name: @r0 + } + } + result: { + COMPREHENSION [8] { + iter_var: #unused + iter_range: { + LIST [9] { + elements: { + } + } + } + accu_var: @r1 + accu_init: { + SELECT [10] { + SELECT [11] { + IDENT [12] { + name: @r0 + }.payload + }.single_int64 + } + } + loop_condition: { + CONSTANT [13] { value: false } + } + loop_step: { + IDENT [14] { + name: @r1 + } + } + result: { + CALL [15] { + function: _?_:_ + args: { + SELECT [16] { + IDENT [17] { + name: @r0 + }.payload~presence_test + } + IDENT [18] { + name: @r1 + } + CALL [19] { + function: _*_ + args: { + IDENT [20] { + name: @r1 + } + CONSTANT [21] { value: 0 } + } + } + } + } + } + } + } + } + CONSTANT [22] { 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: _==_ + args: { + COMPREHENSION [2] { + iter_var: #unused + iter_range: { + LIST [3] { + elements: { + } + } + } + accu_var: @r0 + accu_init: { + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + } + } + loop_condition: { + CONSTANT [7] { value: false } + } + loop_step: { + IDENT [8] { + name: @r0 + } + } + result: { + COMPREHENSION [9] { + iter_var: #unused + iter_range: { + LIST [10] { + elements: { + } + } + } + accu_var: @r1 + accu_init: { + SELECT [11] { + IDENT [12] { + name: @r0 + }.single_int64 + } + } + loop_condition: { + CONSTANT [13] { value: false } + } + loop_step: { + IDENT [14] { + name: @r1 + } + } + result: { + CALL [15] { + function: _?_:_ + args: { + SELECT [16] { + IDENT [17] { + name: @r0 + }.single_int64~presence_test + } + IDENT [18] { + name: @r1 + } + CALL [19] { + function: _*_ + args: { + IDENT [20] { + name: @r1 + } + CONSTANT [21] { value: 0 } + } + } + } + } + } + } + } + } + CONSTANT [22] { 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 +=====> +COMPREHENSION [1] { + iter_var: #unused + iter_range: { + LIST [2] { + elements: { + } + } + } + accu_var: @r0 + accu_init: { + SELECT [3] { + IDENT [4] { + name: msg + }.oneof_type + } + } + loop_condition: { + CONSTANT [5] { value: false } + } + loop_step: { + IDENT [6] { + name: @r0 + } + } + result: { + COMPREHENSION [7] { + iter_var: #unused + iter_range: { + LIST [8] { + elements: { + } + } + } + accu_var: @r1 + accu_init: { + SELECT [9] { + IDENT [10] { + name: @r0 + }.payload + } + } + loop_condition: { + CONSTANT [11] { value: false } + } + loop_step: { + IDENT [12] { + name: @r1 + } + } + result: { + CALL [13] { + function: _?_:_ + args: { + CALL [14] { + function: _&&_ + args: { + CALL [15] { + function: _&&_ + args: { + SELECT [16] { + IDENT [17] { + name: msg + }.oneof_type~presence_test + } + SELECT [18] { + IDENT [19] { + name: @r0 + }.payload~presence_test + } + } + } + SELECT [20] { + IDENT [21] { + name: @r1 + }.single_int64~presence_test + } + } + } + COMPREHENSION [22] { + iter_var: #unused + iter_range: { + LIST [23] { + elements: { + } + } + } + accu_var: @r2 + accu_init: { + SELECT [24] { + IDENT [25] { + name: @r1 + }.map_string_string + } + } + loop_condition: { + CONSTANT [26] { value: false } + } + loop_step: { + IDENT [27] { + name: @r2 + } + } + result: { + CALL [28] { + function: _?_:_ + args: { + CALL [29] { + function: _&&_ + args: { + SELECT [30] { + IDENT [31] { + name: @r1 + }.map_string_string~presence_test + } + SELECT [32] { + IDENT [33] { + name: @r2 + }.key~presence_test + } + } + } + CALL [34] { + function: _==_ + args: { + SELECT [35] { + IDENT [36] { + name: @r2 + }.key + } + CONSTANT [37] { value: "A" } + } + } + CONSTANT [38] { value: false } + } + } + } + } + CONSTANT [39] { value: false } + } + } + } + } + } +} +Test case: OPTIONAL_LIST +Source: [10, ?optional.none(), [?optional.none(), ?opt_x], [?optional.none(), ?opt_x]] == [10, [5], [5]] +=====> +CALL [1] { + function: _==_ + args: { + COMPREHENSION [2] { + iter_var: #unused + iter_range: { + LIST [3] { + elements: { + } + } + } + accu_var: @r0 + accu_init: { + CALL [4] { + function: optional.none + args: { + } + } + } + loop_condition: { + CONSTANT [5] { value: false } + } + loop_step: { + IDENT [6] { + name: @r0 + } + } + result: { + COMPREHENSION [7] { + iter_var: #unused + iter_range: { + LIST [8] { + elements: { + } + } + } + accu_var: @r1 + accu_init: { + LIST [9] { + elements: { + IDENT [10] { + name: @r0 + } + IDENT [11] { + name: opt_x + } + } + optional_indices: [0, 1] + } + } + loop_condition: { + CONSTANT [12] { value: false } + } + loop_step: { + IDENT [13] { + name: @r1 + } + } + result: { + LIST [14] { + elements: { + CONSTANT [15] { value: 10 } + IDENT [16] { + name: @r0 + } + IDENT [17] { + name: @r1 + } + IDENT [18] { + name: @r1 + } + } + optional_indices: [0] + } + } + } + } + } + COMPREHENSION [19] { + iter_var: #unused + iter_range: { + LIST [20] { + elements: { + } + } + } + accu_var: @r2 + accu_init: { + LIST [21] { + elements: { + CONSTANT [22] { value: 5 } + } + } + } + loop_condition: { + CONSTANT [23] { value: false } + } + loop_step: { + IDENT [24] { + name: @r2 + } + } + result: { + LIST [25] { + elements: { + CONSTANT [26] { value: 10 } + IDENT [27] { + name: @r2 + } + IDENT [28] { + name: @r2 + } + } + } + } + } + } +} +Test case: OPTIONAL_MAP +Source: {?'hello': optional.of('hello')}['hello'] + {?'hello': optional.of('hello')}['hello'] == 'hellohello' +=====> +CALL [1] { + function: _==_ + args: { + COMPREHENSION [2] { + iter_var: #unused + iter_range: { + LIST [3] { + elements: { + } + } + } + accu_var: @r0 + accu_init: { + CALL [4] { + function: _[_] + args: { + MAP [5] { + MAP_ENTRY [6] { + key: { + CONSTANT [7] { value: "hello" } + } + optional_entry: true + value: { + CALL [8] { + function: optional.of + args: { + CONSTANT [9] { value: "hello" } + } + } + } + } + } + CONSTANT [10] { value: "hello" } + } + } + } + loop_condition: { + CONSTANT [11] { value: false } + } + loop_step: { + IDENT [12] { + name: @r0 + } + } + result: { + CALL [13] { + function: _+_ + args: { + IDENT [14] { + name: @r0 + } + IDENT [15] { + name: @r0 + } + } + } + } + } + CONSTANT [16] { 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: _==_ + args: { + COMPREHENSION [2] { + iter_var: #unused + iter_range: { + LIST [3] { + elements: { + } + } + } + accu_var: @r0 + accu_init: { + MAP [4] { + MAP_ENTRY [5] { + key: { + CONSTANT [6] { value: "key" } + } + value: { + CONSTANT [7] { value: "test" } + } + } + } + } + loop_condition: { + CONSTANT [8] { value: false } + } + loop_step: { + IDENT [9] { + name: @r0 + } + } + result: { + CALL [10] { + function: orValue + target: { + CALL [11] { + function: or + target: { + CALL [12] { + function: _[?_] + args: { + MAP [13] { + MAP_ENTRY [14] { + key: { + CONSTANT [15] { value: "key" } + } + optional_entry: true + value: { + CALL [16] { + function: optional.of + args: { + CONSTANT [17] { value: "test" } + } + } + } + } + } + CONSTANT [18] { value: "bogus" } + } + } + } + args: { + CALL [19] { + function: _[?_] + args: { + IDENT [20] { + name: @r0 + } + CONSTANT [21] { value: "bogus" } + } + } + } + } + } + args: { + CALL [22] { + function: _[_] + args: { + IDENT [23] { + name: @r0 + } + CONSTANT [24] { value: "key" } + } + } + } + } + } + } + CONSTANT [25] { 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: _==_ + args: { + COMPREHENSION [2] { + iter_var: #unused + iter_range: { + LIST [3] { + elements: { + } + } + } + accu_var: @r0 + accu_init: { + STRUCT [4] { + name: TestAllTypes + entries: { + ENTRY [5] { + field_key: single_int64 + optional_entry: true + value: { + CALL [6] { + function: optional.ofNonZeroValue + args: { + CONSTANT [7] { value: 1 } + } + } + } + } + ENTRY [8] { + field_key: single_int32 + optional_entry: true + value: { + CALL [9] { + function: optional.of + args: { + CONSTANT [10] { value: 4 } + } + } + } + } + } + } + } + loop_condition: { + CONSTANT [11] { value: false } + } + loop_step: { + IDENT [12] { + name: @r0 + } + } + result: { + CALL [13] { + function: _+_ + args: { + SELECT [14] { + IDENT [15] { + name: @r0 + }.single_int32 + } + SELECT [16] { + IDENT [17] { + name: @r0 + }.single_int64 + } + } + } + } + } + CONSTANT [18] { value: 5 } + } +} +Test case: CALL +Source: ('h' + 'e' + 'l' + 'l' + 'o' + ' world').matches('h' + 'e' + 'l' + 'l' + 'o') +=====> +COMPREHENSION [1] { + iter_var: #unused + iter_range: { + LIST [2] { + elements: { + } + } + } + accu_var: @r0 + accu_init: { + 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" } + } + } + } + loop_condition: { + CONSTANT [12] { value: false } + } + loop_step: { + IDENT [13] { + name: @r0 + } + } + result: { + CALL [14] { + function: matches + target: { + CALL [15] { + function: _+_ + args: { + IDENT [16] { + name: @r0 + } + CONSTANT [17] { value: " world" } + } + } + } + args: { + IDENT [18] { + name: @r0 + } + } + } + } +} +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: _+_ + args: { + COMPREHENSION [2] { + iter_var: #unused + iter_range: { + LIST [3] { + elements: { + } + } + } + accu_var: @r0 + accu_init: { + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + } + } + loop_condition: { + CONSTANT [7] { value: false } + } + loop_step: { + IDENT [8] { + name: @r0 + } + } + result: { + COMPREHENSION [9] { + iter_var: #unused + iter_range: { + LIST [10] { + elements: { + } + } + } + accu_var: @r1 + accu_init: { + SELECT [11] { + IDENT [12] { + name: @r0 + }.single_int64 + } + } + loop_condition: { + CONSTANT [13] { value: false } + } + loop_step: { + IDENT [14] { + name: @r1 + } + } + result: { + CALL [15] { + function: _+_ + args: { + CALL [16] { + function: _+_ + args: { + CALL [17] { + function: non_pure_custom_func + args: { + IDENT [18] { + name: @r1 + } + } + } + CALL [19] { + function: non_pure_custom_func + args: { + SELECT [20] { + IDENT [21] { + name: @r0 + }.single_int32 + } + } + } + } + } + CALL [22] { + function: non_pure_custom_func + args: { + IDENT [23] { + name: @r1 + } + } + } + } + } + } + } + } + } + CALL [24] { + function: non_pure_custom_func + args: { + SELECT [25] { + IDENT [26] { + 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: _+_ + args: { + COMPREHENSION [2] { + iter_var: #unused + iter_range: { + LIST [3] { + elements: { + } + } + } + accu_var: @r0 + accu_init: { + SELECT [4] { + SELECT [5] { + IDENT [6] { + name: msg + }.oneof_type + }.payload + } + } + loop_condition: { + CONSTANT [7] { value: false } + } + loop_step: { + IDENT [8] { + name: @r0 + } + } + result: { + COMPREHENSION [9] { + iter_var: #unused + iter_range: { + LIST [10] { + elements: { + } + } + } + accu_var: @r1 + accu_init: { + CALL [11] { + function: pure_custom_func + args: { + SELECT [12] { + IDENT [13] { + name: @r0 + }.single_int64 + } + } + } + } + loop_condition: { + CONSTANT [14] { value: false } + } + loop_step: { + IDENT [15] { + name: @r1 + } + } + result: { + CALL [16] { + function: _+_ + args: { + CALL [17] { + function: _+_ + args: { + IDENT [18] { + name: @r1 + } + CALL [19] { + function: pure_custom_func + args: { + SELECT [20] { + IDENT [21] { + name: @r0 + }.single_int32 + } + } + } + } + } + IDENT [22] { + name: @r1 + } + } + } + } + } + } + } + CALL [23] { + function: pure_custom_func + args: { + SELECT [24] { + IDENT [25] { + name: msg + }.single_int64 + } + } + } + } +} \ No newline at end of file diff --git a/optimizer/src/test/resources/subexpression_unparsed.baseline b/optimizer/src/test/resources/subexpression_unparsed.baseline new file mode 100644 index 000000000..f2ec443d3 --- /dev/null +++ b/optimizer/src/test/resources/subexpression_unparsed.baseline @@ -0,0 +1,659 @@ +Test case: SIZE_1 +Source: size([1,2]) + size([1,2]) + 1 == 5 +=====> +Result: true +[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([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 +[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([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 +[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([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 +[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([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 +[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.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 +[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}["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}}} +[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]: 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.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 +[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], 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], 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], 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 +[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]: 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 +[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: 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]: cel.@block([[1, 2, 3], [2, 3, 4]], @index0.map(@it:0:0, @index0.map(@it:1:0, @it:1:0 + 1)) == [@index1, @index1, @index1]) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([[1, 2, 3], [2, 3, 4], [@index1, @index1, @index1]], @index0.map(@it:0:0, @index0.map(@it:1:0, @it:1:0 + 1)) == @index2) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([[1, 2, 3], [2, 3, 4], [@index1, @index1, @index1]], @index0.map(@it:0:0, @index0.map(@it:1:0, @it:1:0 + 1)) == @index2) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([[1, 2, 3], [2, 3, 4], [@index1, @index1, @index1]], @index0.map(@it:0:0, @index0.map(@it:1:0, @it:1:0 + 1)) == @index2) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([[1, 2, 3], [2, 3, 4], @index0.map(@it:1:0, @it:1:0 + 1)], @index0.map(@it:0:0, @index2) == [@index1, @index1, @index1]) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([[1, 2, 3], [2, 3, 4], @index0.map(@it:1:0, @it:1:0 + 1)], @index0.map(@it:0:0, @index2) == [@index1, @index1, @index1]) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([[1, 2, 3], [2, 3, 4], @index0.map(@it:1:0, @it:1:0 + 1)], @index0.map(@it:0:0, @index2) == [@index1, @index1, @index1]) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([[1, 2, 3], [2, 3, 4]], @index0.map(@it:0:0, @index0.map(@it:1:0, @it:1:0 + 1)) == [@index1, @index1, @index1]) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([[1, 2, 3], [2, 3, 4]], @index0.map(@it:0:0, @index0.map(@it:1:0, @it:1:0 + 1)) == [@index1, @index1, @index1]) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([[1, 2, 3], [2, 3, 4]], @index0.map(@it:0:0, @index0.map(@it:1:0, @it:1: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 +[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:0:0, @index1.filter(@it:1:0, @it:1:0 == @it:0:0)) == @index4) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([[[1], [2]], [1, 2], [1, 2, 3]], @index1.map(@it:0:0, @index2.filter(@it:1:0, @it:1:0 == @it:0:0)) == @index0) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([[[1], [2]], [1, 2], [1, 2, 3]], @index1.map(@it:0:0, @index2.filter(@it:1:0, @it:1:0 == @it:0:0)) == @index0) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([[1, 2, 3].filter(@it:1:0, @it:1:0 == @it:0:0)], [1, 2].map(@it:0:0, @index0) == [[1], [2]]) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([[1, 2, 3].filter(@it:1:0, @it:1:0 == @it:0:0)], [1, 2].map(@it:0:0, @index0) == [[1], [2]]) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([[1, 2, 3].filter(@it:1:0, @it:1:0 == @it:0:0)], [1, 2].map(@it:0:0, @index0) == [[1], [2]]) +[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: 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:0:0, @index0.map(@it:1:0, @it:1:0 + 1))], @index1 == @index1) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([[1, 2, 3]], @index0.map(@it:0:0, @index0.map(@it:1:0, @it:1:0 + 1)) == @index0.map(@it:0:0, @index0.map(@it:1:0, @it:1:0 + 1))) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([[1, 2, 3]], @index0.map(@it:0:0, @index0.map(@it:1:0, @it:1:0 + 1)) == @index0.map(@it:0:0, @index0.map(@it:1:0, @it:1:0 + 1))) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([[1, 2, 3]], @index0.map(@it:0:0, @index0.map(@it:1:0, @it:1:0 + 1)) == @index0.map(@it:0:0, @index0.map(@it:1:0, @it:1:0 + 1))) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([[1, 2, 3].map(@it:1:0, @it:1:0 + 1), [1, 2, 3].map(@it:0:0, @index0)], @index1 == @index1) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([[1, 2, 3].map(@it:1:0, @it:1:0 + 1), [1, 2, 3].map(@it:0:0, @index0)], @index1 == @index1) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([[1, 2, 3].map(@it:1:0, @it:1:0 + 1), [1, 2, 3].map(@it:0:0, @index0)], @index1 == @index1) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([[1, 2, 3].map(@it:0:0, [1, 2, 3].map(@it:1:0, @it:1:0 + 1))], @index0 == @index0) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([[1, 2, 3].map(@it:0:0, [1, 2, 3].map(@it:1:0, @it:1:0 + 1))], @index0 == @index0) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([[1, 2, 3].map(@it:0:0, [1, 2, 3].map(@it:1:0, @it:1: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 +[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, 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 +[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}], 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 +[BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([[1, 2], [3, 4], [@index1, @index1]], @index0.map(@it:0:0, @index0.map(@it:1:0, @index1)) == [@index2, @index2]) +[BLOCK_RECURSION_DEPTH_1]: cel.@block([[1, 2], [3, 4], [@index1, @index1], [@index1], [@index2, @index2]], @index0.map(@it:0:0, @index0.map(@it:1:0, @index1)) == @index4) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([[[3, 4], [3, 4]], [1, 2], [[3, 4]], @index1.map(@it:1:0, [3, 4]), [@index3]], @index1.map(@it:0:0, @index3) == [@index0, @index0]) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([[[3, 4], [3, 4]], [1, 2], [[3, 4]], @index1.map(@it:1:0, [3, 4])], @index1.map(@it:0:0, @index3) == [@index0, @index0]) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([[[3, 4], [3, 4]], [1, 2], @index1.map(@it:1:0, [3, 4])], @index1.map(@it:0:0, @index2) == [@index0, @index0]) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([[[3, 4], [3, 4]], [1, 2], @index1.map(@it:1:0, [3, 4])], @index1.map(@it:0:0, @index2) == [@index0, @index0]) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([[[3, 4], [3, 4]], [1, 2], @index1.map(@it:1:0, [3, 4])], @index1.map(@it:0:0, @index2) == [@index0, @index0]) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([[[3, 4], [3, 4]], [1, 2]], @index1.map(@it:0:0, @index1.map(@it:1:0, [3, 4])) == [@index0, @index0]) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([[[3, 4], [3, 4]], [1, 2]], @index1.map(@it:0:0, @index1.map(@it:1:0, [3, 4])) == [@index0, @index0]) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([[[3, 4], [3, 4]], [1, 2]], @index1.map(@it:0:0, @index1.map(@it:1: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 +[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]: ["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:1:0, [@it:1:0 + @it:1:0, @it:1:0 + @it:1:0]).map(@it:0:0, [@it:0:0 + @it:0:0, @it:0:0 + @it:0:0])) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([["foo", "bar"]], @index0.map(@it:1:0, [@it:1:0 + @it:1:0, @it:1:0 + @it:1:0]).map(@it:0:0, [@it:0:0 + @it:0:0, @it:0:0 + @it:0:0])) +[BLOCK_RECURSION_DEPTH_3]: cel.@block([["foo", "bar"]], @index0.map(@it:1:0, [@it:1:0 + @it:1:0, @it:1:0 + @it:1:0]).map(@it:0:0, [@it:0:0 + @it:0:0, @it:0:0 + @it:0:0])) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([["foo", "bar"]], @index0.map(@it:1:0, [@it:1:0 + @it:1:0, @it:1:0 + @it:1:0]).map(@it:0:0, [@it:0:0 + @it:0:0, @it:0:0 + @it:0:0])) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([["foo", "bar"].map(@it:1:0, [@it:1:0 + @it:1:0, @it:1:0 + @it:1:0])], @index0.map(@it:0:0, [@it:0:0 + @it:0:0, @it:0:0 + @it:0: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: PRESENCE_TEST +Source: has({'a': true}.a) && {'a':true}['a'] +=====> +Result: true +[BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([{"a": true}], has(@index0.a) && @index0["a"]) +[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 +[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([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, ?@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 +[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([{?"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 +[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"}, 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"]) == "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 +[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_int32, @index2.single_int64, @index3 + @index4], @index5 == 5) +[BLOCK_RECURSION_DEPTH_2]: cel.@block([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([TestAllTypes{?single_int64: optional.ofNonZeroValue(1), ?single_int32: optional.of(4)}], @index0.single_int32 + @index0.single_int64 == 5) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([TestAllTypes{?single_int64: optional.ofNonZeroValue(1), ?single_int32: optional.of(4)}], @index0.single_int32 + @index0.single_int64 == 5) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([TestAllTypes{?single_int64: optional.ofNonZeroValue(1), ?single_int32: optional.of(4)}], @index0.single_int32 + @index0.single_int64 == 5) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([TestAllTypes{?single_int64: optional.ofNonZeroValue(1), ?single_int32: optional.of(4)}], @index0.single_int32 + @index0.single_int64 == 5) +[BLOCK_RECURSION_DEPTH_7]: cel.@block([TestAllTypes{?single_int64: optional.ofNonZeroValue(1), ?single_int32: optional.of(4)}], @index0.single_int32 + @index0.single_int64 == 5) +[BLOCK_RECURSION_DEPTH_8]: cel.@block([TestAllTypes{?single_int64: optional.ofNonZeroValue(1), ?single_int32: optional.of(4)}], @index0.single_int32 + @index0.single_int64 == 5) +[BLOCK_RECURSION_DEPTH_9]: cel.@block([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 +[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" + "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 +[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"], "hello world".matches(@index0 + "o")) +[BLOCK_RECURSION_DEPTH_4]: 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 +[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").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]: ("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 +[BLOCK_COMMON_SUBEXPR_ONLY]: ("h" + "e" + "l" + "l" + "o" + " world").matches("w" + "o" + "r" + "l" + "d") +[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 +[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/parser/BUILD.bazel b/parser/BUILD.bazel index ba31cd46a..f05a5cf93 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,14 +7,25 @@ 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 = "unparser_visitor", + exports = ["//parser/src/main/java/dev/cel/parser:unparser_visitor"], +) + java_library( name = "macro", exports = ["//parser/src/main/java/dev/cel/parser:macro"], @@ -25,7 +38,7 @@ java_library( 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..2bbc8c0e2 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, @@ -45,12 +58,15 @@ java_library( ":macro", ":operator", ":parser_builder", - "//common", + "//common:cel_ast", + "//common:cel_source", "//common:compiler_common", "//common:options", + "//common:source_location", "//common/annotations", "//common/ast", "//common/internal", + "//common/internal:env_visitor", "//parser:cel_g4_visitors", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", @@ -65,7 +81,7 @@ java_library( ], deps = [ ":macro", - "//common", + "//common:cel_source", "//common:compiler_common", "//common:options", "@maven//:com_google_errorprone_error_prone_annotations", @@ -80,13 +96,12 @@ java_library( deps = [ ":operator", "//:auto_value", - "//common", "//common:compiler_common", + "//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", ], ) @@ -108,7 +123,7 @@ java_library( ], deps = [ ":unparser_visitor", - "//common", + "//common:cel_ast", "//common:options", ], ) @@ -120,9 +135,12 @@ java_library( ], deps = [ ":operator", - "//common", + "//common:cel_ast", + "//common:cel_source", "//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/CelMacro.java b/parser/src/main/java/dev/cel/parser/CelMacro.java index 06e48d8be..cf75714a0 100644 --- a/parser/src/main/java/dev/cel/parser/CelMacro.java +++ b/parser/src/main/java/dev/cel/parser/CelMacro.java @@ -19,70 +19,13 @@ import static com.google.common.base.Strings.isNullOrEmpty; import com.google.auto.value.AutoValue; -import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.CheckReturnValue; import com.google.errorprone.annotations.Immutable; -import dev.cel.common.CelIssue; -import dev.cel.common.ast.CelExpr; -import java.util.Optional; /** Describes a function signature to match and the {@link CelMacroExpander} to apply. */ @AutoValue @Immutable public abstract class CelMacro implements Comparable { - - private static final String ACCUMULATOR_VAR = "__result__"; - - /** Field presence test macro */ - public static final CelMacro HAS = - newGlobalMacro(Operator.HAS.getFunction(), 1, CelMacro::expandHasMacro); - - /** - * Boolean comprehension which asserts that a predicate holds true for all elements in the input - * range. - */ - public static final CelMacro ALL = - newReceiverMacro(Operator.ALL.getFunction(), 2, CelMacro::expandAllMacro); - - /** - * Boolean comprehension which asserts that a predicate holds true for at least one element in the - * input range. - */ - public static final CelMacro EXISTS = - newReceiverMacro(Operator.EXISTS.getFunction(), 2, CelMacro::expandExistsMacro); - - /** - * Boolean comprehension which asserts that a predicate holds true for exactly one element in the - * input range. - */ - public static final CelMacro EXISTS_ONE = - newReceiverMacro(Operator.EXISTS_ONE.getFunction(), 2, CelMacro::expandExistsOneMacro); - - /** - * Comprehension which applies a transform to each element in the input range and produces a list - * of equivalent size as output. - */ - public static final CelMacro MAP = - newReceiverMacro(Operator.MAP.getFunction(), 2, CelMacro::expandMapMacro); - - /** - * Comprehension which conditionally applies a transform to elements in the list which satisfy the - * filter predicate. - */ - public static final CelMacro MAP_FILTER = - newReceiverMacro(Operator.MAP.getFunction(), 3, CelMacro::expandMapMacro); - - /** - * Comprehension which produces a list containing elements in the input range which match the - * filter. - */ - public static final CelMacro FILTER = - newReceiverMacro(Operator.FILTER.getFunction(), 2, CelMacro::expandFilterMacro); - - /** Set of all standard macros supported by the CEL spec. */ - public static final ImmutableList STANDARD_MACROS = - ImmutableList.of(HAS, ALL, EXISTS, EXISTS_ONE, MAP, MAP_FILTER, FILTER); - // Package-private default constructor to prevent extensions outside of the codebase. CelMacro() {} @@ -241,186 +184,4 @@ abstract static class Builder { @CheckReturnValue abstract CelMacro build(); } - - // CelMacroExpander implementation for CEL's has() macro. - private static Optional expandHasMacro( - CelMacroExprFactory exprFactory, CelExpr target, ImmutableList arguments) { - checkNotNull(exprFactory); - checkNotNull(target); - checkArgument(arguments.size() == 1); - CelExpr arg = checkNotNull(arguments.get(0)); - if (arg.exprKind().getKind() != CelExpr.ExprKind.Kind.SELECT) { - return Optional.of(exprFactory.reportError("invalid argument to has() macro")); - } - return Optional.of(exprFactory.newSelect(arg.select().operand(), arg.select().field(), true)); - } - - // CelMacroExpander implementation for CEL's all() macro. - private static Optional expandAllMacro( - CelMacroExprFactory exprFactory, CelExpr target, ImmutableList arguments) { - checkNotNull(exprFactory); - checkNotNull(target); - checkArgument(arguments.size() == 2); - CelExpr arg0 = checkNotNull(arguments.get(0)); - if (arg0.exprKind().getKind() != CelExpr.ExprKind.Kind.IDENT) { - return Optional.of(reportArgumentError(exprFactory, 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)); - CelExpr step = - exprFactory.newGlobalCall( - Operator.LOGICAL_AND.getFunction(), exprFactory.newIdentifier(ACCUMULATOR_VAR), arg1); - CelExpr result = exprFactory.newIdentifier(ACCUMULATOR_VAR); - return Optional.of( - exprFactory.fold( - arg0.ident().name(), target, ACCUMULATOR_VAR, accuInit, condition, step, result)); - } - - // CelMacroExpander implementation for CEL's exists() macro. - private static Optional expandExistsMacro( - CelMacroExprFactory exprFactory, CelExpr target, ImmutableList arguments) { - checkNotNull(exprFactory); - checkNotNull(target); - checkArgument(arguments.size() == 2); - CelExpr arg0 = checkNotNull(arguments.get(0)); - if (arg0.exprKind().getKind() != CelExpr.ExprKind.Kind.IDENT) { - return Optional.of(reportArgumentError(exprFactory, arg0)); - } - CelExpr arg1 = checkNotNull(arguments.get(1)); - CelExpr accuInit = exprFactory.newBoolLiteral(false); - CelExpr condition = - exprFactory.newGlobalCall( - Operator.NOT_STRICTLY_FALSE.getFunction(), - exprFactory.newGlobalCall( - Operator.LOGICAL_NOT.getFunction(), exprFactory.newIdentifier(ACCUMULATOR_VAR))); - CelExpr step = - exprFactory.newGlobalCall( - Operator.LOGICAL_OR.getFunction(), exprFactory.newIdentifier(ACCUMULATOR_VAR), arg1); - CelExpr result = exprFactory.newIdentifier(ACCUMULATOR_VAR); - return Optional.of( - exprFactory.fold( - arg0.ident().name(), target, ACCUMULATOR_VAR, accuInit, condition, step, result)); - } - - // CelMacroExpander implementation for CEL's exists_one() macro. - private static Optional expandExistsOneMacro( - CelMacroExprFactory exprFactory, CelExpr target, ImmutableList arguments) { - checkNotNull(exprFactory); - checkNotNull(target); - checkArgument(arguments.size() == 2); - CelExpr arg0 = checkNotNull(arguments.get(0)); - if (arg0.exprKind().getKind() != CelExpr.ExprKind.Kind.IDENT) { - return Optional.of(reportArgumentError(exprFactory, arg0)); - } - CelExpr arg1 = checkNotNull(arguments.get(1)); - CelExpr zeroExpr = exprFactory.newIntLiteral(0); - CelExpr oneExpr = exprFactory.newIntLiteral(1); - CelExpr accuInit = zeroExpr; - 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)); - CelExpr result = - exprFactory.newGlobalCall( - Operator.EQUALS.getFunction(), exprFactory.newIdentifier(ACCUMULATOR_VAR), oneExpr); - return Optional.of( - exprFactory.fold( - arg0.ident().name(), target, ACCUMULATOR_VAR, accuInit, condition, step, result)); - } - - // CelMacroExpander implementation for CEL's map() macro. - private static Optional expandMapMacro( - CelMacroExprFactory exprFactory, CelExpr target, ImmutableList arguments) { - checkNotNull(exprFactory); - checkNotNull(target); - checkArgument(arguments.size() == 2 || arguments.size() == 3); - CelExpr arg0 = checkNotNull(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"))); - } - CelExpr arg1; - CelExpr arg2; - if (arguments.size() == 3) { - arg2 = checkNotNull(arguments.get(1)); - arg1 = checkNotNull(arguments.get(2)); - } else { - arg1 = checkNotNull(arguments.get(1)); - arg2 = null; - } - CelExpr accuInit = exprFactory.newList(); - CelExpr condition = exprFactory.newBoolLiteral(true); - CelExpr step = - exprFactory.newGlobalCall( - Operator.ADD.getFunction(), - exprFactory.newIdentifier(ACCUMULATOR_VAR), - exprFactory.newList(arg1)); - if (arg2 != null) { - step = - exprFactory.newGlobalCall( - Operator.CONDITIONAL.getFunction(), - arg2, - step, - exprFactory.newIdentifier(ACCUMULATOR_VAR)); - } - return Optional.of( - exprFactory.fold( - arg0.ident().name(), - target, - ACCUMULATOR_VAR, - accuInit, - condition, - step, - exprFactory.newIdentifier(ACCUMULATOR_VAR))); - } - - // CelMacroExpander implementation for CEL's filter() macro. - private static Optional expandFilterMacro( - CelMacroExprFactory exprFactory, CelExpr target, ImmutableList arguments) { - checkNotNull(exprFactory); - checkNotNull(target); - checkArgument(arguments.size() == 2); - CelExpr arg0 = checkNotNull(arguments.get(0)); - if (arg0.exprKind().getKind() != CelExpr.ExprKind.Kind.IDENT) { - return Optional.of(reportArgumentError(exprFactory, arg0)); - } - CelExpr arg1 = checkNotNull(arguments.get(1)); - CelExpr accuInit = exprFactory.newList(); - CelExpr condition = exprFactory.newBoolLiteral(true); - CelExpr step = - exprFactory.newGlobalCall( - Operator.ADD.getFunction(), - exprFactory.newIdentifier(ACCUMULATOR_VAR), - exprFactory.newList(arg0)); - step = - exprFactory.newGlobalCall( - Operator.CONDITIONAL.getFunction(), - arg1, - step, - exprFactory.newIdentifier(ACCUMULATOR_VAR)); - return Optional.of( - exprFactory.fold( - arg0.ident().name(), - target, - ACCUMULATOR_VAR, - accuInit, - condition, - step, - exprFactory.newIdentifier(ACCUMULATOR_VAR))); - } - - private static CelExpr reportArgumentError(CelMacroExprFactory exprFactory, CelExpr argument) { - return exprFactory.reportError( - CelIssue.formatError( - exprFactory.getSourceLocation(argument), "The argument must be a simple name")); - } } 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/CelParser.java b/parser/src/main/java/dev/cel/parser/CelParser.java index 53879d11b..da1b7ba2a 100644 --- a/parser/src/main/java/dev/cel/parser/CelParser.java +++ b/parser/src/main/java/dev/cel/parser/CelParser.java @@ -52,4 +52,6 @@ default CelValidationResult parse(String expression) { */ @CheckReturnValue CelValidationResult parse(CelSource source); + + CelParserBuilder toParserBuilder(); } diff --git a/parser/src/main/java/dev/cel/parser/CelParserImpl.java b/parser/src/main/java/dev/cel/parser/CelParserImpl.java index 5d9944397..2624731d8 100644 --- a/parser/src/main/java/dev/cel/parser/CelParserImpl.java +++ b/parser/src/main/java/dev/cel/parser/CelParserImpl.java @@ -16,15 +16,19 @@ import static com.google.common.base.Preconditions.checkNotNull; +import com.google.common.annotations.VisibleForTesting; 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; @@ -40,7 +44,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. @@ -50,6 +54,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; + /** Creates a new {@link Builder}. */ public static CelParserBuilder newBuilder() { return new Builder().setOptions(CelOptions.DEFAULT); @@ -65,6 +73,11 @@ public CelValidationResult parse(CelSource source) { return Parser.parse(this, source, getOptions()); } + @Override + public CelParserBuilder toParserBuilder() { + return new Builder(parserBuilder); + } + Optional findMacro(String key) { return Optional.ofNullable(macros.get(key)); } @@ -125,6 +138,7 @@ public CelParserBuilder addLibraries(Iterable librar return this; } + @CanIgnoreReturnValue @Override public Builder setOptions(CelOptions options) { this.options = checkNotNull(options); @@ -136,6 +150,23 @@ public CelOptions getOptions() { return this.options; } + // The following getters exist for asserting immutability for collections held by this builder, + // and shouldn't be exposed to the public. + @VisibleForTesting + List getStandardMacros() { + return this.standardMacros; + } + + @VisibleForTesting + Map getMacros() { + return this.macros; + } + + @VisibleForTesting + ImmutableSet.Builder getParserLibraries() { + return this.celParserLibraries; + } + @Override @CheckReturnValue public CelParserImpl build() { @@ -149,7 +180,7 @@ public CelParserImpl build() { standardMacros.stream() .map(CelStandardMacro::getDefinition) .forEach(celMacro -> builder.put(celMacro.getKey(), celMacro)); - return new CelParserImpl(builder.buildOrThrow(), checkNotNull(options)); + return new CelParserImpl(builder.buildOrThrow(), checkNotNull(options), this); } private Builder() { @@ -157,10 +188,28 @@ 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) { + private CelParserImpl( + ImmutableMap macros, CelOptions options, Builder parserBuilder) { this.macros = macros; this.options = options; + this.parserBuilder = new Builder(parserBuilder); + } + + @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 a4aa50fff..0dce8b295 100644 --- a/parser/src/main/java/dev/cel/parser/CelStandardMacro.java +++ b/parser/src/main/java/dev/cel/parser/CelStandardMacro.java @@ -14,50 +14,65 @@ package dev.cel.parser; +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.ImmutableSet; +import dev.cel.common.CelIssue; +import dev.cel.common.ast.CelExpr; +import java.util.Optional; /** * CelStandardMacro enum represents all of the macros defined as part of the CEL standard library. */ public enum CelStandardMacro { + /** Field presence test macro */ - HAS(CelMacro.HAS), + HAS(CelMacro.newGlobalMacro(Operator.HAS.getFunction(), 1, CelStandardMacro::expandHasMacro)), /** * Boolean comprehension which asserts that a predicate holds true for all elements in the input * range. */ - ALL(CelMacro.ALL), + ALL(CelMacro.newReceiverMacro(Operator.ALL.getFunction(), 2, CelStandardMacro::expandAllMacro)), /** * Boolean comprehension which asserts that a predicate holds true for at least one element in the * input range. */ - EXISTS(CelMacro.EXISTS), + EXISTS( + CelMacro.newReceiverMacro( + Operator.EXISTS.getFunction(), 2, CelStandardMacro::expandExistsMacro)), /** * Boolean comprehension which asserts that a predicate holds true for exactly one element in the * input range. */ - EXISTS_ONE(CelMacro.EXISTS_ONE), + EXISTS_ONE( + CelMacro.newReceiverMacro( + Operator.EXISTS_ONE.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. */ - MAP(CelMacro.MAP), + MAP(CelMacro.newReceiverMacro(Operator.MAP.getFunction(), 2, CelStandardMacro::expandMapMacro)), /** * Comprehension which conditionally applies a transform to elements in the list which satisfy the * filter predicate. */ - MAP_FILTER(CelMacro.MAP_FILTER), + MAP_FILTER( + CelMacro.newReceiverMacro(Operator.MAP.getFunction(), 3, CelStandardMacro::expandMapMacro)), /** * Comprehension which produces a list containing elements in the input range which match the * filter. */ - FILTER(CelMacro.FILTER); + FILTER( + CelMacro.newReceiverMacro( + Operator.FILTER.getFunction(), 2, CelStandardMacro::expandFilterMacro)); /** Set of all standard macros supported by the CEL spec. */ public static final ImmutableSet STANDARD_MACROS = @@ -78,4 +93,212 @@ public String getFunction() { public CelMacro getDefinition() { return macro; } + + // CelMacroExpander implementation for CEL's has() macro. + private static Optional expandHasMacro( + CelMacroExprFactory exprFactory, CelExpr target, ImmutableList arguments) { + checkNotNull(exprFactory); + checkNotNull(target); + checkArgument(arguments.size() == 1); + CelExpr arg = checkNotNull(arguments.get(0)); + if (arg.exprKind().getKind() != CelExpr.ExprKind.Kind.SELECT) { + return Optional.of(exprFactory.reportError("invalid argument to has() macro")); + } + return Optional.of(exprFactory.newSelect(arg.select().operand(), arg.select().field(), true)); + } + + // CelMacroExpander implementation for CEL's all() macro. + private static Optional expandAllMacro( + CelMacroExprFactory exprFactory, CelExpr target, ImmutableList arguments) { + checkNotNull(exprFactory); + checkNotNull(target); + checkArgument(arguments.size() == 2); + CelExpr arg0 = checkNotNull(arguments.get(0)); + if (arg0.exprKind().getKind() != CelExpr.ExprKind.Kind.IDENT) { + return Optional.of(reportArgumentError(exprFactory, arg0)); + } + CelExpr arg1 = checkNotNull(arguments.get(1)); + 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()), + arg1); + CelExpr result = exprFactory.newIdentifier(exprFactory.getAccumulatorVarName()); + return Optional.of( + exprFactory.fold( + arg0.ident().name(), + target, + exprFactory.getAccumulatorVarName(), + accuInit, + condition, + step, + result)); + } + + // CelMacroExpander implementation for CEL's exists() macro. + private static Optional expandExistsMacro( + CelMacroExprFactory exprFactory, CelExpr target, ImmutableList arguments) { + checkNotNull(exprFactory); + checkNotNull(target); + checkArgument(arguments.size() == 2); + CelExpr arg0 = checkNotNull(arguments.get(0)); + if (arg0.exprKind().getKind() != CelExpr.ExprKind.Kind.IDENT) { + return Optional.of(reportArgumentError(exprFactory, arg0)); + } + CelExpr arg1 = checkNotNull(arguments.get(1)); + 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()), + arg1); + CelExpr result = exprFactory.newIdentifier(exprFactory.getAccumulatorVarName()); + return Optional.of( + exprFactory.fold( + arg0.ident().name(), + target, + exprFactory.getAccumulatorVarName(), + accuInit, + condition, + step, + result)); + } + + // CelMacroExpander implementation for CEL's exists_one() macro. + private static Optional expandExistsOneMacro( + CelMacroExprFactory exprFactory, CelExpr target, ImmutableList arguments) { + checkNotNull(exprFactory); + checkNotNull(target); + checkArgument(arguments.size() == 2); + CelExpr arg0 = checkNotNull(arguments.get(0)); + if (arg0.exprKind().getKind() != CelExpr.ExprKind.Kind.IDENT) { + return Optional.of(reportArgumentError(exprFactory, arg0)); + } + CelExpr arg1 = checkNotNull(arguments.get(1)); + 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(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(), + target, + exprFactory.getAccumulatorVarName(), + accuInit, + condition, + step, + result)); + } + + // CelMacroExpander implementation for CEL's map() macro. + private static Optional expandMapMacro( + CelMacroExprFactory exprFactory, CelExpr target, ImmutableList arguments) { + checkNotNull(exprFactory); + checkNotNull(target); + checkArgument(arguments.size() == 2 || arguments.size() == 3); + CelExpr arg0 = checkNotNull(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"))); + } + CelExpr arg1; + CelExpr arg2; + if (arguments.size() == 3) { + arg2 = checkNotNull(arguments.get(1)); + arg1 = checkNotNull(arguments.get(2)); + } else { + arg1 = checkNotNull(arguments.get(1)); + arg2 = null; + } + CelExpr accuInit = exprFactory.newList(); + CelExpr condition = exprFactory.newBoolLiteral(true); + CelExpr step = + exprFactory.newGlobalCall( + Operator.ADD.getFunction(), + exprFactory.newIdentifier(exprFactory.getAccumulatorVarName()), + exprFactory.newList(arg1)); + if (arg2 != null) { + step = + exprFactory.newGlobalCall( + Operator.CONDITIONAL.getFunction(), + arg2, + step, + exprFactory.newIdentifier(exprFactory.getAccumulatorVarName())); + } + return Optional.of( + exprFactory.fold( + arg0.ident().name(), + target, + exprFactory.getAccumulatorVarName(), + accuInit, + condition, + step, + exprFactory.newIdentifier(exprFactory.getAccumulatorVarName()))); + } + + // CelMacroExpander implementation for CEL's filter() macro. + private static Optional expandFilterMacro( + CelMacroExprFactory exprFactory, CelExpr target, ImmutableList arguments) { + checkNotNull(exprFactory); + checkNotNull(target); + checkArgument(arguments.size() == 2); + CelExpr arg0 = checkNotNull(arguments.get(0)); + if (arg0.exprKind().getKind() != CelExpr.ExprKind.Kind.IDENT) { + return Optional.of(reportArgumentError(exprFactory, arg0)); + } + CelExpr arg1 = checkNotNull(arguments.get(1)); + CelExpr accuInit = exprFactory.newList(); + CelExpr condition = exprFactory.newBoolLiteral(true); + CelExpr step = + exprFactory.newGlobalCall( + Operator.ADD.getFunction(), + exprFactory.newIdentifier(exprFactory.getAccumulatorVarName()), + exprFactory.newList(arg0)); + step = + exprFactory.newGlobalCall( + Operator.CONDITIONAL.getFunction(), + arg1, + step, + exprFactory.newIdentifier(exprFactory.getAccumulatorVarName())); + return Optional.of( + exprFactory.fold( + arg0.ident().name(), + target, + exprFactory.getAccumulatorVarName(), + accuInit, + condition, + step, + exprFactory.newIdentifier(exprFactory.getAccumulatorVarName()))); + } + + private static CelExpr reportArgumentError(CelMacroExprFactory exprFactory, CelExpr argument) { + return exprFactory.reportError( + CelIssue.formatError( + exprFactory.getSourceLocation(argument), "The argument must be a simple 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..398a554ca 100644 --- a/parser/src/main/java/dev/cel/parser/CelUnparserVisitor.java +++ b/parser/src/main/java/dev/cel/parser/CelUnparserVisitor.java @@ -13,20 +13,22 @@ // limitations under the License. package dev.cel.parser; -import com.google.protobuf.ByteString; +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.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 +45,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 +66,14 @@ public String unparse() { 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 +177,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 +213,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); } @@ -263,7 +277,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 +319,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 +329,7 @@ private void visitMaybeNested(CelExpr expr, boolean nested) { } } - private boolean isBinaryOrTernaryOperator(CelExpr expr) { + protected boolean isBinaryOrTernaryOperator(CelExpr expr) { if (!isComplexOperator(expr)) { return false; } @@ -346,7 +360,7 @@ private boolean isComplexOperatorWithRespectTo(CelExpr expr, String op) { // 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)); diff --git a/parser/src/main/java/dev/cel/parser/Operator.java b/parser/src/main/java/dev/cel/parser/Operator.java index 8ae1b5461..25bd2ca5b 100644 --- a/parser/src/main/java/dev/cel/parser/Operator.java +++ b/parser/src/main/java/dev/cel/parser/Operator.java @@ -103,22 +103,34 @@ static Optional find(String 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(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 +180,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)); } diff --git a/parser/src/main/java/dev/cel/parser/Parser.java b/parser/src/main/java/dev/cel/parser/Parser.java index abc5eb21f..2e7b08dc0 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; @@ -125,6 +129,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 +148,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 +442,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 +534,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 +551,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 +588,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 +619,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 +637,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 +685,37 @@ private Optional visitMacro( CelExpr.newBuilder().setCall(callExpr.build()).build()); } + exprFactory.maybeDeleteId(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 +728,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 +746,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 +762,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 +908,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 +939,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 +1075,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 +1109,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 +1164,17 @@ private long nextExprId(int position) { return exprId; } + @Override + protected void maybeDeleteId(long id) { + sourceInfo.removePositions(id); + super.maybeDeleteId(id); + } + + @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..8b912168c 100644 --- a/parser/src/main/java/dev/cel/parser/gen/BUILD.bazel +++ b/parser/src/main/java/dev/cel/parser/gen/BUILD.bazel @@ -4,6 +4,7 @@ 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( 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..b6a89ff66 100644 --- a/parser/src/test/java/dev/cel/parser/BUILD.bazel +++ b/parser/src/test/java/dev/cel/parser/BUILD.bazel @@ -1,3 +1,4 @@ +load("@rules_java//java:defs.bzl", "java_library") load("//:testing.bzl", "junit4_test_suites") package(default_applicable_licenses = [ @@ -12,20 +13,25 @@ java_library( deps = [ "//:auto_value", "//:java_truth", - "//common", + "//common:cel_ast", + "//common:cel_source", "//common:compiler_common", "//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", "//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 68ac5e662..72abf81a4 100644 --- a/parser/src/test/java/dev/cel/parser/CelMacroExprFactoryTest.java +++ b/parser/src/test/java/dev/cel/parser/CelMacroExprFactoryTest.java @@ -15,18 +15,17 @@ package dev.cel.parser; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth8.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; @@ -42,6 +41,11 @@ private TestCelExprFactory() { exprId = 0L; } + @Override + public long copyExprId(long unused) { + return nextExprId(); + } + @Override public long nextExprId() { return ++exprId; @@ -57,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; @@ -94,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 @@ -151,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 @@ -173,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); } @@ -184,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 @@ -210,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); @@ -219,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/CelMacroTest.java b/parser/src/test/java/dev/cel/parser/CelMacroTest.java index 301d26449..585b01578 100644 --- a/parser/src/test/java/dev/cel/parser/CelMacroTest.java +++ b/parser/src/test/java/dev/cel/parser/CelMacroTest.java @@ -16,7 +16,6 @@ import static com.google.common.truth.Truth.assertThat; -import com.google.common.testing.EqualsTester; import dev.cel.common.ast.CelExpr; import java.util.Optional; import org.junit.Test; @@ -26,103 +25,6 @@ @RunWith(JUnit4.class) public final class CelMacroTest { - @Test - public void testHas() { - assertThat(CelMacro.HAS.getFunction()).isEqualTo(Operator.HAS.getFunction()); - assertThat(CelMacro.HAS.getArgumentCount()).isEqualTo(1); - assertThat(CelMacro.HAS.isReceiverStyle()).isFalse(); - assertThat(CelMacro.HAS.getKey()).isEqualTo("has:1:false"); - assertThat(CelMacro.HAS.isVariadic()).isFalse(); - assertThat(CelMacro.HAS.toString()).isEqualTo(CelMacro.HAS.getKey()); - assertThat(CelMacro.HAS.hashCode()).isEqualTo(CelMacro.HAS.getKey().hashCode()); - assertThat(CelMacro.HAS).isEquivalentAccordingToCompareTo(CelMacro.HAS); - } - - @Test - public void testAll() { - assertThat(CelMacro.ALL.getFunction()).isEqualTo(Operator.ALL.getFunction()); - assertThat(CelMacro.ALL.getArgumentCount()).isEqualTo(2); - assertThat(CelMacro.ALL.isReceiverStyle()).isTrue(); - assertThat(CelMacro.ALL.getKey()).isEqualTo("all:2:true"); - assertThat(CelMacro.ALL.isVariadic()).isFalse(); - assertThat(CelMacro.ALL.toString()).isEqualTo(CelMacro.ALL.getKey()); - assertThat(CelMacro.ALL.hashCode()).isEqualTo(CelMacro.ALL.getKey().hashCode()); - assertThat(CelMacro.ALL).isEquivalentAccordingToCompareTo(CelMacro.ALL); - } - - @Test - public void testExists() { - assertThat(CelMacro.EXISTS.getFunction()).isEqualTo(Operator.EXISTS.getFunction()); - assertThat(CelMacro.EXISTS.getArgumentCount()).isEqualTo(2); - assertThat(CelMacro.EXISTS.isReceiverStyle()).isTrue(); - assertThat(CelMacro.EXISTS.getKey()).isEqualTo("exists:2:true"); - assertThat(CelMacro.EXISTS.isVariadic()).isFalse(); - assertThat(CelMacro.EXISTS.toString()).isEqualTo(CelMacro.EXISTS.getKey()); - assertThat(CelMacro.EXISTS.hashCode()).isEqualTo(CelMacro.EXISTS.getKey().hashCode()); - assertThat(CelMacro.EXISTS).isEquivalentAccordingToCompareTo(CelMacro.EXISTS); - } - - @Test - public void testExistsOne() { - assertThat(CelMacro.EXISTS_ONE.getFunction()).isEqualTo(Operator.EXISTS_ONE.getFunction()); - assertThat(CelMacro.EXISTS_ONE.getArgumentCount()).isEqualTo(2); - assertThat(CelMacro.EXISTS_ONE.isReceiverStyle()).isTrue(); - assertThat(CelMacro.EXISTS_ONE.getKey()).isEqualTo("exists_one:2:true"); - assertThat(CelMacro.EXISTS_ONE.isVariadic()).isFalse(); - assertThat(CelMacro.EXISTS_ONE.toString()).isEqualTo(CelMacro.EXISTS_ONE.getKey()); - assertThat(CelMacro.EXISTS_ONE.hashCode()).isEqualTo(CelMacro.EXISTS_ONE.getKey().hashCode()); - assertThat(CelMacro.EXISTS_ONE).isEquivalentAccordingToCompareTo(CelMacro.EXISTS_ONE); - } - - @Test - public void testMap2() { - assertThat(CelMacro.MAP.getFunction()).isEqualTo(Operator.MAP.getFunction()); - assertThat(CelMacro.MAP.getArgumentCount()).isEqualTo(2); - assertThat(CelMacro.MAP.isReceiverStyle()).isTrue(); - assertThat(CelMacro.MAP.getKey()).isEqualTo("map:2:true"); - assertThat(CelMacro.MAP.isVariadic()).isFalse(); - assertThat(CelMacro.MAP.toString()).isEqualTo(CelMacro.MAP.getKey()); - assertThat(CelMacro.MAP.hashCode()).isEqualTo(CelMacro.MAP.getKey().hashCode()); - assertThat(CelMacro.MAP).isEquivalentAccordingToCompareTo(CelMacro.MAP); - } - - @Test - public void testMap3() { - assertThat(CelMacro.MAP_FILTER.getFunction()).isEqualTo(Operator.MAP.getFunction()); - assertThat(CelMacro.MAP_FILTER.getArgumentCount()).isEqualTo(3); - assertThat(CelMacro.MAP_FILTER.isReceiverStyle()).isTrue(); - assertThat(CelMacro.MAP_FILTER.getKey()).isEqualTo("map:3:true"); - assertThat(CelMacro.MAP_FILTER.isVariadic()).isFalse(); - assertThat(CelMacro.MAP_FILTER.toString()).isEqualTo(CelMacro.MAP_FILTER.getKey()); - assertThat(CelMacro.MAP_FILTER.hashCode()).isEqualTo(CelMacro.MAP_FILTER.getKey().hashCode()); - assertThat(CelMacro.MAP_FILTER).isEquivalentAccordingToCompareTo(CelMacro.MAP_FILTER); - } - - @Test - public void testFilter() { - assertThat(CelMacro.FILTER.getFunction()).isEqualTo(Operator.FILTER.getFunction()); - assertThat(CelMacro.FILTER.getArgumentCount()).isEqualTo(2); - assertThat(CelMacro.FILTER.isReceiverStyle()).isTrue(); - assertThat(CelMacro.FILTER.getKey()).isEqualTo("filter:2:true"); - assertThat(CelMacro.FILTER.isVariadic()).isFalse(); - assertThat(CelMacro.FILTER.toString()).isEqualTo(CelMacro.FILTER.getKey()); - assertThat(CelMacro.FILTER.hashCode()).isEqualTo(CelMacro.FILTER.getKey().hashCode()); - assertThat(CelMacro.FILTER).isEquivalentAccordingToCompareTo(CelMacro.FILTER); - } - - @Test - public void testEquals() { - new EqualsTester() - .addEqualityGroup(CelMacro.HAS) - .addEqualityGroup(CelMacro.ALL) - .addEqualityGroup(CelMacro.EXISTS) - .addEqualityGroup(CelMacro.EXISTS_ONE) - .addEqualityGroup(CelMacro.MAP) - .addEqualityGroup(CelMacro.MAP_FILTER) - .addEqualityGroup(CelMacro.FILTER) - .testEquals(); - } - @Test public void testGlobalVarArgMacro() { CelMacro macro = diff --git a/parser/src/test/java/dev/cel/parser/CelParserImplTest.java b/parser/src/test/java/dev/cel/parser/CelParserImplTest.java index 8a9f1439e..f12fef292 100644 --- a/parser/src/test/java/dev/cel/parser/CelParserImplTest.java +++ b/parser/src/test/java/dev/cel/parser/CelParserImplTest.java @@ -15,7 +15,6 @@ package dev.cel.parser; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth8.assertThat; import static org.junit.Assert.assertThrows; import com.google.testing.junit.testparameterinjector.TestParameter; @@ -40,14 +39,17 @@ public final class CelParserImplTest { @Test public void build_withMacros_containsAllMacros() { CelParserImpl parser = - (CelParserImpl) CelParserImpl.newBuilder().addMacros(CelMacro.STANDARD_MACROS).build(); - assertThat(parser.findMacro("has:1:false")).hasValue(CelMacro.HAS); - assertThat(parser.findMacro("all:2:true")).hasValue(CelMacro.ALL); - assertThat(parser.findMacro("exists:2:true")).hasValue(CelMacro.EXISTS); - assertThat(parser.findMacro("exists_one:2:true")).hasValue(CelMacro.EXISTS_ONE); - assertThat(parser.findMacro("map:2:true")).hasValue(CelMacro.MAP); - assertThat(parser.findMacro("map:3:true")).hasValue(CelMacro.MAP_FILTER); - assertThat(parser.findMacro("filter:2:true")).hasValue(CelMacro.FILTER); + (CelParserImpl) + CelParserImpl.newBuilder().setStandardMacros(CelStandardMacro.STANDARD_MACROS).build(); + assertThat(parser.findMacro("has:1:false")).hasValue(CelStandardMacro.HAS.getDefinition()); + assertThat(parser.findMacro("all:2:true")).hasValue(CelStandardMacro.ALL.getDefinition()); + assertThat(parser.findMacro("exists:2:true")).hasValue(CelStandardMacro.EXISTS.getDefinition()); + assertThat(parser.findMacro("exists_one:2:true")) + .hasValue(CelStandardMacro.EXISTS_ONE.getDefinition()); + assertThat(parser.findMacro("map:2:true")).hasValue(CelStandardMacro.MAP.getDefinition()); + assertThat(parser.findMacro("map:3:true")) + .hasValue(CelStandardMacro.MAP_FILTER.getDefinition()); + assertThat(parser.findMacro("filter:2:true")).hasValue(CelStandardMacro.FILTER.getDefinition()); } @Test @@ -55,13 +57,15 @@ public void build_withStandardMacros_containsAllMacros() { CelParserImpl parser = (CelParserImpl) CelParserImpl.newBuilder().setStandardMacros(CelStandardMacro.STANDARD_MACROS).build(); - assertThat(parser.findMacro("has:1:false")).hasValue(CelMacro.HAS); - assertThat(parser.findMacro("all:2:true")).hasValue(CelMacro.ALL); - assertThat(parser.findMacro("exists:2:true")).hasValue(CelMacro.EXISTS); - assertThat(parser.findMacro("exists_one:2:true")).hasValue(CelMacro.EXISTS_ONE); - assertThat(parser.findMacro("map:2:true")).hasValue(CelMacro.MAP); - assertThat(parser.findMacro("map:3:true")).hasValue(CelMacro.MAP_FILTER); - assertThat(parser.findMacro("filter:2:true")).hasValue(CelMacro.FILTER); + assertThat(parser.findMacro("has:1:false")).hasValue(CelStandardMacro.HAS.getDefinition()); + assertThat(parser.findMacro("all:2:true")).hasValue(CelStandardMacro.ALL.getDefinition()); + assertThat(parser.findMacro("exists:2:true")).hasValue(CelStandardMacro.EXISTS.getDefinition()); + assertThat(parser.findMacro("exists_one:2:true")) + .hasValue(CelStandardMacro.EXISTS_ONE.getDefinition()); + assertThat(parser.findMacro("map:2:true")).hasValue(CelStandardMacro.MAP.getDefinition()); + assertThat(parser.findMacro("map:3:true")) + .hasValue(CelStandardMacro.MAP_FILTER.getDefinition()); + assertThat(parser.findMacro("filter:2:true")).hasValue(CelStandardMacro.FILTER.getDefinition()); } @Test @@ -76,28 +80,30 @@ public void build_withStandardMacrosAndCustomMacros_containsAllMacros() { .addMacros(customMacro) .build(); - assertThat(parser.findMacro("has:1:false")).hasValue(CelMacro.HAS); - assertThat(parser.findMacro("all:2:true")).hasValue(CelMacro.ALL); - assertThat(parser.findMacro("exists:2:true")).hasValue(CelMacro.EXISTS); - assertThat(parser.findMacro("exists_one:2:true")).hasValue(CelMacro.EXISTS_ONE); - assertThat(parser.findMacro("map:2:true")).hasValue(CelMacro.MAP); - assertThat(parser.findMacro("map:3:true")).hasValue(CelMacro.MAP_FILTER); - assertThat(parser.findMacro("filter:2:true")).hasValue(CelMacro.FILTER); + assertThat(parser.findMacro("has:1:false")).hasValue(CelStandardMacro.HAS.getDefinition()); + assertThat(parser.findMacro("all:2:true")).hasValue(CelStandardMacro.ALL.getDefinition()); + assertThat(parser.findMacro("exists:2:true")).hasValue(CelStandardMacro.EXISTS.getDefinition()); + assertThat(parser.findMacro("exists_one:2:true")) + .hasValue(CelStandardMacro.EXISTS_ONE.getDefinition()); + assertThat(parser.findMacro("map:2:true")).hasValue(CelStandardMacro.MAP.getDefinition()); + assertThat(parser.findMacro("map:3:true")) + .hasValue(CelStandardMacro.MAP_FILTER.getDefinition()); + assertThat(parser.findMacro("filter:2:true")).hasValue(CelStandardMacro.FILTER.getDefinition()); assertThat(parser.findMacro("customMacro:1:true")).hasValue(customMacro); } @Test public void build_withMacro_containsMacro() { CelParserImpl parser = - (CelParserImpl) CelParserImpl.newBuilder().addMacros(CelMacro.HAS).build(); - assertThat(parser.findMacro("has:1:false")).hasValue(CelMacro.HAS); + (CelParserImpl) CelParserImpl.newBuilder().setStandardMacros(CelStandardMacro.HAS).build(); + assertThat(parser.findMacro("has:1:false")).hasValue(CelStandardMacro.HAS.getDefinition()); } @Test public void build_withStandardMacro_containsMacro() { CelParserImpl parser = (CelParserImpl) CelParserImpl.newBuilder().setStandardMacros(CelStandardMacro.HAS).build(); - assertThat(parser.findMacro("has:1:false")).hasValue(CelMacro.HAS); + assertThat(parser.findMacro("has:1:false")).hasValue(CelStandardMacro.HAS.getDefinition()); } @Test @@ -109,7 +115,7 @@ public void build_withStandardMacro_secondCallReplaces() { .setStandardMacros(CelStandardMacro.HAS) .build(); - assertThat(parser.findMacro("has:1:false")).hasValue(CelMacro.HAS); + assertThat(parser.findMacro("has:1:false")).hasValue(CelStandardMacro.HAS.getDefinition()); assertThat(parser.findMacro("all:2:true")).isEmpty(); } @@ -256,7 +262,8 @@ public void parse_exprUnderMaxRecursionLimit_doesNotThrow( @TestParameters("{expression: 'A.exists_one(a?b, c)'}") @TestParameters("{expression: 'A.filter(a?b, c)'}") public void parse_macroArgumentContainsSyntaxError_throws(String expression) { - CelParser parser = CelParserImpl.newBuilder().addMacros(CelMacro.STANDARD_MACROS).build(); + CelParser parser = + CelParserImpl.newBuilder().setStandardMacros(CelStandardMacro.STANDARD_MACROS).build(); CelValidationResult parseResult = parser.parse(expression); @@ -264,4 +271,44 @@ public void parse_macroArgumentContainsSyntaxError_throws(String expression) { assertThat(parseResult.getErrorString()).containsMatch("ERROR: .*mismatched input ','"); assertThrows(CelValidationException.class, parseResult::getAst); } + + @Test + public void toParserBuilder_isNewInstance() { + CelParserBuilder celParserBuilder = CelParserFactory.standardCelParserBuilder(); + CelParserImpl celParser = (CelParserImpl) celParserBuilder.build(); + + CelParserImpl.Builder newParserBuilder = (CelParserImpl.Builder) celParser.toParserBuilder(); + + assertThat(newParserBuilder).isNotEqualTo(celParserBuilder); + } + + @Test + public void toParserBuilder_isImmutable() { + CelParserBuilder originalParserBuilder = CelParserFactory.standardCelParserBuilder(); + CelParserImpl celParser = (CelParserImpl) originalParserBuilder.build(); + originalParserBuilder.addLibraries(new CelParserLibrary() {}); + + CelParserImpl.Builder newParserBuilder = (CelParserImpl.Builder) celParser.toParserBuilder(); + + assertThat(newParserBuilder.getParserLibraries().build()).isEmpty(); + } + + @Test + public void toParserBuilder_collectionProperties_copied() { + CelParserBuilder celParserBuilder = + CelParserFactory.standardCelParserBuilder() + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addMacros( + CelMacro.newGlobalMacro( + "test", 1, (a, b, c) -> Optional.of(CelExpr.newBuilder().build()))) + .addLibraries(new CelParserLibrary() {}); + CelParserImpl celParser = (CelParserImpl) celParserBuilder.build(); + + CelParserImpl.Builder newParserBuilder = (CelParserImpl.Builder) celParser.toParserBuilder(); + + assertThat(newParserBuilder.getStandardMacros()) + .hasSize(CelStandardMacro.STANDARD_MACROS.size()); + assertThat(newParserBuilder.getMacros()).hasSize(1); + assertThat(newParserBuilder.getParserLibraries().build()).hasSize(1); + } } diff --git a/parser/src/test/java/dev/cel/parser/CelParserParameterizedTest.java b/parser/src/test/java/dev/cel/parser/CelParserParameterizedTest.java index 599a5478c..0e4490aa6 100644 --- a/parser/src/test/java/dev/cel/parser/CelParserParameterizedTest.java +++ b/parser/src/test/java/dev/cel/parser/CelParserParameterizedTest.java @@ -31,7 +31,9 @@ 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; @@ -68,7 +70,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 +125,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"); @@ -190,6 +206,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 @@ -238,6 +277,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 +286,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 +349,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 498c14f0e..c06be2688 100644 --- a/parser/src/test/java/dev/cel/parser/CelStandardMacroTest.java +++ b/parser/src/test/java/dev/cel/parser/CelStandardMacroTest.java @@ -16,6 +16,7 @@ import static com.google.common.truth.Truth.assertThat; +import com.google.common.testing.EqualsTester; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -34,4 +35,109 @@ public void getFunction() { assertThat(CelStandardMacro.MAP.getFunction()).isEqualTo(Operator.MAP.getFunction()); assertThat(CelStandardMacro.MAP_FILTER.getFunction()).isEqualTo(Operator.MAP.getFunction()); } + + @Test + public void testHas() { + assertThat(CelStandardMacro.HAS.getFunction()).isEqualTo(Operator.HAS.getFunction()); + assertThat(CelStandardMacro.HAS.getDefinition().getArgumentCount()).isEqualTo(1); + assertThat(CelStandardMacro.HAS.getDefinition().isReceiverStyle()).isFalse(); + assertThat(CelStandardMacro.HAS.getDefinition().getKey()).isEqualTo("has:1:false"); + assertThat(CelStandardMacro.HAS.getDefinition().isVariadic()).isFalse(); + assertThat(CelStandardMacro.HAS.getDefinition().toString()) + .isEqualTo(CelStandardMacro.HAS.getDefinition().getKey()); + assertThat(CelStandardMacro.HAS.getDefinition().hashCode()) + .isEqualTo(CelStandardMacro.HAS.getDefinition().getKey().hashCode()); + } + + @Test + public void testAll() { + assertThat(CelStandardMacro.ALL.getFunction()).isEqualTo(Operator.ALL.getFunction()); + assertThat(CelStandardMacro.ALL.getDefinition().getArgumentCount()).isEqualTo(2); + assertThat(CelStandardMacro.ALL.getDefinition().isReceiverStyle()).isTrue(); + assertThat(CelStandardMacro.ALL.getDefinition().getKey()).isEqualTo("all:2:true"); + assertThat(CelStandardMacro.ALL.getDefinition().isVariadic()).isFalse(); + assertThat(CelStandardMacro.ALL.getDefinition().toString()) + .isEqualTo(CelStandardMacro.ALL.getDefinition().getKey()); + assertThat(CelStandardMacro.ALL.getDefinition().hashCode()) + .isEqualTo(CelStandardMacro.ALL.getDefinition().getKey().hashCode()); + } + + @Test + public void testExists() { + assertThat(CelStandardMacro.EXISTS.getFunction()).isEqualTo(Operator.EXISTS.getFunction()); + assertThat(CelStandardMacro.EXISTS.getDefinition().getArgumentCount()).isEqualTo(2); + assertThat(CelStandardMacro.EXISTS.getDefinition().isReceiverStyle()).isTrue(); + assertThat(CelStandardMacro.EXISTS.getDefinition().getKey()).isEqualTo("exists:2:true"); + assertThat(CelStandardMacro.EXISTS.getDefinition().isVariadic()).isFalse(); + assertThat(CelStandardMacro.EXISTS.getDefinition().toString()) + .isEqualTo(CelStandardMacro.EXISTS.getDefinition().getKey()); + assertThat(CelStandardMacro.EXISTS.getDefinition().hashCode()) + .isEqualTo(CelStandardMacro.EXISTS.getDefinition().getKey().hashCode()); + } + + @Test + public void testExistsOne() { + assertThat(CelStandardMacro.EXISTS_ONE.getFunction()) + .isEqualTo(Operator.EXISTS_ONE.getFunction()); + assertThat(CelStandardMacro.EXISTS_ONE.getDefinition().getArgumentCount()).isEqualTo(2); + assertThat(CelStandardMacro.EXISTS_ONE.getDefinition().isReceiverStyle()).isTrue(); + assertThat(CelStandardMacro.EXISTS_ONE.getDefinition().getKey()).isEqualTo("exists_one:2:true"); + assertThat(CelStandardMacro.EXISTS_ONE.getDefinition().isVariadic()).isFalse(); + assertThat(CelStandardMacro.EXISTS_ONE.getDefinition().toString()) + .isEqualTo(CelStandardMacro.EXISTS_ONE.getDefinition().getKey()); + assertThat(CelStandardMacro.EXISTS_ONE.getDefinition().hashCode()) + .isEqualTo(CelStandardMacro.EXISTS_ONE.getDefinition().getKey().hashCode()); + } + + @Test + public void testMap2() { + assertThat(CelStandardMacro.MAP.getFunction()).isEqualTo(Operator.MAP.getFunction()); + assertThat(CelStandardMacro.MAP.getDefinition().getArgumentCount()).isEqualTo(2); + assertThat(CelStandardMacro.MAP.getDefinition().isReceiverStyle()).isTrue(); + assertThat(CelStandardMacro.MAP.getDefinition().getKey()).isEqualTo("map:2:true"); + assertThat(CelStandardMacro.MAP.getDefinition().isVariadic()).isFalse(); + assertThat(CelStandardMacro.MAP.getDefinition().toString()) + .isEqualTo(CelStandardMacro.MAP.getDefinition().getKey()); + assertThat(CelStandardMacro.MAP.getDefinition().hashCode()) + .isEqualTo(CelStandardMacro.MAP.getDefinition().getKey().hashCode()); + } + + @Test + public void testMap3() { + assertThat(CelStandardMacro.MAP_FILTER.getFunction()).isEqualTo(Operator.MAP.getFunction()); + assertThat(CelStandardMacro.MAP_FILTER.getDefinition().getArgumentCount()).isEqualTo(3); + assertThat(CelStandardMacro.MAP_FILTER.getDefinition().isReceiverStyle()).isTrue(); + assertThat(CelStandardMacro.MAP_FILTER.getDefinition().getKey()).isEqualTo("map:3:true"); + assertThat(CelStandardMacro.MAP_FILTER.getDefinition().isVariadic()).isFalse(); + assertThat(CelStandardMacro.MAP_FILTER.getDefinition().toString()) + .isEqualTo(CelStandardMacro.MAP_FILTER.getDefinition().getKey()); + assertThat(CelStandardMacro.MAP_FILTER.getDefinition().hashCode()) + .isEqualTo(CelStandardMacro.MAP_FILTER.getDefinition().getKey().hashCode()); + } + + @Test + public void testFilter() { + assertThat(CelStandardMacro.FILTER.getFunction()).isEqualTo(Operator.FILTER.getFunction()); + assertThat(CelStandardMacro.FILTER.getDefinition().getArgumentCount()).isEqualTo(2); + assertThat(CelStandardMacro.FILTER.getDefinition().isReceiverStyle()).isTrue(); + assertThat(CelStandardMacro.FILTER.getDefinition().getKey()).isEqualTo("filter:2:true"); + assertThat(CelStandardMacro.FILTER.getDefinition().isVariadic()).isFalse(); + assertThat(CelStandardMacro.FILTER.getDefinition().toString()) + .isEqualTo(CelStandardMacro.FILTER.getDefinition().getKey()); + assertThat(CelStandardMacro.FILTER.getDefinition().hashCode()) + .isEqualTo(CelStandardMacro.FILTER.getDefinition().getKey().hashCode()); + } + + @Test + public void testEquals() { + new EqualsTester() + .addEqualityGroup(CelStandardMacro.HAS) + .addEqualityGroup(CelStandardMacro.ALL) + .addEqualityGroup(CelStandardMacro.EXISTS) + .addEqualityGroup(CelStandardMacro.EXISTS_ONE) + .addEqualityGroup(CelStandardMacro.MAP) + .addEqualityGroup(CelStandardMacro.MAP_FILTER) + .addEqualityGroup(CelStandardMacro.FILTER) + .testEquals(); + } } diff --git a/parser/src/test/java/dev/cel/parser/CelUnparserImplTest.java b/parser/src/test/java/dev/cel/parser/CelUnparserImplTest.java index d17cdf9a0..0f2c00022 100644 --- a/parser/src/test/java/dev/cel/parser/CelUnparserImplTest.java +++ b/parser/src/test/java/dev/cel/parser/CelUnparserImplTest.java @@ -18,16 +18,16 @@ import static org.junit.Assert.assertThrows; import com.google.testing.junit.testparameterinjector.TestParameter; -import com.google.testing.junit.testparameterinjector.TestParameter.TestParameterValuesProvider; import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import com.google.testing.junit.testparameterinjector.TestParameterValuesProvider; 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,16 +39,20 @@ 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) - .addMacros(CelMacro.STANDARD_MACROS) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) .build(); private final CelUnparserImpl unparser = new CelUnparserImpl(); - private static final class ValidExprDataProvider implements TestParameterValuesProvider { + private static final class ValidExprDataProvider extends TestParameterValuesProvider { @Override - public List provideValues() { + public List provideValues(Context context) { return Arrays.asList( "a + b - c", "a && b && c && d && e", @@ -99,6 +103,15 @@ public List provideValues() { "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 +153,7 @@ public List provideValues() { // 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 @@ -182,9 +196,9 @@ public void unparse_succeeds( .isEqualTo(CelProtoAbstractSyntaxTree.fromCelAst(astOne).toParsedExpr()); } - private static final class InvalidExprDataProvider implements TestParameterValuesProvider { + private static final class InvalidExprDataProvider extends TestParameterValuesProvider { @Override - public List provideValues() { + public List provideValues(Context context) { return Arrays.asList( CelExpr.newBuilder().build(), // empty expr CelExpr.newBuilder() @@ -196,11 +210,11 @@ public List provideValues() { .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 +222,10 @@ public List provideValues() { .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()) @@ -249,7 +263,7 @@ public void unparse_comprehensionWithoutMacroCallTracking_presenceTestSucceeds() CelParser parser = CelParserImpl.newBuilder() .setOptions(CelOptions.newBuilder().populateMacroCalls(false).build()) - .addMacros(CelMacro.STANDARD_MACROS) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) .build(); CelAbstractSyntaxTree ast = parser.parse("has(hello.world)").getAst(); @@ -261,7 +275,7 @@ public void unparse_comprehensionWithoutMacroCallTracking_throwsException() thro CelParser parser = CelParserImpl.newBuilder() .setOptions(CelOptions.newBuilder().populateMacroCalls(false).build()) - .addMacros(CelMacro.STANDARD_MACROS) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) .build(); CelAbstractSyntaxTree ast = parser.parse("[1, 2, 3].all(x, x > 0)").getAst(); @@ -273,4 +287,18 @@ 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\"))"); + } } diff --git a/parser/src/test/java/dev/cel/parser/OperatorTest.java b/parser/src/test/java/dev/cel/parser/OperatorTest.java index 93346e631..9bed3c466 100644 --- a/parser/src/test/java/dev/cel/parser/OperatorTest.java +++ b/parser/src/test/java/dev/cel/parser/OperatorTest.java @@ -14,11 +14,12 @@ package dev.cel.parser; -import static com.google.common.truth.Truth8.assertThat; +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; @@ -50,6 +51,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(); diff --git a/parser/src/test/resources/parser.baseline b/parser/src/test/resources/parser.baseline index 79dd0ae65..9b509f61e 100644 --- a/parser/src/test/resources/parser.baseline +++ b/parser/src/test/resources/parser.baseline @@ -735,50 +735,50 @@ 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# @@ -792,40 +792,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 +839,7 @@ P: __comprehension__( // Target m^#1:Expr.Ident#, // Accumulator - __result__, + @result, // Init []^#6:Expr.CreateList#, // LoopCondition @@ -848,22 +848,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 +872,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 +895,7 @@ P: __comprehension__( // Target m^#1:Expr.Ident#, // Accumulator - __result__, + @result, // Init []^#5:Expr.CreateList#, // LoopCondition @@ -904,22 +904,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 +928,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 +1205,7 @@ P: __comprehension__( // Target x^#1:Expr.Ident#, // Accumulator - __result__, + @result, // Init []^#19:Expr.CreateList#, // LoopCondition @@ -1218,7 +1218,7 @@ P: __comprehension__( // Target y^#4:Expr.Ident#, // Accumulator - __result__, + @result, // Init []^#10:Expr.CreateList#, // LoopCondition @@ -1230,32 +1230,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 +1268,7 @@ L: __comprehension__( // Target y^#4[1,12]#, // Accumulator - __result__, + @result, // Init []^#10[1,20]#, // LoopCondition @@ -1280,25 +1280,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 +1319,7 @@ P: __comprehension__( // Target a^#2:Expr.Ident#.b~test-only~^#4:Expr.Select#, // Accumulator - __result__, + @result, // Init []^#8:Expr.CreateList#, // LoopCondition @@ -1328,22 +1328,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 +1352,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 +1377,7 @@ P: __comprehension__( // Target x^#1:Expr.Ident#, // Accumulator - __result__, + @result, // Init []^#35:Expr.CreateList#, // LoopCondition @@ -1391,62 +1391,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 +1460,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 +1543,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 +1623,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..9f4b96825 100644 --- a/parser/src/test/resources/parser_errors.baseline +++ b/parser/src/test/resources/parser_errors.baseline @@ -255,13 +255,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 +297,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(.`.` + | ........^ 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..5979f1ba7 --- /dev/null +++ b/policy/BUILD.bazel @@ -0,0 +1,61 @@ +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"], +) 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..ee35e8271 --- /dev/null +++ b/policy/src/main/java/dev/cel/policy/BUILD.bazel @@ -0,0 +1,262 @@ +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: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", + "@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: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"], + visibility = ["//visibility:private"], + deps = [ + ":compiled_rule", + "//:auto_value", + "//bundle:cel", + "//common:cel_ast", + "//common:compiler_common", + "//common:mutable_ast", + "//common/ast", + "//common/formats:value_string", + "//common/navigation:mutable_navigation", + "//extensions:optional_library", + "//optimizer:ast_optimizer", + "//optimizer:mutable_ast", + "//parser:operator", + "@maven//:com_google_errorprone_error_prone_annotations", + "@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..36f1685fc --- /dev/null +++ b/policy/src/main/java/dev/cel/policy/CelCompiledRule.java @@ -0,0 +1,160 @@ +// 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()) { + return true; + } + + if (match.isConditionTriviallyTrue()) { + return false; + } + + 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..00c23e7e9 --- /dev/null +++ b/policy/src/main/java/dev/cel/policy/CelPolicy.java @@ -0,0 +1,324 @@ +// 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.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()); + } + + /** 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); + + // This should stay package-private to encourage add/set methods to be used instead. + abstract ImmutableMap.Builder metadataBuilder(); + + public abstract Builder setMetadata(ImmutableMap value); + + private final ArrayList importList = new ArrayList<>(); + + abstract Builder setImports(ImmutableList value); + + public List imports() { + return Collections.unmodifiableList(importList); + } + + @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) { + metadataBuilder().put(key, value); + return this; + } + + @CanIgnoreReturnValue + public Builder putMetadata(Map map) { + metadataBuilder().putAll(map); + return this; + } + + abstract CelPolicy autoBuild(); + + public CelPolicy build() { + setImports(ImmutableList.copyOf(importList)); + 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(); + + 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..592a0120d --- /dev/null +++ b/policy/src/main/java/dev/cel/policy/CelPolicyCompilerBuilder.java @@ -0,0 +1,43 @@ +// 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; + +/** 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); + + @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..f6f893c1c --- /dev/null +++ b/policy/src/main/java/dev/cel/policy/CelPolicyCompilerImpl.java @@ -0,0 +1,397 @@ +// 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.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 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( + ConstantFoldingOptimizer.getInstance(), + SubexpressionOptimizer.newInstance( + SubexpressionOptimizerOptions.newBuilder() + // "record" is used for recording subexpression results via + // BlueprintLateFunctionBinding. Safely eliminable, since repeated + // invocation does not change the intermediate results. + .addEliminableFunctions("record") + .populateMacroCalls(true) + .enableCelBlock(true) + .build())) + .build(); + 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) { + boolean ruleHasOptional = compiledRule.hasOptionalOutput(); + ImmutableList compiledMatches = compiledRule.matches(); + int matchCount = compiledMatches.size(); + for (int i = matchCount - 1; i >= 0; i--) { + CelCompiledMatch compiledMatch = compiledMatches.get(i); + boolean isTriviallyTrue = compiledMatch.isConditionTriviallyTrue(); + + if (isTriviallyTrue && !ruleHasOptional && i != matchCount - 1) { + if (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 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 CelPolicyCompilerBuilder setAstDepthLimit(int astDepthLimit) { + if (astDepthLimit < 0) { + astDepthLimitValidator = Optional.empty(); + } else { + astDepthLimitValidator = Optional.of(AstDepthLimitValidator.newInstance(astDepthLimit)); + } + return this; + } + + @Override + public CelPolicyCompiler build() { + return new CelPolicyCompilerImpl( + cel, this.variablesPrefix, this.iterationLimit, astDepthLimitValidator); + } + } + + static Builder newBuilder(Cel cel) { + return new Builder(cel) + .setVariablesPrefix(DEFAULT_VARIABLE_PREFIX) + .setIterationLimit(DEFAULT_ITERATION_LIMIT); + } + + private CelPolicyCompilerImpl( + Cel cel, + String variablesPrefix, + int iterationLimit, + Optional astDepthValidator) { + this.cel = checkNotNull(cel); + this.variablesPrefix = checkNotNull(variablesPrefix); + this.iterationLimit = iterationLimit; + 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..ba34a1a60 --- /dev/null +++ b/policy/src/main/java/dev/cel/policy/CelPolicyParserBuilder.java @@ -0,0 +1,35 @@ +// 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); + + /** 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..b7255c93b --- /dev/null +++ b/policy/src/main/java/dev/cel/policy/CelPolicyYamlParser.java @@ -0,0 +1,440 @@ +// 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; + + @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, policySource, description); + return parser.parseYaml(); + } + + private static class ParserImpl implements PolicyParserContext { + + private final TagVisitor tagVisitor; + 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.newValueString(valueNode)); + break; + case "description": + policyBuilder.setDescription(ctx.newValueString(valueNode)); + break; + case "display_name": + policyBuilder.setDisplayName(ctx.newValueString(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.newValueString(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.newValueString(value)); + break; + case "description": + ruleBuilder.setDescription(ctx.newValueString(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.newValueString(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.newValueString(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.newValueString(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(); + + 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.newValueString(valueNode)); + break; + case "expression": + builder.setExpression(ctx.newValueString(valueNode)); + break; + case "description": + builder.setDescription(ctx.newValueString(valueNode)); + break; + case "display_name": + builder.setDisplayName(ctx.newValueString(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, String source, String description) { + this.tagVisitor = tagVisitor; + 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 newValueString(Node node) { + return ctx.newValueString(node); + } + } + + static final class Builder implements CelPolicyParserBuilder { + + private TagVisitor tagVisitor; + + private Builder() { + this.tagVisitor = new TagVisitor() {}; + } + + @Override + public CelPolicyParserBuilder addTagVisitor(TagVisitor tagVisitor) { + this.tagVisitor = tagVisitor; + return this; + } + + @Override + public CelPolicyParser build() { + return new CelPolicyYamlParser(tagVisitor); + } + } + + static Builder newBuilder() { + return new Builder(); + } + + private CelPolicyYamlParser(TagVisitor tagVisitor) { + this.tagVisitor = checkNotNull(tagVisitor); + } +} 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..e3b9e7057 --- /dev/null +++ b/policy/src/main/java/dev/cel/policy/RuleComposer.java @@ -0,0 +1,228 @@ +// 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.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import dev.cel.bundle.Cel; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelMutableAst; +import dev.cel.common.CelValidationException; +import dev.cel.common.ast.CelExpr.ExprKind.Kind; +import dev.cel.common.formats.ValueString; +import dev.cel.common.navigation.CelNavigableMutableAst; +import dev.cel.common.navigation.CelNavigableMutableExpr; +import dev.cel.extensions.CelOptionalLibrary.Function; +import dev.cel.optimizer.AstMutator; +import dev.cel.optimizer.CelAstOptimizer; +import dev.cel.parser.Operator; +import dev.cel.policy.CelCompiledRule.CelCompiledMatch; +import dev.cel.policy.CelCompiledRule.CelCompiledMatch.OutputValue; +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) { + RuleOptimizationResult result = optimizeRule(cel, compiledRule); + return OptimizationResult.create(result.ast().toParsedAst()); + } + + @AutoValue + abstract static class RuleOptimizationResult { + abstract CelMutableAst ast(); + + abstract boolean isOptionalResult(); + + static RuleOptimizationResult create(CelMutableAst ast, boolean isOptionalResult) { + return new AutoValue_RuleComposer_RuleOptimizationResult(ast, isOptionalResult); + } + } + + private RuleOptimizationResult optimizeRule(Cel cel, CelCompiledRule compiledRule) { + cel = + cel.toCelBuilder() + .addVarDeclarations( + compiledRule.variables().stream() + .map(CelCompiledVariable::celVarDecl) + .collect(toImmutableList())) + .build(); + + CelMutableAst matchAst = astMutator.newGlobalCall(Function.OPTIONAL_NONE.getFunction()); + boolean isOptionalResult = true; + // Keep track of the last output ID that might cause type-check failure while attempting to + // compose the subgraphs. + long lastOutputId = 0; + for (CelCompiledMatch match : Lists.reverse(compiledRule.matches())) { + CelAbstractSyntaxTree conditionAst = match.condition(); + // If the condition is trivially true, none of the matches in the rule causes the result + // to become optional, and the rule is not the last match, then this will introduce + // unreachable outputs or rules. + boolean isTriviallyTrue = match.isConditionTriviallyTrue(); + + switch (match.result().kind()) { + // For the match's output, determine whether the output should be wrapped + // into an optional value, a conditional, or both. + case OUTPUT: + OutputValue matchOutput = match.result().output(); + CelMutableAst outAst = CelMutableAst.fromCelAst(matchOutput.ast()); + if (isTriviallyTrue) { + matchAst = outAst; + isOptionalResult = false; + lastOutputId = matchOutput.sourceId(); + continue; + } + if (isOptionalResult) { + outAst = astMutator.newGlobalCall(Function.OPTIONAL_OF.getFunction(), outAst); + } + + matchAst = + astMutator.newGlobalCall( + Operator.CONDITIONAL.getFunction(), + CelMutableAst.fromCelAst(conditionAst), + outAst, + matchAst); + assertComposedAstIsValid( + cel, + matchAst, + "conflicting output types found.", + matchOutput.sourceId(), + lastOutputId); + lastOutputId = matchOutput.sourceId(); + continue; + case RULE: + // If the match has a nested rule, then compute the rule and whether it has + // an optional return value. + CelCompiledRule matchNestedRule = match.result().rule(); + RuleOptimizationResult nestedRule = optimizeRule(cel, matchNestedRule); + boolean nestedHasOptional = matchNestedRule.hasOptionalOutput(); + CelMutableAst nestedRuleAst = nestedRule.ast(); + if (isOptionalResult && !nestedHasOptional) { + nestedRuleAst = + astMutator.newGlobalCall(Function.OPTIONAL_OF.getFunction(), nestedRuleAst); + } + if (!isOptionalResult && nestedHasOptional) { + matchAst = astMutator.newGlobalCall(Function.OPTIONAL_OF.getFunction(), matchAst); + isOptionalResult = true; + } + // If either the nested rule or current condition output are optional then + // use optional.or() to specify the combination of the first and second results + // Note, the argument order is reversed due to the traversal of matches in + // reverse order. + if (isOptionalResult && isTriviallyTrue) { + matchAst = astMutator.newMemberCall(nestedRuleAst, Function.OR.getFunction(), matchAst); + } else { + matchAst = + astMutator.newGlobalCall( + Operator.CONDITIONAL.getFunction(), + CelMutableAst.fromCelAst(conditionAst), + nestedRuleAst, + matchAst); + } + + assertComposedAstIsValid( + cel, + matchAst, + String.format( + "failed composing the subrule '%s' due to conflicting output types.", + matchNestedRule.ruleId().map(ValueString::value).orElse("")), + lastOutputId); + break; + } + } + + CelMutableAst result = inlineCompiledVariables(matchAst, compiledRule.variables()); + + result = astMutator.renumberIdsConsecutively(result); + + return RuleOptimizationResult.create(result, isOptionalResult); + } + + 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; + } + + static RuleComposer newInstance( + CelCompiledRule compiledRule, String variablePrefix, int iterationLimit) { + return new RuleComposer(compiledRule, variablePrefix, iterationLimit); + } + + private void assertComposedAstIsValid( + Cel cel, CelMutableAst composedAst, String failureMessage, Long... ids) { + assertComposedAstIsValid(cel, composedAst, failureMessage, Arrays.asList(ids)); + } + + private void assertComposedAstIsValid( + Cel cel, CelMutableAst composedAst, String failureMessage, List ids) { + try { + cel.check(composedAst.toParsedAst()).getAst(); + } catch (CelValidationException e) { + ids = ids.stream().filter(id -> id > 0).collect(toCollection(ArrayList::new)); + throw new RuleCompositionException(failureMessage, e, ids); + } + } + + private RuleComposer(CelCompiledRule compiledRule, String variablePrefix, int iterationLimit) { + this.compiledRule = checkNotNull(compiledRule); + this.variablePrefix = variablePrefix; + this.astMutator = AstMutator.newInstance(iterationLimit); + } + + 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; + } + } +} 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..9106caf70 --- /dev/null +++ b/policy/src/test/java/dev/cel/policy/BUILD.bazel @@ -0,0 +1,55 @@ +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"]), + 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:compiler_factory", + "//policy:parser", + "//policy:parser_factory", + "//policy:policy_parser_context", + "//policy:source", + "//policy:validation_exception", + "//runtime", + "//runtime:function_binding", + "//runtime:late_function_binding", + "//testing/protos:single_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", + "@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..fa0da8a9a --- /dev/null +++ b/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.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.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 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.TestParameterValue; +import com.google.testing.junit.testparameterinjector.TestParameterValuesProvider; +import dev.cel.bundle.Cel; +import dev.cel.bundle.CelEnvironment; +import dev.cel.bundle.CelEnvironmentYamlParser; +import dev.cel.bundle.CelFactory; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelOptions; +import dev.cel.common.types.OptionalType; +import dev.cel.common.types.SimpleType; +import dev.cel.expr.conformance.proto3.TestAllTypes; +import dev.cel.extensions.CelOptionalLibrary; +import dev.cel.parser.CelStandardMacro; +import dev.cel.parser.CelUnparserFactory; +import dev.cel.policy.PolicyTestHelper.K8sTagHandler; +import dev.cel.policy.PolicyTestHelper.PolicyTestSuite; +import dev.cel.policy.PolicyTestHelper.PolicyTestSuite.PolicyTestSection; +import dev.cel.policy.PolicyTestHelper.PolicyTestSuite.PolicyTestSection.PolicyTestCase; +import dev.cel.policy.PolicyTestHelper.PolicyTestSuite.PolicyTestSection.PolicyTestCase.PolicyTestInput; +import dev.cel.policy.PolicyTestHelper.TestYamlPolicy; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.CelLateFunctionBindings; +import dev.cel.testing.testdata.SingleFileProto.SingleFile; +import dev.cel.testing.testdata.proto3.StandaloneGlobalEnum; +import java.io.IOException; +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).build(); + + @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_containsCompilationError_throws( + @TestParameter TestErrorYamlPolicy testCase) throws Exception { + // Read config and produce an environment to compile policies + String configSource = testCase.readConfigYamlContent(); + CelEnvironment celEnvironment = ENVIRONMENT_PARSER.parse(configSource); + Cel cel = celEnvironment.extend(newCel(), CEL_OPTIONS); + // 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); + CelAbstractSyntaxTree expectedOutputAst = cel.compile(testData.testCase.getOutput()).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> + assertThat(evalResult).hasValue(Optional.of(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); + Cel cel = celEnvironment.extend(newCel(), CelOptions.DEFAULT); + 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); + } + + 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 static Cel newCel() { + return CelFactory.standardCelBuilder() + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addCompilerLibraries(CelOptionalLibrary.INSTANCE) + .addRuntimeLibraries(CelOptionalLibrary.INSTANCE) + .addFileTypes(StandaloneGlobalEnum.getDescriptor().getFile()) + .addMessageTypes(TestAllTypes.getDescriptor(), SingleFile.getDescriptor()) + .setOptions(CEL_OPTIONS) + .addFunctionBindings( + CelFunctionBinding.from( + "locationCode_string", + String.class, + (ip) -> { + switch (ip) { + case "10.0.0.1": + return "us"; + case "10.0.0.2": + return "de"; + default: + return "ir"; + } + })) + .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 { + COMPILE_ERRORS("compile_errors"), + COMPOSE_ERRORS_CONFLICTING_OUTPUT("compose_errors_conflicting_output"), + COMPOSE_ERRORS_CONFLICTING_SUBRULE("compose_errors_conflicting_subrule"), + ERRORS_UNREACHABLE("errors_unreachable"); + + private final String name; + private final String policyFilePath; + + private String getPolicyFilePath() { + return policyFilePath; + } + + private String readPolicyYamlContent() throws IOException { + return readFromYaml(String.format("policy/%s/policy.yaml", name)); + } + + private String readConfigYamlContent() throws IOException { + return readFromYaml(String.format("policy/%s/config.yaml", name)); + } + + private String readExpectedErrorsBaseline() throws IOException { + return readFromYaml(String.format("policy/%s/expected_errors.baseline", name)); + } + + 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..803d99202 --- /dev/null +++ b/policy/src/test/java/dev/cel/policy/CelPolicyYamlParserTest.java @@ -0,0 +1,375 @@ +// 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.K8sTagHandler; +import dev.cel.policy.PolicyTestHelper.TestYamlPolicy; +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_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_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..8d9e0084b --- /dev/null +++ b/policy/src/test/java/dev/cel/policy/PolicyTestHelper.java @@ -0,0 +1,362 @@ +// 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.Resources; +import dev.cel.common.formats.ValueString; +import dev.cel.policy.CelPolicy.Match; +import dev.cel.policy.CelPolicy.Match.Result; +import dev.cel.policy.CelPolicy.Rule; +import dev.cel.policy.CelPolicyParser.TagVisitor; +import java.io.IOException; +import java.net.URL; +import java.util.List; +import java.util.Map; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.Constructor; +import org.yaml.snakeyaml.nodes.Node; +import org.yaml.snakeyaml.nodes.SequenceNode; + +/** Package-private class to assist with policy testing. */ +final class PolicyTestHelper { + + enum TestYamlPolicy { + NESTED_RULE( + "nested_rule", + true, + "cel.@block([resource.origin, @index0 in [\"us\", \"uk\", \"es\"], {\"banned\": true}]," + + " ((@index0 in {\"us\": false, \"ru\": false, \"ir\": false} && !@index1) ?" + + " optional.of(@index2) : optional.none()).or(optional.of(@index1 ? {\"banned\":" + + " false} : @index2)))"), + 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()))"), + REQUIRED_LABELS( + "required_labels", + true, + "cel.@block([spec.labels.filter(@it:0:0, !(@it:0:0 in resource.labels)), spec.labels," + + " resource.labels, @index2.filter(@it:0:0, @it:0:0 in @index1 && @index1[@it:0:0] !=" + + " @index2[@it:0:0])], (@index0.size() > 0) ? optional.of(\"missing one or more" + + " required labels: [\"\" + @index0.join(\",\") + \"\"]\") : ((@index3.size() > 0) ?" + + " optional.of(\"invalid values provided on one or more labels: [\"\" +" + + " @index3.join(\",\") + \"\"]\") : optional.none()))"), + 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 ||" + + " dev.cel.testing.testdata.proto3.StandaloneGlobalEnum.SGAR ==" + + " dev.cel.testing.testdata.proto3.StandaloneGlobalEnum.SGOO) ? optional.of(\"invalid" + + " spec, neither nested nor imported enums may refer to BAR\") : optional.none()))"), + 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("policy/%s/policy.yaml", name)); + } + + String readConfigYamlContent() throws IOException { + return readFromYaml(String.format("policy/%s/config.yaml", name)); + } + + PolicyTestSuite readTestYamlContent() throws IOException { + Yaml yaml = new Yaml(new Constructor(PolicyTestSuite.class, new LoaderOptions())); + String testContent = readFile(String.format("policy/%s/tests.yaml", name)); + + 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 description; + private List section; + + 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 String output; + + public void setName(String name) { + this.name = name; + } + + public void setInput(Map input) { + this.input = input; + } + + public void setOutput(String output) { + this.output = output; + } + + public String getName() { + return name; + } + + public Map getInput() { + return input; + } + + public String 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 URL getResource(String path) { + return Resources.getResource(Ascii.toLowerCase(path)); + } + + private static String readFile(String path) throws IOException { + return Resources.toString(getResource(path), UTF_8); + } + + static class K8sTagHandler implements TagVisitor { + + @Override + public void visitPolicyTag( + PolicyParserContext ctx, + long id, + String tagName, + Node node, + CelPolicy.Builder policyBuilder) { + switch (tagName) { + case "kind": + policyBuilder.putMetadata("kind", ctx.newValueString(node)); + break; + case "metadata": + long metadataId = ctx.collectMetadata(node); + if (!node.getTag().getValue().equals("tag:yaml.org,2002:map")) { + ctx.reportError( + metadataId, + String.format( + "invalid 'metadata' type, expected map got: %s", node.getTag().getValue())); + } + break; + case "spec": + Rule rule = ctx.parseRule(ctx, policyBuilder, node); + policyBuilder.setRule(rule); + break; + default: + TagVisitor.super.visitPolicyTag(ctx, id, tagName, node, policyBuilder); + break; + } + } + + @Override + public void visitRuleTag( + PolicyParserContext ctx, + long id, + String tagName, + Node node, + CelPolicy.Builder policyBuilder, + Rule.Builder ruleBuilder) { + switch (tagName) { + case "failurePolicy": + policyBuilder.putMetadata(tagName, ctx.newValueString(node)); + break; + case "matchConstraints": + long matchConstraintsId = ctx.collectMetadata(node); + if (!node.getTag().getValue().equals("tag:yaml.org,2002:map")) { + ctx.reportError( + matchConstraintsId, + String.format( + "invalid 'matchConstraints' type, expected map got: %s", + node.getTag().getValue())); + } + break; + case "validations": + long validationId = ctx.collectMetadata(node); + if (!node.getTag().getValue().equals("tag:yaml.org,2002:seq")) { + ctx.reportError( + validationId, + String.format( + "invalid 'validations' type, expected list got: %s", node.getTag().getValue())); + } + + SequenceNode validationNodes = (SequenceNode) node; + for (Node element : validationNodes.getValue()) { + ruleBuilder.addMatches(ctx.parseMatch(ctx, policyBuilder, element)); + } + break; + default: + TagVisitor.super.visitRuleTag(ctx, id, tagName, node, policyBuilder, ruleBuilder); + break; + } + } + + @Override + public void visitMatchTag( + PolicyParserContext ctx, + long id, + String tagName, + Node node, + CelPolicy.Builder policyBuilder, + Match.Builder matchBuilder) { + switch (tagName) { + case "expression": + // The K8s expression to validate must return false in order to generate a violation + // message. + ValueString conditionValue = ctx.newValueString(node); + conditionValue = + conditionValue.toBuilder().setValue("!(" + conditionValue.value() + ")").build(); + matchBuilder.setCondition(conditionValue); + break; + case "messageExpression": + matchBuilder.setResult(Result.ofOutput(ctx.newValueString(node))); + break; + default: + TagVisitor.super.visitMatchTag(ctx, id, tagName, node, policyBuilder, matchBuilder); + break; + } + } + } + + private PolicyTestHelper() {} +} 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..666915526 --- /dev/null +++ b/protobuf/src/main/java/dev/cel/protobuf/BUILD.bazel @@ -0,0 +1,104 @@ +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_descriptors", + "//common/internal:proto_java_qualified_names", + "@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:proto_java_qualified_names", + "//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..276dd7f91 --- /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 dev.cel.common.CelDescriptorUtil; +import dev.cel.common.internal.ProtoJavaQualifiedNames; +import dev.cel.protobuf.JavaFileGenerator.GeneratedClass; +import dev.cel.protobuf.JavaFileGenerator.JavaFileGeneratorOption; +import java.io.File; +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 = ProtoJavaQualifiedNames.getJavaPackageName(targetFileDescriptor); + 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..be1e07cc2 --- /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-5 +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..0031fe6a6 --- /dev/null +++ b/protobuf/src/main/java/dev/cel/protobuf/ProtoDescriptorCollector.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.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 dev.cel.common.internal.ProtoJavaQualifiedNames; +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 = + ProtoJavaQualifiedNames.getFullyQualifiedJavaClassName(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..c26643291 --- /dev/null +++ b/protobuf/src/test/java/dev/cel/protobuf/CelLiteDescriptorTest.java @@ -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. + +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 testAllTypesMessageLiteDescriptor_fieldInfoMap_containsAllEntries() { + MessageLiteDescriptor testAllTypesDescriptor = + TEST_ALL_TYPES_CEL_LITE_DESCRIPTOR + .getProtoTypeNamesToDescriptors() + .get("cel.expr.conformance.proto3.TestAllTypes"); + + assertThat(testAllTypesDescriptor.getFieldDescriptors()).hasSize(243); + } + + @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 6f8cc71a0..9ca6eb770 100644 --- a/publish/BUILD.bazel +++ b/publish/BUILD.bazel @@ -1,5 +1,5 @@ -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 @@ -9,13 +9,24 @@ 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:runtime_helpers", "//runtime/src/main/java/dev/cel/runtime:unknown_attributes", ] +LITE_RUNTIME_TARGETS = [ + "//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", + "//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", +] + COMPILER_TARGETS = [ "//parser/src/main/java/dev/cel/parser", + "//parser/src/main/java/dev/cel/parser:parser_factory", "//parser/src/main/java/dev/cel/parser:parser_builder", + "//parser/src/main/java/dev/cel/parser:unparser", "//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", @@ -43,20 +54,54 @@ OPTIMIZER_TARGETS = [ "//optimizer/src/main/java/dev/cel/optimizer:mutable_ast", "//optimizer/src/main/java/dev/cel/optimizer:optimizer_impl", "//optimizer/src/main/java/dev/cel/optimizer/optimizers:constant_folding", + "//optimizer/src/main/java/dev/cel/optimizer/optimizers:common_subexpression_elimination", +] + +POLICY_COMPILER_TARGETS = [ + "//policy/src/main/java/dev/cel/policy:policy", + "//policy/src/main/java/dev/cel/policy:source", + "//policy/src/main/java/dev/cel/policy:validation_exception", + "//policy/src/main/java/dev/cel/policy:parser_factory", + "//policy/src/main/java/dev/cel/policy:yaml_parser", + "//policy/src/main/java/dev/cel/policy:parser", + "//policy/src/main/java/dev/cel/policy:parser_builder", + "//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:policy_parser_context", + "//policy/src/main/java/dev/cel/policy:compiled_rule", ] -V1ALPHA1_UTILITY_TARGETS = [ +V1ALPHA1_AST_TARGETS = [ "//common/src/main/java/dev/cel/common:proto_v1alpha1_ast", ] +CANONICAL_AST_TARGETS = [ + "//common/src/main/java/dev/cel/common:proto_ast", +] + EXTENSION_TARGETS = [ "//extensions/src/main/java/dev/cel/extensions", "//extensions/src/main/java/dev/cel/extensions:optional_library", ] -ALL_TARGETS = [ +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", +] + +ALL_TARGETS = BUNDLE_TARGETS + RUNTIME_TARGETS + COMPILER_TARGETS + EXTENSION_TARGETS + V1ALPHA1_AST_TARGETS + CANONICAL_AST_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_pom", @@ -72,6 +117,8 @@ pom_file( java_export( name = "cel", + deploy_env = EXCLUDED_TARGETS, + javadocopts = JAVA_DOC_OPTIONS, maven_coordinates = "dev.cel:cel:%s" % CEL_VERSION, pom_template = ":cel_pom", runtime_deps = ALL_TARGETS, @@ -91,6 +138,8 @@ pom_file( java_export( name = "cel_compiler", + 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, @@ -110,83 +159,72 @@ pom_file( java_export( name = "cel_runtime", + 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, ) 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", - 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 = V1ALPHA1_AST_TARGETS, template_file = "pom_template.xml", ) java_export( - name = "cel_validators", - 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", + runtime_deps = 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 = CANONICAL_AST_TARGETS, template_file = "pom_template.xml", ) java_export( - name = "cel_optimizers", - 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", + runtime_deps = 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", - 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", + runtime_deps = LITE_RUNTIME_TARGETS, ) diff --git a/publish/cel_version.bzl b/publish/cel_version.bzl index b918db871..935b2f7b1 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.11.0" diff --git a/publish/publish.sh b/publish/publish.sh index edb705f19..9dc88deda 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_v1alpha1.publish") +ALL_TARGETS=("//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=$( exprIds; + private final List attributes; + + List exprIds() { + return exprIds; + } + + List attributes() { + return attributes; + } + + @CanIgnoreReturnValue + 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<>()); + } + + static AccumulatedUnknowns create(Collection exprIds, Collection attributes) { + return new AccumulatedUnknowns(new ArrayList<>(exprIds), new ArrayList<>(attributes)); + } + + private static void enforceMaxAttributeSize( + List lhsAttributes, List rhsAttributes) { + int totalSize = lhsAttributes.size() + rhsAttributes.size(); + if (totalSize > MAX_UNKNOWN_ATTRIBUTE_SIZE) { + throw new IllegalArgumentException( + "Exceeded maximum allowed unknown attributes: " + totalSize); + } + } + + private AccumulatedUnknowns(List exprIds, List 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 d713b5113..7c0539d00 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,140 +9,812 @@ package( ], ) -BASE_SOURCES = [ - "DefaultMetadata.java", - "IncompleteData.java", - "InterpreterException.java", - "MessageProvider.java", - "Metadata.java", - "Registrar.java", - "StandardFunctions.java", - "StandardTypeResolver.java", - "TypeResolver.java", -] - -INTERPRETER_SOURCES = [ - "Activation.java", - "CallArgumentChecker.java", - "DefaultDispatcher.java", - "DefaultInterpreter.java", - "DescriptorMessageProvider.java", - "Dispatcher.java", - "DynamicMessageFactory.java", - "GlobalResolver.java", - "Interpretable.java", - "Interpreter.java", - "InterpreterUtil.java", - "MessageFactory.java", - "PartialMessage.java", - "PartialMessageOrBuilder.java", - "RuntimeTypeProvider.java", - "RuntimeUnknownResolver.java", - "UnknownTrackingInterpretable.java", -] - -java_library( - name = "base", - srcs = BASE_SOURCES, +# keep sorted +BASE_SOURCES = [ + "DefaultMetadata.java", + "MessageProvider.java", + "Registrar.java", +] + +# keep sorted +INTERPRETER_SOURCES = [ + "CallArgumentChecker.java", + "DefaultInterpreter.java", + "Interpreter.java", + "RuntimeUnknownResolver.java", + "UnknownTrackingInterpretable.java", +] + +# keep sorted +DESCRIPTOR_MESSAGE_PROVIDER_SOURCES = [ + "DescriptorMessageProvider.java", + "DynamicMessageFactory.java", + "MessageFactory.java", +] + +# keep sorted +LITE_RUNTIME_SOURCES = [ + "CelLiteRuntime.java", + "CelLiteRuntimeBuilder.java", + "CelLiteRuntimeLibrary.java", +] + +# keep sorted +LITE_RUNTIME_IMPL_SOURCES = [ + "LiteProgramImpl.java", + "LiteRuntimeImpl.java", +] + +# keep sorted +FUNCTION_BINDING_SOURCES = [ + "CelFunctionBinding.java", + "FunctionBindingImpl.java", +] + +# keep sorted +FUNCTION_OVERLOAD_IMPL_SOURCES = [ + "FunctionOverload.java", + "FunctionResolver.java", + "ResolvedOverload.java", +] + +# keep sorted +INTERPRABLE_SOURCES = [ + "GlobalResolver.java", + "Interpretable.java", +] + +# keep sorted +DISPATCHER_SOURCES = [ + "DefaultDispatcher.java", + "Dispatcher.java", +] + +java_library( + name = "runtime_type_provider", + srcs = ["RuntimeTypeProvider.java"], + tags = [ + ], + deps = [ + ":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", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +java_library( + name = "descriptor_message_provider", + srcs = DESCRIPTOR_MESSAGE_PROVIDER_SOURCES, + tags = [ + ], + deps = [ + ":runtime_type_provider", + "//common:cel_descriptors", + "//common:error_codes", + "//common:options", + "//common:runtime_exception", + "//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_byte_string", + "@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 = "dispatcher", + srcs = DISPATCHER_SOURCES, + tags = [ + ], + deps = [ + ":base", + ":evaluation_exception", + ":evaluation_exception_builder", + ":function_overload_impl", + "//:auto_value", + "//common:error_codes", + "//common/annotations", + "@maven//:com_google_code_findbugs_annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "dispatcher_android", + srcs = DISPATCHER_SOURCES, + visibility = ["//visibility:private"], + deps = [ + ":base_android", + ":evaluation_exception", + ":evaluation_exception_builder", + ":function_overload_impl_android", + "//:auto_value", + "//common:error_codes", + "//common/annotations", + "@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", + ], +) + +cel_android_library( + name = "activation_android", + srcs = ["Activation.java"], + visibility = ["//visibility:private"], + 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", + ], +) + +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 = "type_resolver", + srcs = ["TypeResolver.java"], + tags = [ + ], + deps = [ + "//common/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", + ], +) + +cel_android_library( + name = "type_resolver_android", + srcs = ["TypeResolver.java"], + visibility = ["//visibility:private"], + deps = [ + "//common/types:type_providers_android", + "//common/types:types_android", + "//common/values:cel_byte_string", + "@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 = "descriptor_type_resolver", + srcs = ["DescriptorTypeResolver.java"], + visibility = ["//visibility:private"], + deps = [ + ":type_resolver", + "//common/types", + "//common/types:type_providers", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + ], +) + +java_library( + name = "base", + srcs = BASE_SOURCES, + tags = [ + ], + deps = [ + ":function_overload_impl", + ":metadata", + "//common:cel_ast", + "//common/annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "base_android", + srcs = BASE_SOURCES, + visibility = ["//visibility:private"], + deps = [ + ":function_overload_impl_android", + ":metadata", + "//common:cel_ast_android", + "//common/annotations", + "@maven//:com_google_code_findbugs_annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", + ], +) + +java_library( + name = "interpreter", + srcs = INTERPRETER_SOURCES, + tags = [ + ], + exports = [":base"], + deps = [ + ":accumulated_unknowns", + ":base", + ":concatenated_list_view", + ":dispatcher", + ":evaluation_exception", + ":evaluation_exception_builder", + ":evaluation_listener", + ":function_overload_impl", + ":interpretable", + ":interpreter_util", + ":metadata", + ":runtime_helpers", + ":runtime_type_provider", + ":type_resolver", + ":unknown_attributes", + "//:auto_value", + "//common:cel_ast", + "//common:error_codes", + "//common:options", + "//common:runtime_exception", + "//common/annotations", + "//common/ast", + "//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", + "@maven//:org_jspecify_jspecify", + ], +) + +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_overload_impl_android", + ":interpretable_android", + ":interpreter_util_android", + ":metadata", + ":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:runtime_exception", + "//common/annotations", + "//common/ast:ast_android", + "//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:error_codes", + "//common:options", + "//common:runtime_exception", + "//common/annotations", + "//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:error_codes", + "//common:options", + "//common:runtime_exception", + "//common/annotations", + "//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:error_codes", + "//common:options", + "//common:runtime_exception", + "//common/annotations", + "//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:error_codes", + "//common:options", + "//common:runtime_exception", + "//common/annotations", + "//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", + "CelRuntimeFactory.java", + "CelRuntimeLegacyImpl.java", + "CelRuntimeLibrary.java", + "CelVariableResolver.java", + "HierarchicalVariableResolver.java", + "ProgramImpl.java", + "UnknownContext.java", +] + +LATE_FUNCTION_BINDING_SOURCES = [ + "CelLateFunctionBindings.java", + "CelResolvedOverload.java", +] + +java_library( + name = "late_function_binding", + srcs = LATE_FUNCTION_BINDING_SOURCES, + tags = [ + ], + deps = [ + ":dispatcher", + ":evaluation_exception", + ":function_binding", + ":function_overload", + ":function_overload_impl", + ":function_resolver", + "//: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_overload_android", + ":function_overload_impl_android", + ":function_resolver_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:runtime_exception", + "//common/annotations", + "//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 + deps = [ + "//common/annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +java_library( + name = "interpretable", + srcs = INTERPRABLE_SOURCES, + deps = [ + ":evaluation_exception", + ":evaluation_listener", + ":function_overload_impl", + "//common/annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:org_jspecify_jspecify", + ], +) + +cel_android_library( + name = "interpretable_android", + srcs = INTERPRABLE_SOURCES, + visibility = ["//visibility:private"], + deps = [ + ":evaluation_exception", + ":evaluation_listener_android", + ":function_overload_impl_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: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:size", + "//runtime/standard:standard_function", + "//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 = [ - ":runtime_helper", - "//:auto_value", - "//common", - "//common:error_codes", + ":function_binding_android", + ":runtime_equality_android", "//common:options", - "//common:runtime_exception", "//common/annotations", - "//common/internal:comparison_functions", - "//common/internal:default_message_factory", - "//common/internal:dynamic_proto", - "//common/types", - "//common/types:type_providers", - "@cel_spec//proto/cel/expr:expr_java_proto", + "//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:size_android", + "//runtime/standard:standard_function_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//:com_google_protobuf_protobuf_java", - "@maven//:com_google_protobuf_protobuf_java_util", - "@maven//:com_google_re2j_re2j", - "@maven//:org_jspecify_jspecify", + "@maven_android//:com_google_guava_guava", + "@maven_android//:com_google_protobuf_protobuf_javalite", ], ) java_library( - name = "interpreter", - srcs = INTERPRETER_SOURCES, - deprecation = "Please use CEL-Java Fluent APIs //runtime:runtime instead", + name = "function_binding", + srcs = FUNCTION_BINDING_SOURCES, tags = [ ], - exports = [":base"], deps = [ - ":base", - ":evaluation_listener", - ":runtime_helper", - ":unknown_attributes", - "//:auto_value", - "//common", - "//common:error_codes", - "//common:features", - "//common:options", - "//common:runtime_exception", + ":function_overload", "//common/annotations", - "//common/ast", - "//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", "@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", + ], +) + +cel_android_library( + name = "function_binding_android", + srcs = FUNCTION_BINDING_SOURCES, + tags = [ + ], + deps = [ + ":function_overload_android", + "//common/annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", + ], +) + +java_library( + name = "function_resolver", + srcs = ["CelFunctionResolver.java"], + tags = [ + ], + deps = [ + ":function_overload_impl", + "@maven//:com_google_code_findbugs_annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +cel_android_library( + name = "function_resolver_android", + srcs = ["CelFunctionResolver.java"], + deps = [ + ":function_overload_impl_android", + "@maven//:com_google_code_findbugs_annotations", + "@maven//:com_google_errorprone_error_prone_annotations", ], ) java_library( - name = "runtime_helper", + name = "function_overload", srcs = [ - "RuntimeEquality.java", - "RuntimeHelpers.java", + "CelFunctionOverload.java", ], tags = [ ], - # NOTE: do not grow this dependencies arbitrarily deps = [ - "//common:error_codes", - "//common:options", - "//common:runtime_exception", + ":function_overload_impl", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +cel_android_library( + name = "function_overload_android", + srcs = [ + "CelFunctionOverload.java", + ], + deps = [ + ":function_overload_impl_android", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +java_library( + name = "function_overload_impl", + srcs = FUNCTION_OVERLOAD_IMPL_SOURCES, + tags = [ + ], + deps = [ + ":evaluation_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", -] +cel_android_library( + name = "function_overload_impl_android", + srcs = FUNCTION_OVERLOAD_IMPL_SOURCES, + deps = [ + ":evaluation_exception", + "//common/annotations", + "@maven//:com_google_code_findbugs_annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_protobuf_protobuf_javalite", + ], +) java_library( name = "runtime", @@ -147,12 +822,27 @@ java_library( tags = [ ], deps = [ + ":activation", + ":cel_value_runtime_type_provider", + ":descriptor_message_provider", + ":descriptor_type_resolver", + ":dispatcher", + ":evaluation_exception", ":evaluation_listener", - ":runtime_type_provider_legacy", + ":function_binding", + ":function_resolver", + ":interpretable", + ":interpreter", + ":lite_runtime", + ":proto_message_activation_factory", + ":proto_message_runtime_equality", + ":runtime_equality", + ":runtime_type_provider", + ":standard_functions", ":unknown_attributes", "//:auto_value", - "//common", - "//common:error_codes", + "//common:cel_ast", + "//common:cel_descriptors", "//common:options", "//common/annotations", "//common/internal:cel_descriptor_pools", @@ -162,7 +852,6 @@ java_library( "//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//:com_google_guava_guava", @@ -171,6 +860,114 @@ java_library( ], ) +java_library( + name = "lite_runtime", + srcs = LITE_RUNTIME_SOURCES, + tags = [ + ], + deps = [ + ":evaluation_exception", + ":function_binding", + ":function_resolver", + "//: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 = [ + ":activation", + ":cel_value_runtime_type_provider", + ":dispatcher", + ":evaluation_exception", + ":function_binding", + ":function_resolver", + ":interpretable", + ":interpreter", + ":lite_runtime", + ":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_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "lite_runtime_impl_android", + srcs = LITE_RUNTIME_IMPL_SOURCES, + tags = [ + ], + deps = [ + ":activation_android", + ":cel_value_runtime_type_provider_android", + ":dispatcher_android", + ":evaluation_exception", + ":function_binding_android", + ":function_resolver_android", + ":interpretable_android", + ":interpreter_android", + ":lite_runtime_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", @@ -191,12 +988,12 @@ java_library( ], deps = [ ":unknown_attributes", - "//common", + "//common:cel_ast", "//common:compiler_common", - "//parser", + "//common/ast", "//parser:operator", "//parser:parser_builder", - "@cel_spec//proto/cel/expr:expr_java_proto", + "//parser:parser_factory", "@maven//:com_google_guava_guava", ], ) @@ -214,26 +1011,58 @@ java_library( ], ) +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 = "runtime_type_provider_legacy", - srcs = ["RuntimeTypeProviderLegacyImpl.java"], + name = "cel_value_runtime_type_provider", + srcs = ["CelValueRuntimeTypeProvider.java"], deps = [ + ":runtime_type_provider", ":unknown_attributes", - "//common:options", + "//common:error_codes", + "//common:runtime_exception", "//common/annotations", - "//common/internal:cel_descriptor_pools", - "//common/internal:dynamic_proto", - "//common/types", - "//common/types:type_providers", "//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:proto_message_value", - "//runtime:interpreter", - "@cel_spec//proto/cel/expr:expr_java_proto", + "//common/values:combined_cel_value_provider", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", - "@maven//:org_jspecify_jspecify", + "@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:error_codes", + "//common:runtime_exception", + "//common/annotations", + "//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", ], ) @@ -243,10 +1072,24 @@ java_library( tags = [ ], deps = [ - ":base", - "//common:error_codes", + ":accumulated_unknowns", + ":evaluation_exception", + ":unknown_attributes", + "//common/annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + "@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", - "@cel_spec//proto/cel/expr:expr_java_proto", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:org_jspecify_jspecify", ], @@ -263,3 +1106,62 @@ java_library( "@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", + ":function_resolver_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 + visibility = ["//visibility:private"], +) + +java_library( + name = "accumulated_unknowns", + srcs = ["AccumulatedUnknowns.java"], + visibility = ["//visibility:private"], + deps = [ + ":unknown_attributes", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +cel_android_library( + name = "accumulated_unknowns_android", + srcs = ["AccumulatedUnknowns.java"], + visibility = ["//visibility:private"], + deps = [ + ":unknown_attributes_android", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) 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/CelAttributeParser.java b/runtime/src/main/java/dev/cel/runtime/CelAttributeParser.java index 4ad4722da..a14d8bfd5 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelAttributeParser.java +++ b/runtime/src/main/java/dev/cel/runtime/CelAttributeParser.java @@ -16,15 +16,15 @@ 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.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; @@ -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/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..6aaed4da7 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/CelEvaluationExceptionBuilder.java @@ -0,0 +1,97 @@ +// 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.CelRuntimeException; +import dev.cel.common.annotations.Internal; +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) { + Throwable cause = celRuntimeException.getCause(); + return new CelEvaluationExceptionBuilder(cause.getMessage()) + .setCause(cause) + .setErrorCode(celRuntimeException.getErrorCode()); + } + + 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..1cb9e8d97 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelEvaluationListener.java +++ b/runtime/src/main/java/dev/cel/runtime/CelEvaluationListener.java @@ -21,6 +21,7 @@ * Functional interface for a callback method invoked by the runtime. Implementations must ensure * that its instances are unconditionally thread-safe. */ +@SuppressWarnings("AndroidJdkLibsChecker") // FunctionalInterface added in 24 @FunctionalInterface @ThreadSafe public interface CelEvaluationListener { @@ -32,9 +33,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..8fe2b8a2e --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/CelFunctionBinding.java @@ -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. + +package dev.cel.runtime; + +import com.google.common.collect.ImmutableList; +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.annotations.Internal; + +/** + * 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 + */ +@Internal +@Immutable +public interface CelFunctionBinding { + String getOverloadId(); + + ImmutableList> getArgTypes(); + + CelFunctionOverload getDefinition(); + + /** Create a unary function binding from the {@code overloadId}, {@code arg}, and {@code impl}. */ + @SuppressWarnings("unchecked") + static CelFunctionBinding from( + String overloadId, Class arg, CelFunctionOverload.Unary impl) { + return from(overloadId, ImmutableList.of(arg), (args) -> impl.apply((T) args[0])); + } + + /** + * Create a binary function binding from the {@code overloadId}, {@code arg1}, {@code arg2}, and + * {@code impl}. + */ + @SuppressWarnings("unchecked") + static CelFunctionBinding from( + String overloadId, Class arg1, Class arg2, CelFunctionOverload.Binary impl) { + return from( + overloadId, ImmutableList.of(arg1, arg2), (args) -> impl.apply((T1) args[0], (T2) args[1])); + } + + /** 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); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/CelFunctionOverload.java b/runtime/src/main/java/dev/cel/runtime/CelFunctionOverload.java index 2ea1cb529..f9b98df5b 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. @@ -17,12 +17,10 @@ import com.google.errorprone.annotations.Immutable; /** Interface describing the general signature of all CEL custom function implementations. */ +@SuppressWarnings("AndroidJdkLibsChecker") // FunctionalInterface added in 24 @Immutable @FunctionalInterface -public interface CelFunctionOverload { - - /** Evaluate a set of arguments throwing a {@code CelEvaluationException} on error. */ - Object apply(Object[] args) throws CelEvaluationException; +public interface CelFunctionOverload extends FunctionOverload { /** * Helper interface for describing unary functions where the type-parameter is used to improve @@ -30,9 +28,7 @@ public interface CelFunctionOverload { */ @Immutable @FunctionalInterface - interface Unary { - Object apply(T arg) throws CelEvaluationException; - } + interface Unary extends FunctionOverload.Unary {} /** * Helper interface for describing binary functions where the type parameters are used to improve @@ -40,7 +36,5 @@ interface Unary { */ @Immutable @FunctionalInterface - interface Binary { - Object apply(T1 arg1, T2 arg2) throws CelEvaluationException; - } + interface Binary extends FunctionOverload.Binary {} } diff --git a/runtime/src/main/java/dev/cel/runtime/IncompleteData.java b/runtime/src/main/java/dev/cel/runtime/CelFunctionResolver.java similarity index 65% rename from runtime/src/main/java/dev/cel/runtime/IncompleteData.java rename to runtime/src/main/java/dev/cel/runtime/CelFunctionResolver.java index 4859f63c6..d769ff238 100644 --- a/runtime/src/main/java/dev/cel/runtime/IncompleteData.java +++ b/runtime/src/main/java/dev/cel/runtime/CelFunctionResolver.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,13 +14,11 @@ package dev.cel.runtime; -import dev.cel.common.annotations.Internal; +import javax.annotation.concurrent.ThreadSafe; /** - * Add an interface for interpreter to check if an object is an instance of {@link PartialMessage}. - * - *

Deprecated. New clients should use {@link CelAttribute} based unknowns. + * Interface to a resolver for CEL functions based on the function name, overload ids, and + * arguments. */ -@Deprecated -@Internal -public interface IncompleteData {} +@ThreadSafe +public interface CelFunctionResolver extends FunctionResolver {} 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..7f83e38fd --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/CelLateFunctionBindings.java @@ -0,0 +1,63 @@ +// 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.List; +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 findOverload( + String functionName, List overloadIds, Object[] args) throws CelEvaluationException { + return DefaultDispatcher.findOverload(functionName, overloadIds, functions, args); + } + + public static CelLateFunctionBindings from(CelFunctionBinding... functions) { + return from(Arrays.asList(functions)); + } + + public static CelLateFunctionBindings from(List functions) { + return new CelLateFunctionBindings( + functions.stream() + .collect( + toImmutableMap( + CelFunctionBinding::getOverloadId, + CelLateFunctionBindings::createResolvedOverload))); + } + + private static ResolvedOverload createResolvedOverload(CelFunctionBinding binding) { + return CelResolvedOverload.of( + binding.getOverloadId(), + binding.getArgTypes(), + (args) -> binding.getDefinition().apply(args)); + } +} 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..0652a7a63 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/CelLiteRuntime.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.runtime; + +import com.google.errorprone.annotations.Immutable; +import javax.annotation.concurrent.ThreadSafe; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.annotations.Beta; +import java.util.Map; + +/** + * 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(); + + /** Creates an evaluable {@code Program} instance which is thread-safe and immutable. */ + @Immutable + 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; + } +} 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..e23749f15 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/CelResolvedOverload.java @@ -0,0 +1,56 @@ +// 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; + +/** + * Representation of a function overload which has been resolved to a specific set of argument types + * and a function definition. + */ +@AutoValue +@Immutable +public abstract class CelResolvedOverload implements ResolvedOverload { + + /** The overload id of the function. */ + @Override + public abstract String getOverloadId(); + + /** The types of the function parameters. */ + @Override + public abstract ImmutableList> getParameterTypes(); + + /** The function definition. */ + @Override + public abstract CelFunctionOverload getDefinition(); + + /** + * Creates a new resolved overload from the given overload id, parameter types, and definition. + */ + public static CelResolvedOverload of( + String overloadId, Class[] parameterTypes, CelFunctionOverload definition) { + return of(overloadId, ImmutableList.copyOf(parameterTypes), definition); + } + + /** + * Creates a new resolved overload from the given overload id, parameter types, and definition. + */ + public static CelResolvedOverload of( + String overloadId, ImmutableList> parameterTypes, CelFunctionOverload definition) { + return new AutoValue_CelResolvedOverload(overloadId, parameterTypes, definition); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntime.java b/runtime/src/main/java/dev/cel/runtime/CelRuntime.java index 01260fb79..d730701e7 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; @@ -37,65 +33,72 @@ public interface CelRuntime { @CanIgnoreReturnValue Program createProgram(CelAbstractSyntaxTree ast) throws CelEvaluationException; + 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 CelLiteRuntime.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())); - } + Object eval(Message message) throws CelEvaluationException; /** 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(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; /** * 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; + + /** + * 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. + */ + Object trace( + CelVariableResolver resolver, + CelFunctionResolver lateBoundFunctionResolver, + CelEvaluationListener listener) + throws CelEvaluationException; + + /** + * 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. + */ + Object trace( + Map mapValue, + CelFunctionResolver lateBoundFunctionResolver, + CelEvaluationListener listener) + throws CelEvaluationException; /** * Advance evaluation based on the current unknown context. @@ -106,131 +109,6 @@ public Object trace(CelVariableResolver resolver, CelEvaluationListener listener *

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). - */ - 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(); - - /** - * Create a unary function binding from the {@code overloadId}, {@code arg}, and {@code impl}. - */ - @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])); - } - - /** - * Create a binary function binding from the {@code overloadId}, {@code arg1}, {@code arg2}, and - * {@code impl}. - */ - @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])); - } - - /** - * Create a function binding from the {@code overloadId}, {@code argTypes}, and {@code impl}. - */ - 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..e1e3c1b51 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeBuilder.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeBuilder.java @@ -33,20 +33,20 @@ 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); /** * Add message {@link Descriptor}s to the builder for type-checking and object creation at @@ -140,9 +140,10 @@ 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. */ @@ -153,6 +154,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); diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java index 5f8f3f259..2947445d1 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java @@ -17,6 +17,7 @@ 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; @@ -42,10 +43,14 @@ import dev.cel.common.types.CelTypes; import dev.cel.common.values.CelValueProvider; import dev.cel.common.values.ProtoMessageValueProvider; +import dev.cel.runtime.CelStandardFunctions.StandardFunction.Overload.Arithmetic; +import dev.cel.runtime.CelStandardFunctions.StandardFunction.Overload.Comparison; +import dev.cel.runtime.CelStandardFunctions.StandardFunction.Overload.Conversions; import java.util.Arrays; +import java.util.HashMap; 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. @@ -60,10 +65,57 @@ public final class CelRuntimeLegacyImpl implements CelRuntime { private final Interpreter interpreter; private final CelOptions options; + 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 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() { + CelRuntimeBuilder builder = + new Builder() + .setOptions(options) + .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. */ @@ -74,17 +126,22 @@ public static CelRuntimeBuilder newBuilder() { /** Builder class for {@code CelRuntimeLegacyImpl}. */ public static final class Builder implements CelRuntimeBuilder { - private final ImmutableSet.Builder fileTypes; - private final ImmutableMap.Builder 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) { @@ -99,7 +156,7 @@ public CelRuntimeBuilder addFunctionBindings(CelFunctionBinding... bindings) { @Override public CelRuntimeBuilder addFunctionBindings(Iterable bindings) { - bindings.forEach(o -> functionBindings.put(o.getOverloadId(), o)); + bindings.forEach(o -> customFunctionBindings.putIfAbsent(o.getOverloadId(), o)); return this; } @@ -148,6 +205,12 @@ public CelRuntimeBuilder setStandardEnvironmentEnabled(boolean value) { return this; } + @Override + public CelRuntimeBuilder setStandardFunctions(CelStandardFunctions standardFunctions) { + this.overriddenStandardFunctions = standardFunctions; + return this; + } + @Override public CelRuntimeBuilder addLibraries(CelRuntimeLibrary... libraries) { checkNotNull(libraries); @@ -171,12 +234,16 @@ public CelRuntimeBuilder setExtensionRegistry(ExtensionRegistry extensionRegistr /** 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( @@ -197,48 +264,126 @@ 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); + } + } + + ImmutableMap.Builder functionBindingsBuilder = + ImmutableMap.builder(); + for (CelFunctionBinding standardFunctionBinding : + newStandardFunctionBindings(runtimeEquality)) { + functionBindingsBuilder.put( + standardFunctionBinding.getOverloadId(), standardFunctionBinding); } - ImmutableMap functionBindingMap = functionBindings.buildOrThrow(); - 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(); - } - })); + functionBindingsBuilder.putAll(customFunctionBindings); + + DefaultDispatcher dispatcher = DefaultDispatcher.create(); + functionBindingsBuilder + .buildOrThrow() + .forEach( + (String overloadId, CelFunctionBinding func) -> + dispatcher.add( + overloadId, func.getArgTypes(), (args) -> func.getDefinition().apply(args))); 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); } else { runtimeTypeProvider = new DescriptorMessageProvider(runtimeTypeFactory, options); } + DefaultInterpreter interpreter = + new DefaultInterpreter( + DescriptorTypeResolver.create(), + runtimeTypeProvider, + dispatcher.immutableCopy(), + options); + return new CelRuntimeLegacyImpl( - new DefaultInterpreter(runtimeTypeProvider, dispatcher, options), options); + 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(Conversions.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(Conversions.INT64_TO_TIMESTAMP)) { + return options.enableTimestampEpoch(); + } + break; + case STRING: + return options.enableStringConversion(); + case ADD: + Arithmetic arithmetic = (Arithmetic) standardOverload; + if (arithmetic.equals(Arithmetic.ADD_STRING)) { + return options.enableStringConcatenation(); + } + if (arithmetic.equals(Arithmetic.ADD_LIST)) { + return options.enableListConcatenation(); + } + break; + default: + if (standardOverload instanceof Comparison + && !options.enableHeterogeneousNumericComparisons()) { + Comparison comparison = (Comparison) standardOverload; + return !comparison.isHeterogeneousComparison(); + } + break; + } + + return true; + }) + .build(); + } else if (overriddenStandardFunctions != null) { + celStandardFunctions = overriddenStandardFunctions; + } else { + return ImmutableSet.of(); + } + + return celStandardFunctions.newFunctionBindings(runtimeEquality, options); } private static CelDescriptorPool newDescriptorPool( @@ -264,15 +409,33 @@ private static ProtoMessageFactory maybeCombineMessageFactory( private Builder() { this.options = CelOptions.newBuilder().build(); this.fileTypes = ImmutableSet.builder(); - this.functionBindings = ImmutableMap.builder(); + this.customFunctionBindings = new HashMap<>(); this.celRuntimeLibraries = ImmutableSet.builder(); this.extensionRegistry = ExtensionRegistry.getEmptyRegistry(); this.customTypeFactory = null; } } - private CelRuntimeLegacyImpl(Interpreter interpreter, CelOptions options) { + private CelRuntimeLegacyImpl( + 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.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..578f428ee --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/CelStandardFunctions.java @@ -0,0 +1,871 @@ +// 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 com.google.common.annotations.VisibleForTesting; +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.annotations.Internal; +import dev.cel.runtime.CelStandardFunctions.StandardFunction.Overload.Arithmetic; +import dev.cel.runtime.CelStandardFunctions.StandardFunction.Overload.BooleanOperator; +import dev.cel.runtime.CelStandardFunctions.StandardFunction.Overload.Comparison; +import dev.cel.runtime.CelStandardFunctions.StandardFunction.Overload.Conversions; +import dev.cel.runtime.CelStandardFunctions.StandardFunction.Overload.DateTime; +import dev.cel.runtime.CelStandardFunctions.StandardFunction.Overload.Index; +import dev.cel.runtime.CelStandardFunctions.StandardFunction.Overload.InternalOperator; +import dev.cel.runtime.CelStandardFunctions.StandardFunction.Overload.Relation; +import dev.cel.runtime.CelStandardFunctions.StandardFunction.Overload.Size; +import dev.cel.runtime.CelStandardFunctions.StandardFunction.Overload.StringMatchers; +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.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.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; + +/** Runtime function bindings for the standard functions in CEL. */ +@Immutable +public final class CelStandardFunctions { + + private final ImmutableSet 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()); + + /** + * Enumeration of Standard Function bindings. + * + *

Note: The conditional, logical_or, logical_and, not_strictly_false, and type functions are + * currently special-cased, and does not appear in this enum. + */ + public enum StandardFunction { + LOGICAL_NOT(BooleanOperator.LOGICAL_NOT), + IN(InternalOperator.IN_LIST, InternalOperator.IN_MAP), + EQUALS(Relation.EQUALS), + NOT_EQUALS(Relation.NOT_EQUALS), + BOOL(Conversions.BOOL_TO_BOOL, Conversions.STRING_TO_BOOL), + ADD( + Arithmetic.ADD_INT64, + Arithmetic.ADD_UINT64, + Arithmetic.ADD_DOUBLE, + Arithmetic.ADD_STRING, + Arithmetic.ADD_BYTES, + Arithmetic.ADD_LIST, + Arithmetic.ADD_TIMESTAMP_DURATION, + Arithmetic.ADD_DURATION_TIMESTAMP, + Arithmetic.ADD_DURATION_DURATION), + SUBTRACT( + Arithmetic.SUBTRACT_INT64, + Arithmetic.SUBTRACT_TIMESTAMP_TIMESTAMP, + Arithmetic.SUBTRACT_TIMESTAMP_DURATION, + Arithmetic.SUBTRACT_UINT64, + Arithmetic.SUBTRACT_DOUBLE, + Arithmetic.SUBTRACT_DURATION_DURATION), + MULTIPLY(Arithmetic.MULTIPLY_INT64, Arithmetic.MULTIPLY_DOUBLE, Arithmetic.MULTIPLY_UINT64), + DIVIDE(Arithmetic.DIVIDE_DOUBLE, Arithmetic.DIVIDE_INT64, Arithmetic.DIVIDE_UINT64), + MODULO(Arithmetic.MODULO_INT64, Arithmetic.MODULO_UINT64), + NEGATE(Arithmetic.NEGATE_INT64, Arithmetic.NEGATE_DOUBLE), + INDEX(Index.INDEX_LIST, Index.INDEX_MAP), + SIZE( + Size.SIZE_STRING, + Size.SIZE_BYTES, + Size.SIZE_LIST, + Size.SIZE_MAP, + Size.STRING_SIZE, + Size.BYTES_SIZE, + Size.LIST_SIZE, + Size.MAP_SIZE), + INT( + Conversions.INT64_TO_INT64, + Conversions.UINT64_TO_INT64, + Conversions.DOUBLE_TO_INT64, + Conversions.STRING_TO_INT64, + Conversions.TIMESTAMP_TO_INT64), + UINT( + Conversions.UINT64_TO_UINT64, + Conversions.INT64_TO_UINT64, + Conversions.DOUBLE_TO_UINT64, + Conversions.STRING_TO_UINT64), + DOUBLE( + Conversions.DOUBLE_TO_DOUBLE, + Conversions.INT64_TO_DOUBLE, + Conversions.STRING_TO_DOUBLE, + Conversions.UINT64_TO_DOUBLE), + STRING( + Conversions.STRING_TO_STRING, + Conversions.INT64_TO_STRING, + Conversions.DOUBLE_TO_STRING, + Conversions.BOOL_TO_STRING, + Conversions.BYTES_TO_STRING, + Conversions.TIMESTAMP_TO_STRING, + Conversions.DURATION_TO_STRING, + Conversions.UINT64_TO_STRING), + BYTES(Conversions.BYTES_TO_BYTES, Conversions.STRING_TO_BYTES), + DURATION(Conversions.DURATION_TO_DURATION, Conversions.STRING_TO_DURATION), + TIMESTAMP( + Conversions.STRING_TO_TIMESTAMP, + Conversions.TIMESTAMP_TO_TIMESTAMP, + Conversions.INT64_TO_TIMESTAMP), + DYN(Conversions.TO_DYN), + MATCHES(StringMatchers.MATCHES, StringMatchers.MATCHES_STRING), + CONTAINS(StringMatchers.CONTAINS_STRING), + ENDS_WITH(StringMatchers.ENDS_WITH_STRING), + STARTS_WITH(StringMatchers.STARTS_WITH_STRING), + // Date/time Functions + GET_FULL_YEAR(DateTime.TIMESTAMP_TO_YEAR, DateTime.TIMESTAMP_TO_YEAR_WITH_TZ), + GET_MONTH(DateTime.TIMESTAMP_TO_MONTH, DateTime.TIMESTAMP_TO_MONTH_WITH_TZ), + GET_DAY_OF_YEAR(DateTime.TIMESTAMP_TO_DAY_OF_YEAR, DateTime.TIMESTAMP_TO_DAY_OF_YEAR_WITH_TZ), + GET_DAY_OF_MONTH( + DateTime.TIMESTAMP_TO_DAY_OF_MONTH, DateTime.TIMESTAMP_TO_DAY_OF_MONTH_WITH_TZ), + GET_DATE( + DateTime.TIMESTAMP_TO_DAY_OF_MONTH_1_BASED, + DateTime.TIMESTAMP_TO_DAY_OF_MONTH_1_BASED_WITH_TZ), + GET_DAY_OF_WEEK(DateTime.TIMESTAMP_TO_DAY_OF_WEEK, DateTime.TIMESTAMP_TO_DAY_OF_WEEK_WITH_TZ), + + GET_HOURS( + DateTime.TIMESTAMP_TO_HOURS, + DateTime.TIMESTAMP_TO_HOURS_WITH_TZ, + DateTime.DURATION_TO_HOURS), + GET_MINUTES( + DateTime.TIMESTAMP_TO_MINUTES, + DateTime.TIMESTAMP_TO_MINUTES_WITH_TZ, + DateTime.DURATION_TO_MINUTES), + GET_SECONDS( + DateTime.TIMESTAMP_TO_SECONDS, + DateTime.TIMESTAMP_TO_SECONDS_WITH_TZ, + DateTime.DURATION_TO_SECONDS), + GET_MILLISECONDS( + DateTime.TIMESTAMP_TO_MILLISECONDS, + DateTime.TIMESTAMP_TO_MILLISECONDS_WITH_TZ, + DateTime.DURATION_TO_MILLISECONDS), + LESS( + Comparison.LESS_BOOL, + Comparison.LESS_INT64, + Comparison.LESS_UINT64, + Comparison.LESS_DOUBLE, + Comparison.LESS_STRING, + Comparison.LESS_BYTES, + Comparison.LESS_TIMESTAMP, + Comparison.LESS_DURATION, + Comparison.LESS_INT64_UINT64, + Comparison.LESS_UINT64_INT64, + Comparison.LESS_INT64_DOUBLE, + Comparison.LESS_DOUBLE_INT64, + Comparison.LESS_UINT64_DOUBLE, + Comparison.LESS_DOUBLE_UINT64), + LESS_EQUALS( + Comparison.LESS_EQUALS_BOOL, + Comparison.LESS_EQUALS_INT64, + Comparison.LESS_EQUALS_UINT64, + Comparison.LESS_EQUALS_DOUBLE, + Comparison.LESS_EQUALS_STRING, + Comparison.LESS_EQUALS_BYTES, + Comparison.LESS_EQUALS_TIMESTAMP, + Comparison.LESS_EQUALS_DURATION, + Comparison.LESS_EQUALS_INT64_UINT64, + Comparison.LESS_EQUALS_UINT64_INT64, + Comparison.LESS_EQUALS_INT64_DOUBLE, + Comparison.LESS_EQUALS_DOUBLE_INT64, + Comparison.LESS_EQUALS_UINT64_DOUBLE, + Comparison.LESS_EQUALS_DOUBLE_UINT64), + GREATER( + Comparison.GREATER_BOOL, + Comparison.GREATER_INT64, + Comparison.GREATER_UINT64, + Comparison.GREATER_DOUBLE, + Comparison.GREATER_STRING, + Comparison.GREATER_BYTES, + Comparison.GREATER_TIMESTAMP, + Comparison.GREATER_DURATION, + Comparison.GREATER_INT64_UINT64, + Comparison.GREATER_UINT64_INT64, + Comparison.GREATER_INT64_DOUBLE, + Comparison.GREATER_DOUBLE_INT64, + Comparison.GREATER_UINT64_DOUBLE, + Comparison.GREATER_DOUBLE_UINT64), + GREATER_EQUALS( + Comparison.GREATER_EQUALS_BOOL, + Comparison.GREATER_EQUALS_BYTES, + Comparison.GREATER_EQUALS_DOUBLE, + Comparison.GREATER_EQUALS_DURATION, + Comparison.GREATER_EQUALS_INT64, + Comparison.GREATER_EQUALS_STRING, + Comparison.GREATER_EQUALS_TIMESTAMP, + Comparison.GREATER_EQUALS_UINT64, + Comparison.GREATER_EQUALS_INT64_UINT64, + Comparison.GREATER_EQUALS_UINT64_INT64, + Comparison.GREATER_EQUALS_INT64_DOUBLE, + Comparison.GREATER_EQUALS_DOUBLE_INT64, + Comparison.GREATER_EQUALS_UINT64_DOUBLE, + Comparison.GREATER_EQUALS_DOUBLE_UINT64); + + /** 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) */ + public enum InternalOperator implements StandardOverload { + IN_LIST(InOverload.IN_LIST::newFunctionBinding), + IN_MAP(InOverload.IN_MAP::newFunctionBinding); + + private final FunctionBindingCreator bindingCreator; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + InternalOperator(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + /** Overloads for functions that test relations. */ + public enum Relation implements StandardOverload { + EQUALS(EqualsOverload.EQUALS::newFunctionBinding), + NOT_EQUALS(NotEqualsOverload.NOT_EQUALS::newFunctionBinding); + + private final FunctionBindingCreator bindingCreator; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + Relation(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + /** Overloads for performing arithmetic operations. */ + public enum Arithmetic implements StandardOverload { + ADD_INT64(AddOverload.ADD_INT64::newFunctionBinding), + ADD_UINT64(AddOverload.ADD_UINT64::newFunctionBinding), + ADD_BYTES(AddOverload.ADD_BYTES::newFunctionBinding), + ADD_DOUBLE(AddOverload.ADD_DOUBLE::newFunctionBinding), + ADD_DURATION_DURATION(AddOverload.ADD_DURATION_DURATION::newFunctionBinding), + ADD_TIMESTAMP_DURATION(AddOverload.ADD_TIMESTAMP_DURATION::newFunctionBinding), + ADD_STRING(AddOverload.ADD_STRING::newFunctionBinding), + ADD_DURATION_TIMESTAMP(AddOverload.ADD_DURATION_TIMESTAMP::newFunctionBinding), + ADD_LIST(AddOverload.ADD_LIST::newFunctionBinding), + + SUBTRACT_INT64(SubtractOverload.SUBTRACT_INT64::newFunctionBinding), + SUBTRACT_TIMESTAMP_TIMESTAMP( + SubtractOverload.SUBTRACT_TIMESTAMP_TIMESTAMP::newFunctionBinding), + SUBTRACT_TIMESTAMP_DURATION( + SubtractOverload.SUBTRACT_TIMESTAMP_DURATION::newFunctionBinding), + SUBTRACT_UINT64(SubtractOverload.SUBTRACT_UINT64::newFunctionBinding), + SUBTRACT_DOUBLE(SubtractOverload.SUBTRACT_DOUBLE::newFunctionBinding), + SUBTRACT_DURATION_DURATION(SubtractOverload.SUBTRACT_DURATION_DURATION::newFunctionBinding), + + MULTIPLY_INT64(MultiplyOverload.MULTIPLY_INT64::newFunctionBinding), + MULTIPLY_DOUBLE(MultiplyOverload.MULTIPLY_DOUBLE::newFunctionBinding), + MULTIPLY_UINT64(MultiplyOverload.MULTIPLY_UINT64::newFunctionBinding), + + DIVIDE_DOUBLE(DivideOverload.DIVIDE_DOUBLE::newFunctionBinding), + DIVIDE_INT64(DivideOverload.DIVIDE_INT64::newFunctionBinding), + DIVIDE_UINT64(DivideOverload.DIVIDE_UINT64::newFunctionBinding), + + MODULO_INT64(ModuloOverload.MODULO_INT64::newFunctionBinding), + MODULO_UINT64(ModuloOverload.MODULO_UINT64::newFunctionBinding), + + NEGATE_INT64(NegateOverload.NEGATE_INT64::newFunctionBinding), + NEGATE_DOUBLE(NegateOverload.NEGATE_DOUBLE::newFunctionBinding); + + private final FunctionBindingCreator bindingCreator; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + Arithmetic(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + /** Overloads for indexing a list or a map. */ + public enum Index implements StandardOverload { + INDEX_LIST(IndexOverload.INDEX_LIST::newFunctionBinding), + INDEX_MAP(IndexOverload.INDEX_MAP::newFunctionBinding); + + private final FunctionBindingCreator bindingCreator; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + Index(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + /** Overloads for retrieving the size of a literal or a collection. */ + public enum Size implements StandardOverload { + SIZE_BYTES(SizeOverload.SIZE_BYTES::newFunctionBinding), + BYTES_SIZE(SizeOverload.BYTES_SIZE::newFunctionBinding), + SIZE_LIST(SizeOverload.SIZE_LIST::newFunctionBinding), + LIST_SIZE(SizeOverload.LIST_SIZE::newFunctionBinding), + SIZE_STRING(SizeOverload.SIZE_STRING::newFunctionBinding), + STRING_SIZE(SizeOverload.STRING_SIZE::newFunctionBinding), + SIZE_MAP(SizeOverload.SIZE_MAP::newFunctionBinding), + MAP_SIZE(SizeOverload.MAP_SIZE::newFunctionBinding); + + private final FunctionBindingCreator bindingCreator; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + Size(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + /** Overloads for performing type conversions. */ + public enum Conversions implements StandardOverload { + BOOL_TO_BOOL(BoolOverload.BOOL_TO_BOOL::newFunctionBinding), + STRING_TO_BOOL(BoolOverload.STRING_TO_BOOL::newFunctionBinding), + INT64_TO_INT64(IntOverload.INT64_TO_INT64::newFunctionBinding), + DOUBLE_TO_INT64(IntOverload.DOUBLE_TO_INT64::newFunctionBinding), + STRING_TO_INT64(IntOverload.STRING_TO_INT64::newFunctionBinding), + TIMESTAMP_TO_INT64(IntOverload.TIMESTAMP_TO_INT64::newFunctionBinding), + UINT64_TO_INT64(IntOverload.UINT64_TO_INT64::newFunctionBinding), + + UINT64_TO_UINT64(UintOverload.UINT64_TO_UINT64::newFunctionBinding), + INT64_TO_UINT64(UintOverload.INT64_TO_UINT64::newFunctionBinding), + DOUBLE_TO_UINT64(UintOverload.DOUBLE_TO_UINT64::newFunctionBinding), + STRING_TO_UINT64(UintOverload.STRING_TO_UINT64::newFunctionBinding), + + DOUBLE_TO_DOUBLE(DoubleOverload.DOUBLE_TO_DOUBLE::newFunctionBinding), + INT64_TO_DOUBLE(DoubleOverload.INT64_TO_DOUBLE::newFunctionBinding), + STRING_TO_DOUBLE(DoubleOverload.STRING_TO_DOUBLE::newFunctionBinding), + UINT64_TO_DOUBLE(DoubleOverload.UINT64_TO_DOUBLE::newFunctionBinding), + + STRING_TO_STRING(StringOverload.STRING_TO_STRING::newFunctionBinding), + INT64_TO_STRING(StringOverload.INT64_TO_STRING::newFunctionBinding), + DOUBLE_TO_STRING(StringOverload.DOUBLE_TO_STRING::newFunctionBinding), + BOOL_TO_STRING(StringOverload.BOOL_TO_STRING::newFunctionBinding), + BYTES_TO_STRING(StringOverload.BYTES_TO_STRING::newFunctionBinding), + TIMESTAMP_TO_STRING(StringOverload.TIMESTAMP_TO_STRING::newFunctionBinding), + DURATION_TO_STRING(StringOverload.DURATION_TO_STRING::newFunctionBinding), + UINT64_TO_STRING(StringOverload.UINT64_TO_STRING::newFunctionBinding), + + BYTES_TO_BYTES(BytesOverload.BYTES_TO_BYTES::newFunctionBinding), + STRING_TO_BYTES(BytesOverload.STRING_TO_BYTES::newFunctionBinding), + + DURATION_TO_DURATION(DurationOverload.DURATION_TO_DURATION::newFunctionBinding), + STRING_TO_DURATION(DurationOverload.STRING_TO_DURATION::newFunctionBinding), + + STRING_TO_TIMESTAMP(TimestampOverload.STRING_TO_TIMESTAMP::newFunctionBinding), + TIMESTAMP_TO_TIMESTAMP(TimestampOverload.TIMESTAMP_TO_TIMESTAMP::newFunctionBinding), + INT64_TO_TIMESTAMP(TimestampOverload.INT64_TO_TIMESTAMP::newFunctionBinding), + + TO_DYN(DynOverload.TO_DYN::newFunctionBinding); + + private final FunctionBindingCreator bindingCreator; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + Conversions(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + /** + * Overloads for functions performing string matching, such as regular expressions or contains + * check. + */ + public enum StringMatchers implements StandardOverload { + MATCHES(MatchesOverload.MATCHES::newFunctionBinding), + MATCHES_STRING(MatchesOverload.MATCHES_STRING::newFunctionBinding), + CONTAINS_STRING(ContainsOverload.CONTAINS_STRING::newFunctionBinding), + ENDS_WITH_STRING(EndsWithOverload.ENDS_WITH_STRING::newFunctionBinding), + STARTS_WITH_STRING(StartsWithOverload.STARTS_WITH_STRING::newFunctionBinding); + + private final FunctionBindingCreator bindingCreator; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + StringMatchers(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + /** Overloads for logical operators that return a bool as a result. */ + public enum BooleanOperator implements StandardOverload { + LOGICAL_NOT(LogicalNotOverload.LOGICAL_NOT::newFunctionBinding); + + private final FunctionBindingCreator bindingCreator; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + BooleanOperator(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + /** Overloads for functions performing date/time operations. */ + public enum DateTime implements StandardOverload { + TIMESTAMP_TO_YEAR(GetFullYearOverload.TIMESTAMP_TO_YEAR::newFunctionBinding), + TIMESTAMP_TO_YEAR_WITH_TZ( + GetFullYearOverload.TIMESTAMP_TO_YEAR_WITH_TZ::newFunctionBinding), + TIMESTAMP_TO_MONTH(GetMonthOverload.TIMESTAMP_TO_MONTH::newFunctionBinding), + TIMESTAMP_TO_MONTH_WITH_TZ(GetMonthOverload.TIMESTAMP_TO_MONTH_WITH_TZ::newFunctionBinding), + TIMESTAMP_TO_DAY_OF_YEAR(GetDayOfYearOverload.TIMESTAMP_TO_DAY_OF_YEAR::newFunctionBinding), + TIMESTAMP_TO_DAY_OF_YEAR_WITH_TZ( + GetDayOfYearOverload.TIMESTAMP_TO_DAY_OF_YEAR_WITH_TZ::newFunctionBinding), + TIMESTAMP_TO_DAY_OF_MONTH( + GetDayOfMonthOverload.TIMESTAMP_TO_DAY_OF_MONTH::newFunctionBinding), + TIMESTAMP_TO_DAY_OF_MONTH_WITH_TZ( + GetDayOfMonthOverload.TIMESTAMP_TO_DAY_OF_MONTH_WITH_TZ::newFunctionBinding), + TIMESTAMP_TO_DAY_OF_MONTH_1_BASED( + GetDateOverload.TIMESTAMP_TO_DAY_OF_MONTH_1_BASED::newFunctionBinding), + TIMESTAMP_TO_DAY_OF_MONTH_1_BASED_WITH_TZ( + GetDateOverload.TIMESTAMP_TO_DAY_OF_MONTH_1_BASED_WITH_TZ::newFunctionBinding), + + TIMESTAMP_TO_DAY_OF_WEEK(GetDayOfWeekOverload.TIMESTAMP_TO_DAY_OF_WEEK::newFunctionBinding), + TIMESTAMP_TO_DAY_OF_WEEK_WITH_TZ( + GetDayOfWeekOverload.TIMESTAMP_TO_DAY_OF_WEEK_WITH_TZ::newFunctionBinding), + TIMESTAMP_TO_HOURS(GetHoursOverload.TIMESTAMP_TO_HOURS::newFunctionBinding), + TIMESTAMP_TO_HOURS_WITH_TZ(GetHoursOverload.TIMESTAMP_TO_HOURS_WITH_TZ::newFunctionBinding), + TIMESTAMP_TO_MINUTES(GetMinutesOverload.TIMESTAMP_TO_MINUTES::newFunctionBinding), + TIMESTAMP_TO_MINUTES_WITH_TZ( + GetMinutesOverload.TIMESTAMP_TO_MINUTES_WITH_TZ::newFunctionBinding), + TIMESTAMP_TO_SECONDS(GetSecondsOverload.TIMESTAMP_TO_SECONDS::newFunctionBinding), + TIMESTAMP_TO_SECONDS_WITH_TZ( + GetSecondsOverload.TIMESTAMP_TO_SECONDS_WITH_TZ::newFunctionBinding), + TIMESTAMP_TO_MILLISECONDS( + GetMillisecondsOverload.TIMESTAMP_TO_MILLISECONDS::newFunctionBinding), + TIMESTAMP_TO_MILLISECONDS_WITH_TZ( + GetMillisecondsOverload.TIMESTAMP_TO_MILLISECONDS_WITH_TZ::newFunctionBinding), + DURATION_TO_HOURS(GetHoursOverload.DURATION_TO_HOURS::newFunctionBinding), + DURATION_TO_MINUTES(GetMinutesOverload.DURATION_TO_MINUTES::newFunctionBinding), + DURATION_TO_SECONDS(GetSecondsOverload.DURATION_TO_SECONDS::newFunctionBinding), + DURATION_TO_MILLISECONDS( + GetMillisecondsOverload.DURATION_TO_MILLISECONDS::newFunctionBinding); + + private final FunctionBindingCreator bindingCreator; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + DateTime(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + /** Overloads for performing numeric comparisons. */ + public enum Comparison implements StandardOverload { + LESS_BOOL(LessOverload.LESS_BOOL::newFunctionBinding, false), + LESS_INT64(LessOverload.LESS_INT64::newFunctionBinding, false), + LESS_UINT64(LessOverload.LESS_UINT64::newFunctionBinding, false), + LESS_BYTES(LessOverload.LESS_BYTES::newFunctionBinding, false), + LESS_DOUBLE(LessOverload.LESS_DOUBLE::newFunctionBinding, false), + LESS_DOUBLE_UINT64(LessOverload.LESS_DOUBLE_UINT64::newFunctionBinding, true), + LESS_INT64_UINT64(LessOverload.LESS_INT64_UINT64::newFunctionBinding, true), + LESS_UINT64_INT64(LessOverload.LESS_UINT64_INT64::newFunctionBinding, true), + LESS_INT64_DOUBLE(LessOverload.LESS_INT64_DOUBLE::newFunctionBinding, true), + LESS_DOUBLE_INT64(LessOverload.LESS_DOUBLE_INT64::newFunctionBinding, true), + LESS_UINT64_DOUBLE(LessOverload.LESS_UINT64_DOUBLE::newFunctionBinding, true), + LESS_DURATION(LessOverload.LESS_DURATION::newFunctionBinding, false), + LESS_STRING(LessOverload.LESS_STRING::newFunctionBinding, false), + LESS_TIMESTAMP(LessOverload.LESS_TIMESTAMP::newFunctionBinding, false), + LESS_EQUALS_BOOL(LessEqualsOverload.LESS_EQUALS_BOOL::newFunctionBinding, false), + LESS_EQUALS_BYTES(LessEqualsOverload.LESS_EQUALS_BYTES::newFunctionBinding, false), + LESS_EQUALS_DOUBLE(LessEqualsOverload.LESS_EQUALS_DOUBLE::newFunctionBinding, false), + LESS_EQUALS_DURATION(LessEqualsOverload.LESS_EQUALS_DURATION::newFunctionBinding, false), + LESS_EQUALS_INT64(LessEqualsOverload.LESS_EQUALS_INT64::newFunctionBinding, false), + LESS_EQUALS_STRING(LessEqualsOverload.LESS_EQUALS_STRING::newFunctionBinding, false), + LESS_EQUALS_TIMESTAMP(LessEqualsOverload.LESS_EQUALS_TIMESTAMP::newFunctionBinding, false), + LESS_EQUALS_UINT64(LessEqualsOverload.LESS_EQUALS_UINT64::newFunctionBinding, false), + LESS_EQUALS_INT64_UINT64( + LessEqualsOverload.LESS_EQUALS_INT64_UINT64::newFunctionBinding, true), + LESS_EQUALS_UINT64_INT64( + LessEqualsOverload.LESS_EQUALS_UINT64_INT64::newFunctionBinding, true), + LESS_EQUALS_INT64_DOUBLE( + LessEqualsOverload.LESS_EQUALS_INT64_DOUBLE::newFunctionBinding, true), + LESS_EQUALS_DOUBLE_INT64( + LessEqualsOverload.LESS_EQUALS_DOUBLE_INT64::newFunctionBinding, true), + LESS_EQUALS_UINT64_DOUBLE( + LessEqualsOverload.LESS_EQUALS_UINT64_DOUBLE::newFunctionBinding, true), + LESS_EQUALS_DOUBLE_UINT64( + LessEqualsOverload.LESS_EQUALS_DOUBLE_UINT64::newFunctionBinding, true), + GREATER_BOOL(GreaterOverload.GREATER_BOOL::newFunctionBinding, false), + GREATER_BYTES(GreaterOverload.GREATER_BYTES::newFunctionBinding, false), + GREATER_DOUBLE(GreaterOverload.GREATER_DOUBLE::newFunctionBinding, false), + GREATER_DURATION(GreaterOverload.GREATER_DURATION::newFunctionBinding, false), + GREATER_INT64(GreaterOverload.GREATER_INT64::newFunctionBinding, false), + GREATER_STRING(GreaterOverload.GREATER_STRING::newFunctionBinding, false), + GREATER_TIMESTAMP(GreaterOverload.GREATER_TIMESTAMP::newFunctionBinding, false), + GREATER_UINT64(GreaterOverload.GREATER_UINT64::newFunctionBinding, false), + GREATER_INT64_UINT64(GreaterOverload.GREATER_INT64_UINT64::newFunctionBinding, true), + GREATER_UINT64_INT64(GreaterOverload.GREATER_UINT64_INT64::newFunctionBinding, true), + GREATER_INT64_DOUBLE(GreaterOverload.GREATER_INT64_DOUBLE::newFunctionBinding, true), + GREATER_DOUBLE_INT64(GreaterOverload.GREATER_DOUBLE_INT64::newFunctionBinding, true), + GREATER_UINT64_DOUBLE(GreaterOverload.GREATER_UINT64_DOUBLE::newFunctionBinding, true), + GREATER_DOUBLE_UINT64(GreaterOverload.GREATER_DOUBLE_UINT64::newFunctionBinding, true), + GREATER_EQUALS_BOOL(GreaterEqualsOverload.GREATER_EQUALS_BOOL::newFunctionBinding, false), + GREATER_EQUALS_BYTES(GreaterEqualsOverload.GREATER_EQUALS_BYTES::newFunctionBinding, false), + GREATER_EQUALS_DOUBLE( + GreaterEqualsOverload.GREATER_EQUALS_DOUBLE::newFunctionBinding, false), + GREATER_EQUALS_DURATION( + GreaterEqualsOverload.GREATER_EQUALS_DURATION::newFunctionBinding, false), + GREATER_EQUALS_INT64(GreaterEqualsOverload.GREATER_EQUALS_INT64::newFunctionBinding, false), + GREATER_EQUALS_STRING( + GreaterEqualsOverload.GREATER_EQUALS_STRING::newFunctionBinding, false), + GREATER_EQUALS_TIMESTAMP( + GreaterEqualsOverload.GREATER_EQUALS_TIMESTAMP::newFunctionBinding, false), + GREATER_EQUALS_UINT64( + GreaterEqualsOverload.GREATER_EQUALS_UINT64::newFunctionBinding, false), + GREATER_EQUALS_INT64_UINT64( + GreaterEqualsOverload.GREATER_EQUALS_INT64_UINT64::newFunctionBinding, true), + GREATER_EQUALS_UINT64_INT64( + GreaterEqualsOverload.GREATER_EQUALS_UINT64_INT64::newFunctionBinding, true), + GREATER_EQUALS_INT64_DOUBLE( + GreaterEqualsOverload.GREATER_EQUALS_INT64_DOUBLE::newFunctionBinding, true), + GREATER_EQUALS_DOUBLE_INT64( + GreaterEqualsOverload.GREATER_EQUALS_DOUBLE_INT64::newFunctionBinding, true), + GREATER_EQUALS_UINT64_DOUBLE( + GreaterEqualsOverload.GREATER_EQUALS_UINT64_DOUBLE::newFunctionBinding, true), + GREATER_EQUALS_DOUBLE_UINT64( + GreaterEqualsOverload.GREATER_EQUALS_DOUBLE_UINT64::newFunctionBinding, true); + + private final FunctionBindingCreator bindingCreator; + private final boolean isHeterogeneousComparison; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + Comparison(FunctionBindingCreator bindingCreator, boolean isHeterogeneousComparison) { + this.bindingCreator = bindingCreator; + this.isHeterogeneousComparison = isHeterogeneousComparison; + } + + public boolean isHeterogeneousComparison() { + return isHeterogeneousComparison; + } + } + + private Overload() {} + } + + private final ImmutableSet standardOverloads; + + StandardFunction(StandardOverload... overloads) { + this.standardOverloads = ImmutableSet.copyOf(overloads); + } + + @VisibleForTesting + ImmutableSet getOverloads() { + return standardOverloads; + } + } + + @VisibleForTesting + ImmutableSet getOverloads() { + return standardOverloads; + } + + @Internal + public ImmutableSet newFunctionBindings( + RuntimeEquality runtimeEquality, CelOptions celOptions) { + ImmutableSet.Builder builder = ImmutableSet.builder(); + for (StandardOverload overload : standardOverloads) { + builder.add(overload.newFunctionBinding(celOptions, runtimeEquality)); + } + + return builder.build(); + } + + /** General interface for defining a standard function overload. */ + @Immutable + public interface StandardOverload { + CelFunctionBinding newFunctionBinding(CelOptions celOptions, RuntimeEquality runtimeEquality); + } + + /** 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"); + + ImmutableSet.Builder standardOverloadBuilder = ImmutableSet.builder(); + for (StandardFunction standardFunction : StandardFunction.values()) { + if (hasIncludeFunctions) { + if (this.includeFunctions.contains(standardFunction)) { + standardOverloadBuilder.addAll(standardFunction.standardOverloads); + } + continue; + } + if (hasExcludeFunctions) { + if (!this.excludeFunctions.contains(standardFunction)) { + standardOverloadBuilder.addAll(standardFunction.standardOverloads); + } + continue; + } + if (hasFilterFunction) { + ImmutableSet.Builder filteredOverloadsBuilder = ImmutableSet.builder(); + for (StandardOverload standardOverload : standardFunction.standardOverloads) { + boolean includeOverload = functionFilter.include(standardFunction, standardOverload); + if (includeOverload) { + standardOverloadBuilder.add(standardOverload); + } + } + + ImmutableSet filteredOverloads = filteredOverloadsBuilder.build(); + if (!filteredOverloads.isEmpty()) { + standardOverloadBuilder.addAll(filteredOverloads); + } + + continue; + } + + standardOverloadBuilder.addAll(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. + */ + @SuppressWarnings("AndroidJdkLibsChecker") // FunctionalInterface added in 24 + @FunctionalInterface + public interface FunctionFilter { + boolean include(StandardFunction standardFunction, StandardOverload standardOverload); + } + } + + /** Creates a new builder to configure CelStandardFunctions. */ + public static Builder newBuilder() { + return new Builder(); + } + + @SuppressWarnings("AndroidJdkLibsChecker") // FunctionalInterface added in 24 + @FunctionalInterface + @Immutable + private interface FunctionBindingCreator { + CelFunctionBinding create(CelOptions celOptions, RuntimeEquality runtimeEquality); + } + + private CelStandardFunctions(ImmutableSet 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..c7f1d0c91 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)); + } + + 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..cf53844c0 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/CelValueRuntimeTypeProvider.java @@ -0,0 +1,157 @@ +// 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.CelErrorCode; +import dev.cel.common.CelRuntimeException; +import dev.cel.common.annotations.Internal; +import dev.cel.common.values.BaseProtoCelValueConverter; +import dev.cel.common.values.BaseProtoMessageValueProvider; +import dev.cel.common.values.CelValue; +import dev.cel.common.values.CelValueProvider; +import dev.cel.common.values.CombinedCelValueProvider; +import dev.cel.common.values.SelectableValue; +import dev.cel.common.values.StringValue; +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() { + @Override + public CelValue fromProtoMessageToCelValue(MessageLite msg) { + throw new UnsupportedOperationException( + "A value provider must be provided in the runtime to handle protobuf messages"); + } + }; + + 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 unwrapCelValue( + valueProvider + .newValue(messageName, values) + .orElseThrow( + () -> + new NoSuchElementException( + String.format("cannot resolve '%s' as a message", messageName)))); + } + + @Override + public Object selectField(Object message, String fieldName) { + SelectableValue selectableValue = getSelectableValueOrThrow(message, fieldName); + + return unwrapCelValue(selectableValue.select(StringValue.create(fieldName))); + } + + @Override + public Object hasField(Object message, String fieldName) { + SelectableValue selectableValue = getSelectableValueOrThrow(message, fieldName); + + return selectableValue.find(StringValue.create(fieldName)).isPresent(); + } + + @SuppressWarnings("unchecked") + private SelectableValue getSelectableValueOrThrow(Object obj, String fieldName) { + CelValue convertedCelValue; + if ((obj instanceof MessageLite)) { + convertedCelValue = protoCelValueConverter.fromProtoMessageToCelValue((MessageLite) obj); + } else { + convertedCelValue = protoCelValueConverter.fromJavaObjectToCelValue(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 unwrapCelValue( + protoCelValueConverter.fromProtoMessageToCelValue((MessageLite) message)); + } else { + return unwrapCelValue(protoCelValueConverter.fromJavaObjectToCelValue(message)); + } + } + + /** + * DefaultInterpreter cannot handle CelValue and instead expects plain Java objects. + * + *

This will become unnecessary once we introduce a rewrite of a Cel runtime. + */ + private Object unwrapCelValue(CelValue object) { + return protoCelValueConverter.fromCelValueToJavaObject(object); + } + + private static void throwInvalidFieldSelection(String fieldName) { + throw new CelRuntimeException( + new IllegalArgumentException( + String.format( + "Error resolving field '%s'. Field selections must be performed on messages or" + + " maps.", + fieldName)), + CelErrorCode.ATTRIBUTE_NOT_FOUND); + } + + 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..ac7696751 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/ConcatenatedListView.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 aj +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS 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.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}. + */ +final class ConcatenatedListView extends AbstractList { + private final List> sourceLists; + private int totalSize = 0; + + ConcatenatedListView() { + this.sourceLists = new ArrayList<>(); + } + + 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..1b89939ea 100644 --- a/runtime/src/main/java/dev/cel/runtime/DefaultDispatcher.java +++ b/runtime/src/main/java/dev/cel/runtime/DefaultDispatcher.java @@ -14,122 +14,42 @@ package dev.cel.runtime; +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.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 java.util.ArrayList; 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. - * - *

CEL Library Internals. Do Not Use. */ @ThreadSafe -@Internal -public final class DefaultDispatcher implements Dispatcher, Registrar { - @SuppressWarnings("unused") - private final CelOptions celOptions; - - /** - * @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)); - } - - public static DefaultDispatcher create(CelOptions celOptions) { - DynamicProto dynamicProto = DynamicProto.create(DefaultMessageFactory.INSTANCE); - return create(celOptions, dynamicProto); - } - - public static DefaultDispatcher create(CelOptions celOptions, DynamicProto dynamicProto) { - DefaultDispatcher dispatcher = new DefaultDispatcher(celOptions); - StandardFunctions.add(dispatcher, dynamicProto, celOptions); - return dispatcher; - } - +final class DefaultDispatcher implements Dispatcher, Registrar { public static DefaultDispatcher create() { - return create(CelOptions.LEGACY); - } - - /** 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; - } - continue; - } - if (!paramType.isAssignableFrom(arg.getClass())) { - return false; - } - } - return true; - } + return new DefaultDispatcher(); } @GuardedBy("this") - private final Map overloads = new HashMap<>(); + private final Map overloads = new HashMap<>(); @Override @SuppressWarnings("unchecked") public synchronized void add( - String overloadId, Class argType, final UnaryFunction function) { + String overloadId, Class argType, final Registrar.UnaryFunction function) { overloads.put( - overloadId, new Overload(new Class[] {argType}, args -> function.apply((T) args[0]))); + overloadId, + ResolvedOverloadImpl.of( + overloadId, new Class[] {argType}, args -> function.apply((T) args[0]))); } @Override @@ -138,73 +58,63 @@ public synchronized void add( String overloadId, Class argType1, Class argType2, - final BinaryFunction function) { + final Registrar.BinaryFunction function) { overloads.put( overloadId, - new Overload( + ResolvedOverloadImpl.of( + overloadId, new Class[] {argType1, argType2}, args -> function.apply((T1) args[0], (T2) args[1]))); } @Override - public synchronized void add(String overloadId, List> argTypes, Function function) { - overloads.put(overloadId, new Overload(argTypes.toArray(new Class[0]), function)); + public synchronized void add( + String overloadId, List> argTypes, Registrar.Function function) { + overloads.put( + overloadId, + ResolvedOverloadImpl.of(overloadId, argTypes.toArray(new Class[0]), function)); } - private static Object dispatch( - Metadata metadata, - long exprId, + @Override + public synchronized Optional findOverload( + String functionName, List overloadIds, Object[] args) throws CelEvaluationException { + return DefaultDispatcher.findOverload(functionName, overloadIds, overloads, args); + } + + /** Finds the overload that matches the given function name, overload IDs, and arguments. */ + public static Optional findOverload( String functionName, List overloadIds, - Map overloads, + Map overloads, Object[] args) - throws InterpreterException { - List candidates = new ArrayList<>(); + throws CelEvaluationException { + int matchingOverloadCount = 0; + ResolvedOverload match = null; + List candidates = null; 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(); + ResolvedOverload 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()); + } + candidates.add(overloadId); + } + match = overload; } } - if (candidates.size() > 1) { - throw new InterpreterException.Builder( + + if (matchingOverloadCount > 1) { + throw CelEvaluationExceptionBuilder.newBuilder( "Ambiguous overloads for function '%s'. Matching candidates: %s", - functionName, Joiner.on(",").join(candidates)) + functionName, Joiner.on(", ").join(candidates)) .setErrorCode(CelErrorCode.AMBIGUOUS_OVERLOAD) - .setLocation(metadata, exprId) .build(); } - - 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(); - } - - @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); + return Optional.ofNullable(match); } @Override @@ -214,22 +124,17 @@ public synchronized Dispatcher.ImmutableCopy immutableCopy() { @Immutable private static final class ImmutableCopy implements Dispatcher.ImmutableCopy { - private final ImmutableMap overloads; + private final ImmutableMap overloads; - private ImmutableCopy(Map overloads) { + private ImmutableCopy(Map overloads) { this.overloads = ImmutableMap.copyOf(overloads); } @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 Optional findOverload( + String functionName, List overloadIds, Object[] args) + throws CelEvaluationException { + return DefaultDispatcher.findOverload(functionName, overloadIds, overloads, args); } @Override @@ -237,4 +142,33 @@ public Dispatcher.ImmutableCopy immutableCopy() { return this; } } + + private DefaultDispatcher() {} + + @AutoValue + @Immutable + abstract static class ResolvedOverloadImpl implements ResolvedOverload { + /** The overload id of the function. */ + @Override + public abstract String getOverloadId(); + + /** The types of the function parameters. */ + @Override + public abstract ImmutableList> getParameterTypes(); + + /** The function definition. */ + @Override + public abstract FunctionOverload getDefinition(); + + static ResolvedOverload of( + String overloadId, Class[] parameterTypes, FunctionOverload definition) { + return of(overloadId, ImmutableList.copyOf(parameterTypes), definition); + } + + static ResolvedOverload of( + String overloadId, ImmutableList> parameterTypes, FunctionOverload definition) { + return new AutoValue_DefaultDispatcher_ResolvedOverloadImpl( + overloadId, parameterTypes, definition); + } + } } diff --git a/runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java b/runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java index 44832a2e7..afdca0e3e 100644 --- a/runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java +++ b/runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java @@ -14,36 +14,43 @@ 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.CelRuntimeException; 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; import dev.cel.common.ast.CelReference; 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; @@ -51,35 +58,11 @@ import java.util.Map; import java.util.Optional; -/** - * 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 CelOptions celOptions; @@ -109,10 +92,6 @@ static IntermediateResult create(Object value) { } } - public DefaultInterpreter(RuntimeTypeProvider typeProvider, Dispatcher dispatcher) { - this(typeProvider, dispatcher, CelOptions.LEGACY); - } - /** * Creates a new interpreter * @@ -121,26 +100,25 @@ 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, + Dispatcher 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 Metadata metadata; @@ -148,44 +126,112 @@ private static final class DefaultInterpretable private final CelOptions celOptions; DefaultInterpretable( + TypeResolver typeResolver, RuntimeTypeProvider typeProvider, Dispatcher 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).immutableCopy(); + 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, FunctionResolver lateBoundFunctionResolver) + throws CelEvaluationException { + return evalTrackingUnknowns( + RuntimeUnknownResolver.fromResolver(resolver), + Optional.of(lateBoundFunctionResolver), + Optional.empty()); + } + + @Override + public Object eval( + GlobalResolver resolver, + FunctionResolver 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; @@ -194,7 +240,7 @@ private IntermediateResult evalInternal(ExecutionFrame frame, CelExpr expr) result = IntermediateResult.create(evalConstant(frame, expr, expr.constant())); break; case IDENT: - result = evalIdent(frame, expr, expr.ident()); + result = evalIdent(frame, expr); break; case SELECT: result = evalSelect(frame, expr, expr.select()); @@ -202,14 +248,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()); @@ -218,24 +264,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: @@ -251,14 +310,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, CelIdent unusedIdent) - throws InterpreterException { + private IntermediateResult evalIdent(ExecutionFrame frame, CelExpr expr) + throws CelEvaluationException { CelReference reference = ast.getReferenceOrThrow(expr.id()); if (reference.value().isPresent()) { return IntermediateResult.create(evalConstant(frame, expr, reference.value().get())); @@ -267,23 +331,34 @@ private IntermediateResult evalIdent(ExecutionFrame frame, CelExpr expr, CelIden } 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); } IntermediateResult rawResult = frame.resolveSimpleName(name, expr.id()); + Object value = rawResult.value(); + boolean isLazyExpression = value instanceof LazyExpression; + if (isLazyExpression) { + value = evalInternal(frame, ((LazyExpression) value).celExpr).value(); + } // Value resolved from Binding, it could be Message, PartialMessage or unbound(null) - Object value = InterpreterUtil.strict(typeProvider.adapt(rawResult.value())); - return IntermediateResult.create(rawResult.attribute(), value); + value = InterpreterUtil.strict(typeProvider.adapt(checkedType.name(), value)); + IntermediateResult result = IntermediateResult.create(rawResult.attribute(), value); + + if (isLazyExpression) { + frame.cacheLazilyEvaluatedResult(name, result); + } + + return result; } 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(); @@ -301,7 +376,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(); @@ -330,7 +405,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()); @@ -360,6 +435,8 @@ private IntermediateResult evalCall(ExecutionFrame frame, CelExpr expr, CelCall return result.get(); } break; + case "cel_block_list": + return evalCelBlock(frame, expr, callExpr); default: break; } @@ -376,10 +453,6 @@ private IntermediateResult evalCall(ExecutionFrame frame, CelExpr expr, CelCall // 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."); } Optional indexAttr = @@ -404,12 +477,57 @@ private IntermediateResult evalCall(ExecutionFrame frame, CelExpr expr, CelCall return IntermediateResult.create(attr, unknowns.get()); } - Object[] argArray = Arrays.stream(argResults).map(v -> v.value()).toArray(); + Object[] argArray = Arrays.stream(argResults).map(IntermediateResult::value).toArray(); + ImmutableList overloadIds = reference.overloadIds(); + ResolvedOverload 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 ResolvedOverload findOverloadOrThrow( + ExecutionFrame frame, + CelExpr expr, + String functionName, + List overloadIds, + Object[] args) + throws CelEvaluationException { + try { + Optional funcImpl = + dispatcher.findOverload(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( @@ -441,70 +559,126 @@ 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 + } + + private boolean canShortCircuit(IntermediateResult result, ShortCircuitableOperators operator) { + if (!(result.value() instanceof Boolean)) { + return false; } - IntermediateResult right = evalBooleanNonstrict(frame, callExpr.args().get(1)); - if (right.value() instanceof Boolean && (Boolean) right.value()) { - return right; + Boolean value = (Boolean) result.value(); + if (value && operator.equals(ShortCircuitableOperators.LOGICAL_OR)) { + return true; } - // both false. + return !value && operator.equals(ShortCircuitableOperators.LOGICAL_AND); + } + + 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; + } + + 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 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); } 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; - } + throws CelEvaluationException { + IntermediateResult left; + IntermediateResult right; + if (celOptions.enableShortCircuiting()) { + left = evalBooleanNonstrict(frame, callExpr.args().get(0)); + if (canShortCircuit(left, ShortCircuitableOperators.LOGICAL_AND)) { + 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_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; + } } - // 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); @@ -525,58 +699,49 @@ private IntermediateResult evalNotStrictlyFalse(ExecutionFrame frame, CelCall ca } private IntermediateResult evalType(ExecutionFrame frame, CelCall callExpr) - throws InterpreterException { + throws CelEvaluationException { CelExpr typeExprArg = callExpr.args().get(0); IntermediateResult argResult = evalInternal(frame, typeExprArg); + // Type is a strict function. Early return if the argument is an error or an unknown. + if (isUnknownOrError(argResult.value())) { + return argResult; + } - 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); + CelType checkedType = getCheckedTypeOrThrow(typeExprArg); + CelType checkedTypeValue = typeResolver.adaptType(checkedType); + return IntermediateResult.create( + typeResolver.resolveObjectType(argResult.value(), checkedTypeValue)); } 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(); - } + throws CelEvaluationException { + return evalOptionalOrInternal(frame, callExpr, /* unwrapOptional= */ false); + } - Optional lhsOptionalValue = (Optional) lhsResult.value(); + private IntermediateResult evalOptionalOrValue(ExecutionFrame frame, CelCall callExpr) + throws CelEvaluationException { + return evalOptionalOrInternal(frame, callExpr, /* unwrapOptional= */ true); + } - if (lhsOptionalValue.isPresent()) { - // Short-circuit lhs if a value exists + private IntermediateResult evalOptionalOrInternal( + ExecutionFrame frame, CelCall callExpr, boolean unwrapOptional) + throws CelEvaluationException { + CelExpr lhsExpr = + callExpr + .target() + .orElseThrow( + () -> new IllegalStateException("Missing target for chained optional function")); + IntermediateResult lhsResult = evalInternal(frame, lhsExpr); + + if (isUnknownValue(lhsResult.value())) { return lhsResult; } - return evalInternal(frame, callExpr.args().get(0)); - } - - private IntermediateResult evalOptionalOrValue(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( + throw CelEvaluationExceptionBuilder.newBuilder( "expected optional value, found: %s", lhsResult.value()) .setErrorCode(CelErrorCode.INVALID_ARGUMENT) - .setLocation(metadata, lhsExpr.id()) + .setMetadata(metadata, lhsExpr.id()) .build(); } @@ -584,14 +749,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)) { @@ -614,15 +779,14 @@ private Optional maybeEvalOptionalSelectField( } 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(); } @@ -630,22 +794,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()); @@ -654,13 +816,14 @@ 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)) { Optional optionalVal = (Optional) value; if (!optionalVal.isPresent()) { continue; @@ -675,32 +838,30 @@ 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.id()) .build(); } Object value = valueResult.value(); - if (entry.optionalEntry()) { + if (entry.optionalEntry() && !isUnknownValue(value)) { Optional optionalVal = (Optional) value; if (!optionalVal.isPresent()) { // This is a no-op currently but will be semantically correct when extended proto @@ -717,25 +878,21 @@ 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(); @@ -754,11 +911,12 @@ private IntermediateResult evalStruct( fields.put(entry.fieldKey(), value); } - Optional unknowns = argChecker.maybeUnknowns(); - if (unknowns.isPresent()) { - return IntermediateResult.create(unknowns.get()); - } - return IntermediateResult.create(typeProvider.createMessage(reference.name(), fields)); + return argChecker + .maybeUnknowns() + .map(IntermediateResult::create) + .orElseGet( + () -> + IntermediateResult.create(typeProvider.createMessage(reference.name(), fields))); } // Evaluates the expression and returns a value-or-throwable. @@ -773,10 +931,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 { + throws CelEvaluationException { String accuVar = compre.accuVar(); String iterVar = compre.iterVar(); IntermediateResult iterRangeRaw = evalInternal(frame, compre.iterRange()); @@ -789,14 +973,22 @@ 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 = evalNonstrictly(frame, compre.accuInit()); + IntermediateResult accuValue; + if (LazyExpression.isLazilyEvaluable(compre)) { + 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(); @@ -805,16 +997,32 @@ private IntermediateResult evalComprehension( 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(); @@ -824,33 +1032,98 @@ private IntermediateResult evalComprehension( frame.popScope(); } - frame.pushScope(ImmutableMap.of(accuVar, accuValue)); - IntermediateResult result = evalInternal(frame, compre.result()); - frame.popScope(); + accuValue = maybeAdaptViewToList(accuValue); + + frame.pushScope(Collections.singletonMap(accuVar, accuValue)); + IntermediateResult result; + try { + result = evalInternal(frame, compre.result()); + } finally { + frame.popScope(); + } return result; } + + private IntermediateResult evalCelBlock( + 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. + blockList.put( + "@index" + index, + IntermediateResult.create(new LazyExpression(exprList.elements().get(index)))); + } + frame.pushScope(Collections.unmodifiableMap(blockList)); + + 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()); + } + } + + /** Contains a CelExpr that is to be lazily evaluated. */ + private static class LazyExpression { + private final CelExpr celExpr; + + /** + * Checks whether the provided expression can be evaluated lazily then cached. For example, the + * accumulator initializer in `cel.bind` macro is a good candidate because it never needs to be + * updated after being evaluated once. + */ + private static boolean isLazilyEvaluable(CelComprehension comprehension) { + // For now, just handle cel.bind. cel.block will be a future addition. + return comprehension + .loopCondition() + .constantOrDefault() + .getKind() + .equals(CelConstant.Kind.BOOLEAN_VALUE) + && !comprehension.loopCondition().constant().booleanValue() + && comprehension.iterVar().equals("#unused") + && comprehension.iterRange().exprKind().getKind().equals(ExprKind.Kind.LIST) + && comprehension.iterRange().list().elements().isEmpty(); + } + + private LazyExpression(CelExpr celExpr) { + this.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 RuntimeUnknownResolver currentResolver; private int iterations; + @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 Optional getEvaluationListener() { return evaluationListener; } @@ -858,12 +1131,20 @@ 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().findOverload(function, overloadIds, args); + } + return Optional.empty(); + } + + private void incrementIterations() throws CelEvaluationException { if (maxIterations < 0) { return; } if (++iterations > maxIterations) { - throw new InterpreterException.Builder( + throw CelEvaluationExceptionBuilder.newBuilder( String.format("Iteration budget exceeded: %d", maxIterations)) .setErrorCode(CelErrorCode.ITERATION_BUDGET_EXCEEDED) .build(); @@ -878,13 +1159,21 @@ private Optional resolveAttribute(CelAttribute attr) { return currentResolver.resolveAttribute(attr); } - private void pushScope(ImmutableMap scope) { + private void cacheLazilyEvaluatedResult( + String name, DefaultInterpreter.IntermediateResult result) { + currentResolver.cacheLazilyEvaluatedResult(name, result); + } + + /** 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..ab6934e01 100644 --- a/runtime/src/main/java/dev/cel/runtime/DescriptorMessageProvider.java +++ b/runtime/src/main/java/dev/cel/runtime/DescriptorMessageProvider.java @@ -14,10 +14,8 @@ 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; @@ -26,16 +24,15 @@ 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.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 +47,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,53 +62,18 @@ 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); + this.celOptions = celOptions; + this.protoAdapter = new ProtoAdapter(DynamicProto.create(protoMessageFactory), celOptions); } - @Nullable @Override - @Deprecated - /** {@inheritDoc} */ - public Value adaptType(@Nullable Type type) { - return typeResolver.adaptType(type); - } - - @Nullable - @Override - public Object createMessage(String messageName, Map values) { + public @Nullable Object createMessage(String messageName, Map values) { Message.Builder builder = protoMessageFactory .newBuilder(messageName) @@ -137,34 +99,37 @@ 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 new CelRuntimeException( + new IllegalArgumentException( + String.format("key '%s' is not present in map.", fieldName)), + CelErrorCode.ATTRIBUTE_NOT_FOUND); } - 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)) { @@ -176,10 +141,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 +168,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; @@ -224,16 +194,16 @@ private FieldDescriptor findField(Descriptor descriptor, String fieldName) { return fieldDescriptor; } - 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. + // This can happen when the field selection is done on dyn, and it is not a message. throw new CelRuntimeException( - new IllegalStateException( + new IllegalArgumentException( String.format( - "[internal] expected an instance of 'com.google.protobuf.MessageOrBuilder' " - + "but found '%s'", - candidate.getClass().getName())), - CelErrorCode.INTERNAL_ERROR); + "Error resolving field '%s'. Field selections must be performed on messages or" + + " maps.", + fieldName)), + CelErrorCode.ATTRIBUTE_NOT_FOUND); } 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..46ad0d231 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/DescriptorTypeResolver.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.runtime; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.errorprone.annotations.Immutable; +import com.google.protobuf.MessageOrBuilder; +import dev.cel.common.types.CelType; +import dev.cel.common.types.StructTypeReference; +import dev.cel.common.types.TypeType; +import java.util.Optional; + +/** + * {@code DescriptorTypeResolver} extends {@link TypeResolver} and additionally resolves incoming + * protobuf message types using descriptors. + */ +@Immutable +final class DescriptorTypeResolver extends TypeResolver { + + static DescriptorTypeResolver create() { + return new DescriptorTypeResolver(); + } + + @Override + 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; + return TypeType.create(StructTypeReference.create(msg.getDescriptorForType().getFullName())); + } + + return super.resolveObjectType(obj, typeCheckedType); + } + + private DescriptorTypeResolver() {} +} diff --git a/runtime/src/main/java/dev/cel/runtime/Dispatcher.java b/runtime/src/main/java/dev/cel/runtime/Dispatcher.java index fcaa087aa..017e76685 100644 --- a/runtime/src/main/java/dev/cel/runtime/Dispatcher.java +++ b/runtime/src/main/java/dev/cel/runtime/Dispatcher.java @@ -17,7 +17,6 @@ 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. @@ -26,24 +25,7 @@ */ @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; +interface Dispatcher extends FunctionResolver { /** * Returns an {@link ImmutableCopy} from current instance. 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..501608571 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/FunctionBindingImpl.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; + +import com.google.common.collect.ImmutableList; +import com.google.errorprone.annotations.Immutable; + +@Immutable +final class FunctionBindingImpl implements CelFunctionBinding { + + private final String overloadId; + + private final ImmutableList> argTypes; + + private final CelFunctionOverload definition; + + @Override + public String getOverloadId() { + return overloadId; + } + + @Override + public ImmutableList> getArgTypes() { + return argTypes; + } + + @Override + public CelFunctionOverload getDefinition() { + return definition; + } + + FunctionBindingImpl( + String overloadId, ImmutableList> argTypes, CelFunctionOverload definition) { + this.overloadId = overloadId; + this.argTypes = argTypes; + this.definition = definition; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/FunctionOverload.java b/runtime/src/main/java/dev/cel/runtime/FunctionOverload.java new file mode 100644 index 000000000..bd9a8054c --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/FunctionOverload.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.runtime; + +import com.google.errorprone.annotations.Immutable; + +/** Interface describing the general signature of all CEL custom function implementations. */ +@SuppressWarnings("AndroidJdkLibsChecker") // FunctionalInterface added in 24 +@FunctionalInterface +@Immutable +interface FunctionOverload { + + /** 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. + */ + @Immutable + @FunctionalInterface + interface Unary { + Object apply(T arg) throws CelEvaluationException; + } + + /** + * Helper interface for describing binary functions where the type parameters are used to improve + * compile-time correctness of function bindings. + */ + @Immutable + @FunctionalInterface + interface Binary { + Object apply(T1 arg1, T2 arg2) throws CelEvaluationException; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/FunctionResolver.java b/runtime/src/main/java/dev/cel/runtime/FunctionResolver.java new file mode 100644 index 000000000..888780901 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/FunctionResolver.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 javax.annotation.concurrent.ThreadSafe; +import dev.cel.common.annotations.Internal; +import java.util.List; +import java.util.Optional; + +/** + * Interface to a resolver for CEL functions based on the function name, overload ids, and + * arguments. + * + *

CEL Library Internals. Do Not Use. + */ +@ThreadSafe +@Internal +interface FunctionResolver { + + /** + * 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 findOverload( + String functionName, List overloadIds, Object[] args) throws CelEvaluationException; +} diff --git a/runtime/src/main/java/dev/cel/runtime/GlobalResolver.java b/runtime/src/main/java/dev/cel/runtime/GlobalResolver.java index 2cc1a7d31..ce8dc8b3f 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 @@ -23,10 +23,25 @@ * *

CEL Library Internals. Do Not Use. */ +@SuppressWarnings("AndroidJdkLibsChecker") // FunctionalInterface added in 24 @FunctionalInterface @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/Interpretable.java b/runtime/src/main/java/dev/cel/runtime/Interpretable.java index 0c967416a..21e95921d 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, FunctionResolver 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, + FunctionResolver 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 04b0968dc..000000000 --- a/runtime/src/main/java/dev/cel/runtime/InterpreterException.java +++ /dev/null @@ -1,106 +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 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 = args.length > 0 ? String.format(message, args) : message; - } - - @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 = args.length > 0 ? String.format(message, args) : message; - } - - @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..f84897ac2 100644 --- a/runtime/src/main/java/dev/cel/runtime/InterpreterUtil.java +++ b/runtime/src/main/java/dev/cel/runtime/InterpreterUtil.java @@ -14,16 +14,9 @@ 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.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 +31,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 +46,37 @@ 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; + 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} */ + static Object maybeAdaptToAccumulatedUnknowns(Object val) { + if (!(val instanceof CelUnknownSet)) { + return val; } - unknownsetBuilder.addAllExprs(ids); - return ExprValue.newBuilder().setUnknown(unknownsetBuilder).build(); - } - /** 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)); + return adaptToAccumulatedUnknowns((CelUnknownSet) val); } - /** - * 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(); + static AccumulatedUnknowns adaptToAccumulatedUnknowns(CelUnknownSet unknowns) { + return AccumulatedUnknowns.create(unknowns.unknownExprIds(), unknowns.attributes()); } /** - * 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 +91,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..3002f7c7c --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/LiteProgramImpl.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; + +import com.google.auto.value.AutoValue; +import com.google.errorprone.annotations.Immutable; +import java.util.Map; + +@Immutable +@AutoValue +abstract class LiteProgramImpl implements CelLiteRuntime.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); + } + + static CelLiteRuntime.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..152c96160 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/LiteRuntimeImpl.java @@ -0,0 +1,234 @@ +// 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."); + } + if (!celOptions.enableStringConcatenation()) { + throw new IllegalArgumentException( + prefix + + "enableStringConcatenation cannot be disabled. Subset the environment instead" + + " using setStandardFunctions method."); + } + if (!celOptions.enableStringConversion()) { + throw new IllegalArgumentException( + prefix + + "enableStringConversion cannot be disabled. Subset the environment instead using" + + " setStandardFunctions method."); + } + if (!celOptions.enableListConcatenation()) { + throw new IllegalArgumentException( + prefix + + "enableListConcatenation cannot be disabled. Subset the environment instead using" + + " setStandardFunctions method."); + } + } + + @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 dispatcher = DefaultDispatcher.create(); + functionBindingsBuilder + .buildOrThrow() + .forEach( + (String overloadId, CelFunctionBinding func) -> + dispatcher.add( + overloadId, func.getArgTypes(), (args) -> func.getDefinition().apply(args))); + + Interpreter interpreter = + new DefaultInterpreter( + TypeResolver.create(), + CelValueRuntimeTypeProvider.newInstance(celValueProvider), + dispatcher, + celOptions); + + return new LiteRuntimeImpl( + interpreter, + celOptions, + customFunctionBindings.values(), + standardFunctions, + runtimeLibs, + celValueProvider); + } + + private Builder() { + this.celOptions = + CelOptions.current() + .enableCelValue(true) + .evaluateCanonicalTypesToNativeValues(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/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/ProgramImpl.java b/runtime/src/main/java/dev/cel/runtime/ProgramImpl.java new file mode 100644 index 000000000..d0e64429b --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/ProgramImpl.java @@ -0,0 +1,184 @@ +// 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 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 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..3a3afa1d0 100644 --- a/runtime/src/main/java/dev/cel/runtime/Registrar.java +++ b/runtime/src/main/java/dev/cel/runtime/Registrar.java @@ -14,7 +14,6 @@ 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; @@ -27,33 +26,23 @@ @Internal 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 FunctionOverload {} /** - * 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 FunctionOverload.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 FunctionOverload.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/ResolvedOverload.java b/runtime/src/main/java/dev/cel/runtime/ResolvedOverload.java new file mode 100644 index 000000000..f71748814 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/ResolvedOverload.java @@ -0,0 +1,64 @@ +// 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.errorprone.annotations.Immutable; +import com.google.protobuf.MessageLite; +import java.util.List; +import java.util.Map; + +/** + * Representation of a function overload which has been resolved to a specific set of argument types + * and a function definition. + */ +@Immutable +interface ResolvedOverload { + + /** The overload id of the function. */ + String getOverloadId(); + + /** The types of the function parameters. */ + List> getParameterTypes(); + + /** The function definition. */ + FunctionOverload getDefinition(); + + /** + * Returns true if the overload's expected argument types match the types of the given arguments. + */ + default boolean canHandle(Object[] arguments) { + List> parameterTypes = getParameterTypes(); + 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; + } + continue; + } + if (!paramType.isAssignableFrom(arg.getClass())) { + return false; + } + } + return true; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/RuntimeEquality.java b/runtime/src/main/java/dev/cel/runtime/RuntimeEquality.java index 517fb1729..d0b2fcd6b 100644 --- a/runtime/src/main/java/dev/cel/runtime/RuntimeEquality.java +++ b/runtime/src/main/java/dev/cel/runtime/RuntimeEquality.java @@ -16,54 +16,53 @@ import com.google.common.primitives.UnsignedLong; import com.google.errorprone.annotations.Immutable; -import com.google.protobuf.Message; -import com.google.protobuf.MessageOrBuilder; +import com.google.protobuf.MessageLiteOrBuilder; 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.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(); @@ -73,17 +72,17 @@ public B indexMap(Map map, A index, CelOptions celOptions) { } /** 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 +134,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 +153,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 +175,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 +200,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) { @@ -245,4 +241,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..37247f3d3 100644 --- a/runtime/src/main/java/dev/cel/runtime/RuntimeHelpers.java +++ b/runtime/src/main/java/dev/cel/runtime/RuntimeHelpers.java @@ -17,21 +17,18 @@ 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.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,17 +40,23 @@ * *

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. */ + @SuppressWarnings("AndroidJdkLibsChecker") // DateTimeParseException added in 26 public static Duration createDurationFromString(String d) { try { java.time.Duration dv = AmountFormats.parseUnitBasedDuration(d); @@ -67,29 +70,33 @@ public static Duration createDurationFromString(String d) { } } - /** 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); @@ -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); @@ -220,7 +227,7 @@ public static long uint64Divide(long x, long y, CelOptions celOptions) { } } - 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); @@ -252,7 +259,7 @@ public static UnsignedLong uint64Mod(UnsignedLong x, UnsignedLong y) { 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); @@ -269,7 +276,7 @@ public static long uint64Multiply(long x, long y, CelOptions celOptions) { 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); @@ -303,9 +310,6 @@ public static UnsignedLong uint64Subtract(UnsignedLong x, UnsignedLong y) { return x.minus(y); } - // Object equality - // =================== - // Proto Type Adaption // =================== @@ -314,36 +318,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; + static final Converter INT32_TO_INT64 = Integer::longValue; - public static final Converter UINT32_TO_UINT64 = UnsignedInts::toLong; + static final Converter FLOAT_TO_DOUBLE = Float::doubleValue; - public static final Converter FLOAT_TO_DOUBLE = Float::doubleValue; + static final Converter INT64_TO_INT32 = Ints::checkedCast; - public 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 +372,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 +399,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 e68270cad..e9fb9d052 100644 --- a/runtime/src/main/java/dev/cel/runtime/RuntimeUnknownResolver.java +++ b/runtime/src/main/java/dev/cel/runtime/RuntimeUnknownResolver.java @@ -16,6 +16,7 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import dev.cel.common.annotations.Internal; +import java.util.HashMap; import java.util.Map; import java.util.Optional; @@ -33,6 +34,7 @@ public class RuntimeUnknownResolver { /** The underlying resolver for known values. */ private final GlobalResolver resolver; + /** Resolver for unknown and resolved attributes. */ private final CelAttributeResolver attributeResolver; @@ -51,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() { @@ -64,7 +66,7 @@ public static class Builder { private GlobalResolver resolver; private Builder() { - resolver = Activation.EMPTY; + resolver = GlobalResolver.EMPTY; attributeResolver = DEFAULT_RESOLVER; } @@ -89,8 +91,9 @@ 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. */ @@ -100,7 +103,7 @@ DefaultInterpreter.IntermediateResult resolveSimpleName(String name, Long exprId 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()); } @@ -112,12 +115,17 @@ DefaultInterpreter.IntermediateResult resolveSimpleName(String name, Long exprId attr, InterpreterUtil.valueOrUnknown(result, exprId)); } + void cacheLazilyEvaluatedResult(String name, DefaultInterpreter.IntermediateResult result) { + // no-op. Caching is handled in ScopedResolver. + } + /** * Attempt to resolve an attribute bound to a context variable. This is used to shadow lazily * 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) { @@ -127,6 +135,7 @@ ScopedResolver withScope(Map vars static final class ScopedResolver extends RuntimeUnknownResolver { private final RuntimeUnknownResolver parent; private final Map shadowedVars; + private final Map lazyEvalResultCache; private ScopedResolver( RuntimeUnknownResolver parent, @@ -134,16 +143,46 @@ private ScopedResolver( super(parent.resolver, parent.attributeResolver, parent.attributeTrackingEnabled); this.parent = parent; this.shadowedVars = shadowedVars; + this.lazyEvalResultCache = new HashMap<>(); } @Override DefaultInterpreter.IntermediateResult resolveSimpleName(String name, Long exprId) { - DefaultInterpreter.IntermediateResult shadowed = shadowedVars.get(name); - if (shadowed != null) { - return shadowed; + DefaultInterpreter.IntermediateResult result = lazyEvalResultCache.get(name); + if (result != null) { + return copyIfMutable(result); + } + result = shadowedVars.get(name); + if (result != null) { + return result; } return parent.resolveSimpleName(name, exprId); } + + @Override + void cacheLazilyEvaluatedResult(String name, DefaultInterpreter.IntermediateResult result) { + lazyEvalResultCache.put(name, copyIfMutable(result)); + } + + /** + * 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; + } } /** Null implementation for attribute resolution. */ 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..b9cc42cc1 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,169 @@ 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 dev.cel.common.annotations.Internal; +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.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 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. - * - *

CEL Library Internals. Do Not Use. + * {@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.) */ @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); +class TypeResolver { + + static TypeResolver create() { + return new TypeResolver(); + } + + // 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(Duration.class, TypeType.create(SimpleType.DURATION)) + .put(Timestamp.class, TypeType.create(SimpleType.TIMESTAMP)) + .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}. */ + TypeType resolveObjectType(Object obj, CelType typeCheckedType) { + checkNotNull(obj); + Optional wellKnownTypeType = resolveWellKnownObjectType(obj); + if (wellKnownTypeType.isPresent()) { + return wellKnownTypeType.get(); + } + + 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; + } + + TypeResolver() {} } 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..9a1a4964a 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..245dba691 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, startingUnknownContext, 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/standard/AddOperator.java b/runtime/src/main/java/dev/cel/runtime/standard/AddOperator.java new file mode 100644 index 000000000..9d75b15ce --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/AddOperator.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.runtime.standard; + +import static dev.cel.runtime.standard.ArithmeticHelpers.getArithmeticErrorCode; + +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.CelRuntimeException; +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.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 CelRuntimeException(e, getArithmeticErrorCode(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 CelRuntimeException(e, getArithmeticErrorCode(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 CelRuntimeException(e, getArithmeticErrorCode(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) -> + CelFunctionBinding.from( + "add_duration_duration", Duration.class, Duration.class, ProtoTimeUtils::add)), + ADD_TIMESTAMP_DURATION( + (celOptions, runtimeEquality) -> + 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) -> + CelFunctionBinding.from( + "add_duration_timestamp", + Duration.class, + Timestamp.class, + (Duration x, Timestamp y) -> ProtoTimeUtils.add(y, x))), + @SuppressWarnings({"unchecked"}) + ADD_LIST( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from("add_list", List.class, List.class, RuntimeHelpers::concat)); + + private final FunctionBindingCreator bindingCreator; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + AddOverload(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + private AddOperator(ImmutableSet overloads) { + super(overloads); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/standard/ArithmeticHelpers.java b/runtime/src/main/java/dev/cel/runtime/standard/ArithmeticHelpers.java new file mode 100644 index 000000000..310e9401f --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/ArithmeticHelpers.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.runtime.standard; + +import dev.cel.common.CelErrorCode; + +final class ArithmeticHelpers { + + 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 ArithmeticHelpers() {} +} 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..bac80475d --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/BUILD.bazel @@ -0,0 +1,1468 @@ +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 = [ + ":arithmetic_helpers", + ":standard_function", + ":standard_overload", + "//common:options", + "//common:runtime_exception", + "//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 = [ + ":arithmetic_helpers", + ":standard_function_android", + ":standard_overload_android", + "//common:options", + "//common:runtime_exception", + "//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 = [ + ":arithmetic_helpers", + ":standard_function", + ":standard_overload", + "//common:options", + "//common:runtime_exception", + "//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 = [ + ":arithmetic_helpers", + ":standard_function_android", + ":standard_overload_android", + "//common:options", + "//common:runtime_exception", + "//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:error_codes", + "//common:options", + "//common:runtime_exception", + "//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:error_codes", + "//common:options", + "//common:runtime_exception", + "//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:error_codes", + "//common:options", + "//common:runtime_exception", + "//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:error_codes", + "//common:options", + "//common:runtime_exception", + "//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:error_codes", + "//common:options", + "//common:runtime_exception", + "//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:error_codes", + "//common:options", + "//common:runtime_exception", + "//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: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: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 = [ + ":date_time_helpers", + ":standard_overload", + "//common:options", + "//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 = [ + ":date_time_helpers_android", + ":standard_function_android", + ":standard_overload_android", + "//common:options", + "//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 = [ + ":date_time_helpers", + ":standard_overload", + "//common:options", + "//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 = [ + ":date_time_helpers_android", + ":standard_function_android", + ":standard_overload_android", + "//common:options", + "//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 = [ + ":date_time_helpers", + ":standard_overload", + "//common:options", + "//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 = [ + ":date_time_helpers_android", + ":standard_function_android", + ":standard_overload_android", + "//common:options", + "//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 = [ + ":date_time_helpers", + ":standard_overload", + "//common:options", + "//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 = [ + ":date_time_helpers_android", + ":standard_function_android", + ":standard_overload_android", + "//common:options", + "//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 = [ + ":date_time_helpers", + ":standard_overload", + "//common:options", + "//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 = [ + ":date_time_helpers_android", + ":standard_function_android", + ":standard_overload_android", + "//common:options", + "//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 = [ + ":date_time_helpers", + ":standard_overload", + "//common:options", + "//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 = [ + ":date_time_helpers_android", + ":standard_function_android", + ":standard_overload_android", + "//common:options", + "//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 = [ + ":date_time_helpers", + ":standard_overload", + "//common:options", + "//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 = [ + ":date_time_helpers_android", + ":standard_function_android", + ":standard_overload_android", + "//common:options", + "//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 = [ + ":date_time_helpers", + ":standard_overload", + "//common:options", + "//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 = [ + ":date_time_helpers_android", + ":standard_function_android", + ":standard_overload_android", + "//common:options", + "//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 = [ + ":date_time_helpers", + ":standard_overload", + "//common:options", + "//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 = [ + ":date_time_helpers_android", + ":standard_function_android", + ":standard_overload_android", + "//common:options", + "//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 = [ + ":date_time_helpers", + ":standard_overload", + "//common:options", + "//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 = [ + ":date_time_helpers_android", + ":standard_function_android", + ":standard_overload_android", + "//common:options", + "//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: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: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: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: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: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: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: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: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:error_codes", + "//common:options", + "//common:runtime_exception", + "//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:error_codes", + "//common:options", + "//common:runtime_exception", + "//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: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: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: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: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: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: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:error_codes", + "//common:options", + "//common:runtime_exception", + "//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:error_codes", + "//common:options", + "//common:runtime_exception", + "//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 = [ + ":arithmetic_helpers", + ":standard_overload", + "//common:options", + "//common:runtime_exception", + "//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 = [ + ":arithmetic_helpers", + ":standard_function_android", + ":standard_overload_android", + "//common:options", + "//common:runtime_exception", + "//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 = [ + ":arithmetic_helpers", + ":standard_overload", + "//common:options", + "//common:runtime_exception", + "//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 = [ + ":arithmetic_helpers", + ":standard_function_android", + ":standard_overload_android", + "//common:options", + "//common:runtime_exception", + "//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 = [ + ":arithmetic_helpers", + ":standard_overload", + "//common:options", + "//common:runtime_exception", + "//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 = [ + ":arithmetic_helpers", + ":standard_function_android", + ":standard_overload_android", + "//common:options", + "//common:runtime_exception", + "//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 = [ + ":arithmetic_helpers", + ":standard_overload", + "//common:options", + "//common:runtime_exception", + "//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 = [ + ":arithmetic_helpers", + ":standard_function_android", + ":standard_overload_android", + "//common:options", + "//common:runtime_exception", + "//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: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: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:error_codes", + "//common:options", + "//common:runtime_exception", + "//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:error_codes", + "//common:options", + "//common:runtime_exception", + "//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:error_codes", + "//common:options", + "//common:runtime_exception", + "//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:error_codes", + "//common:options", + "//common:runtime_exception", + "//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 = "uint", + srcs = ["UintFunction.java"], + tags = [ + ], + deps = [ + ":standard_overload", + "//common:error_codes", + "//common:options", + "//common:runtime_exception", + "//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:error_codes", + "//common:options", + "//common:runtime_exception", + "//runtime:function_binding_android", + "//runtime:runtime_equality_android", + "//runtime:runtime_helpers_android", + "@maven_android//:com_google_guava_guava", + ], +) + +java_library( + name = "standard_overload", + srcs = ["CelStandardOverload.java"], + visibility = ["//visibility:private"], + 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"], + visibility = ["//visibility:private"], + deps = [ + "//common:options", + "//runtime:function_binding_android", + "//runtime:runtime_equality_android", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +java_library( + name = "arithmetic_helpers", + srcs = ["ArithmeticHelpers.java"], + # used_by_android + visibility = ["//visibility:private"], + deps = [ + "//common:error_codes", + ], +) + +java_library( + name = "date_time_helpers", + srcs = ["DateTimeHelpers.java"], + visibility = ["//visibility:private"], + deps = [ + "//common:error_codes", + "//common:runtime_exception", + "@maven//:com_google_protobuf_protobuf_java", + ], +) + +cel_android_library( + name = "date_time_helpers_android", + srcs = ["DateTimeHelpers.java"], + visibility = ["//visibility:private"], + deps = [ + "//common:error_codes", + "//common:runtime_exception", + "@maven_android//:com_google_protobuf_protobuf_javalite", + ], +) 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..519755d18 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/BoolFunction.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 dev.cel.common.CelErrorCode; +import dev.cel.common.CelOptions; +import dev.cel.common.CelRuntimeException; +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 CelRuntimeException( + new IllegalArgumentException( + SafeStringFormatter.format( + "Type conversion error from 'string' to 'bool': [%s]", str)), + CelErrorCode.BAD_FORMAT); + } + })); + + private final FunctionBindingCreator bindingCreator; + ; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + BoolOverload(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + private BoolFunction(ImmutableSet overloads) { + super(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..b45d6d65b --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/BytesFunction.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.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) -> + CelFunctionBinding.from("string_to_bytes", String.class, CelByteString::copyFromUtf8)), + ; + + private final FunctionBindingCreator bindingCreator; + ; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + BytesOverload(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + private BytesFunction(ImmutableSet overloads) { + super(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..bde6fa6b5 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/CelStandardFunction.java @@ -0,0 +1,47 @@ +// 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.checkState; + +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 ImmutableSet overloads; + + public ImmutableSet newFunctionBindings( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + ImmutableSet.Builder builder = ImmutableSet.builder(); + for (CelStandardOverload overload : overloads) { + builder.add(overload.newFunctionBinding(celOptions, runtimeEquality)); + } + + return builder.build(); + } + + CelStandardFunction(ImmutableSet overloads) { + checkState(!overloads.isEmpty(), "At least 1 overload must be provided."); + this.overloads = overloads; + } +} 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..25555c13e --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/CelStandardOverload.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.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 +interface CelStandardOverload { + + CelFunctionBinding newFunctionBinding(CelOptions celOptions, RuntimeEquality runtimeEquality); + + @SuppressWarnings("AndroidJdkLibsChecker") // FunctionalInterface added in 24 + @FunctionalInterface + @Immutable + interface FunctionBindingCreator { + CelFunctionBinding create(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..fb4aa37f1 --- /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 FunctionBindingCreator bindingCreator; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + ContainsOverload(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + private ContainsFunction(ImmutableSet overloads) { + super(overloads); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/standard/DateTimeHelpers.java b/runtime/src/main/java/dev/cel/runtime/standard/DateTimeHelpers.java new file mode 100644 index 000000000..d05125ed1 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/DateTimeHelpers.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.runtime.standard; + +import com.google.protobuf.Timestamp; +import dev.cel.common.CelErrorCode; +import dev.cel.common.CelRuntimeException; +import java.time.DateTimeException; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Locale; + +final class DateTimeHelpers { + static final String UTC = "UTC"; + + /** + * 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. + */ + static LocalDateTime newLocalDateTime(Timestamp ts, String tz) { + return Instant.ofEpochSecond(ts.getSeconds(), ts.getNanos()) + .atZone(timeZone(tz)) + .toLocalDateTime(); + } + + /** + * 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 CelRuntimeException(e, CelErrorCode.BAD_FORMAT); + } + + 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.getDefault(), "%02d:%02d", Math.abs(hourOffset), minOffset); + + return ZoneId.of(formattedOffset); + + } catch (DateTimeException e2) { + throw new CelRuntimeException(e2, CelErrorCode.BAD_FORMAT); + } + } + } + + private DateTimeHelpers() {} +} 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..23556d0f1 --- /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.runtime.standard.ArithmeticHelpers.getArithmeticErrorCode; + +import com.google.common.collect.ImmutableSet; +import com.google.common.primitives.UnsignedLong; +import dev.cel.common.CelOptions; +import dev.cel.common.CelRuntimeException; +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 CelRuntimeException(e, getArithmeticErrorCode(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 FunctionBindingCreator bindingCreator; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + DivideOverload(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + private DivideOperator(ImmutableSet overloads) { + super(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..1569a0c0b --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/DoubleFunction.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.common.primitives.UnsignedLong; +import dev.cel.common.CelErrorCode; +import dev.cel.common.CelOptions; +import dev.cel.common.CelRuntimeException; +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 CelRuntimeException(e, CelErrorCode.BAD_FORMAT); + } + })), + 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 FunctionBindingCreator bindingCreator; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + DoubleOverload(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + private DoubleFunction(ImmutableSet overloads) { + super(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..d54e2999b --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/DurationFunction.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.standard; + +import com.google.common.collect.ImmutableSet; +import com.google.protobuf.Duration; +import dev.cel.common.CelErrorCode; +import dev.cel.common.CelOptions; +import dev.cel.common.CelRuntimeException; +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) -> + CelFunctionBinding.from("duration_to_duration", Duration.class, (Duration x) -> x)), + STRING_TO_DURATION( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "string_to_duration", + String.class, + (String d) -> { + try { + return RuntimeHelpers.createDurationFromString(d); + } catch (IllegalArgumentException e) { + throw new CelRuntimeException(e, CelErrorCode.BAD_FORMAT); + } + })), + ; + + private final FunctionBindingCreator bindingCreator; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + DurationOverload(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + private DurationFunction(ImmutableSet overloads) { + super(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..bfaada85c --- /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 FunctionBindingCreator bindingCreator; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + DynOverload(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + private DynFunction(ImmutableSet overloads) { + super(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..048807a10 --- /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 FunctionBindingCreator bindingCreator; + ; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + EndsWithOverload(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + private EndsWithFunction(ImmutableSet overloads) { + super(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..192ffd749 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/EqualsOperator.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 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 FunctionBindingCreator bindingCreator; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + EqualsOverload(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + private EqualsOperator(ImmutableSet overloads) { + super(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..bd48ec7da --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/GetDateFunction.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.standard; + +import static dev.cel.runtime.standard.DateTimeHelpers.UTC; +import static dev.cel.runtime.standard.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.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) -> + 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) -> + 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 FunctionBindingCreator bindingCreator; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + GetDateOverload(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + private GetDateFunction(ImmutableSet overloads) { + super(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..171353c04 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/GetDayOfMonthFunction.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.runtime.standard.DateTimeHelpers.UTC; +import static dev.cel.runtime.standard.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.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) -> + 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) -> + 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 FunctionBindingCreator bindingCreator; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + GetDayOfMonthOverload(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + private GetDayOfMonthFunction(ImmutableSet overloads) { + super(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..92ef90e79 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/GetDayOfWeekFunction.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.runtime.standard; + +import static dev.cel.runtime.standard.DateTimeHelpers.UTC; +import static dev.cel.runtime.standard.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.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) -> + 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) -> + 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 FunctionBindingCreator bindingCreator; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + GetDayOfWeekOverload(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + private GetDayOfWeekFunction(ImmutableSet overloads) { + super(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..8c4d90e64 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/GetDayOfYearFunction.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.standard; + +import static dev.cel.runtime.standard.DateTimeHelpers.UTC; +import static dev.cel.runtime.standard.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.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) -> + 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) -> + 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 FunctionBindingCreator bindingCreator; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + GetDayOfYearOverload(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + private GetDayOfYearFunction(ImmutableSet overloads) { + super(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..61fe189cb --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/GetFullYearFunction.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.runtime.standard.DateTimeHelpers.UTC; +import static dev.cel.runtime.standard.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.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) -> + CelFunctionBinding.from( + "timestamp_to_year", + Timestamp.class, + (Timestamp ts) -> (long) newLocalDateTime(ts, UTC).getYear())), + TIMESTAMP_TO_YEAR_WITH_TZ( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "timestamp_to_year_with_tz", + Timestamp.class, + String.class, + (Timestamp ts, String tz) -> (long) newLocalDateTime(ts, tz).getYear())), + ; + + private final FunctionBindingCreator bindingCreator; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + GetFullYearOverload(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + private GetFullYearFunction(ImmutableSet overloads) { + super(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..ee36bfbb0 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/GetHoursFunction.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 static dev.cel.runtime.standard.DateTimeHelpers.UTC; +import static dev.cel.runtime.standard.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.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) -> + CelFunctionBinding.from( + "timestamp_to_hours", + Timestamp.class, + (Timestamp ts) -> (long) newLocalDateTime(ts, UTC).getHour())), + TIMESTAMP_TO_HOURS_WITH_TZ( + (celOptions, runtimeEquality) -> + 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) -> + CelFunctionBinding.from("duration_to_hours", Duration.class, ProtoTimeUtils::toHours)), + ; + + private final FunctionBindingCreator bindingCreator; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + GetHoursOverload(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + private GetHoursFunction(ImmutableSet overloads) { + super(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..369cd9ea2 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/GetMillisecondsFunction.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 static dev.cel.runtime.standard.DateTimeHelpers.UTC; +import static dev.cel.runtime.standard.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.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) -> + 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) -> + 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) -> + CelFunctionBinding.from( + "duration_to_milliseconds", + Duration.class, + (Duration arg) -> + ProtoTimeUtils.toMillis(arg) % java.time.Duration.ofSeconds(1).toMillis())); + + private final FunctionBindingCreator bindingCreator; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + GetMillisecondsOverload(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + private GetMillisecondsFunction(ImmutableSet overloads) { + super(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..24c285c46 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/GetMinutesFunction.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.runtime.standard; + +import static dev.cel.runtime.standard.DateTimeHelpers.UTC; +import static dev.cel.runtime.standard.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.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) -> + CelFunctionBinding.from( + "timestamp_to_minutes", + Timestamp.class, + (Timestamp ts) -> (long) newLocalDateTime(ts, UTC).getMinute())), + TIMESTAMP_TO_MINUTES_WITH_TZ( + (celOptions, runtimeEquality) -> + 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) -> + CelFunctionBinding.from( + "duration_to_minutes", Duration.class, ProtoTimeUtils::toMinutes)), + ; + + private final FunctionBindingCreator bindingCreator; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + GetMinutesOverload(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + private GetMinutesFunction(ImmutableSet overloads) { + super(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..34a33815d --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/GetMonthFunction.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.standard; + +import static dev.cel.runtime.standard.DateTimeHelpers.UTC; +import static dev.cel.runtime.standard.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.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) -> + CelFunctionBinding.from( + "timestamp_to_month", + Timestamp.class, + (Timestamp ts) -> (long) newLocalDateTime(ts, UTC).getMonthValue() - 1)), + TIMESTAMP_TO_MONTH_WITH_TZ( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "timestamp_to_month_with_tz", + Timestamp.class, + String.class, + (Timestamp ts, String tz) -> (long) newLocalDateTime(ts, tz).getMonthValue() - 1)), + ; + + private final FunctionBindingCreator bindingCreator; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + GetMonthOverload(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + private GetMonthFunction(ImmutableSet overloads) { + super(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..e40fc62c5 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/GetSecondsFunction.java @@ -0,0 +1,84 @@ +// 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.runtime.standard.DateTimeHelpers.UTC; +import static dev.cel.runtime.standard.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.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) -> + CelFunctionBinding.from( + "timestamp_to_seconds", + Timestamp.class, + (Timestamp ts) -> (long) newLocalDateTime(ts, UTC).getSecond())), + TIMESTAMP_TO_SECONDS_WITH_TZ( + (celOptions, runtimeEquality) -> + 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) -> + CelFunctionBinding.from( + "duration_to_seconds", Duration.class, ProtoTimeUtils::toSeconds)), + ; + + private final FunctionBindingCreator bindingCreator; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + GetSecondsOverload(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + private GetSecondsFunction(ImmutableSet overloads) { + super(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..1eaced251 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/GreaterEqualsOperator.java @@ -0,0 +1,183 @@ +// 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.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.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) -> + CelFunctionBinding.from( + "greater_equals_duration", + Duration.class, + Duration.class, + (Duration x, Duration y) -> ProtoTimeUtils.compare(x, y) >= 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) -> + CelFunctionBinding.from( + "greater_equals_timestamp", + Timestamp.class, + Timestamp.class, + (Timestamp x, Timestamp y) -> ProtoTimeUtils.compare(x, y) >= 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 FunctionBindingCreator bindingCreator; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + GreaterEqualsOverload(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + private GreaterEqualsOperator(ImmutableSet overloads) { + super(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..9eed5747b --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/GreaterOperator.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.runtime.standard; + +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.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) -> + CelFunctionBinding.from( + "greater_duration", + Duration.class, + Duration.class, + (Duration x, Duration y) -> ProtoTimeUtils.compare(x, y) > 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) -> + CelFunctionBinding.from( + "greater_timestamp", + Timestamp.class, + Timestamp.class, + (Timestamp x, Timestamp y) -> ProtoTimeUtils.compare(x, y) > 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 FunctionBindingCreator bindingCreator; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + GreaterOverload(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + private GreaterOperator(ImmutableSet overloads) { + super(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..9b4e3986c --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/InOperator.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.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; +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 FunctionBindingCreator bindingCreator; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + InOverload(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + private InOperator(ImmutableSet overloads) { + super(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..f94fd2807 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/IndexOperator.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 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 FunctionBindingCreator bindingCreator; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + IndexOverload(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + private IndexOperator(ImmutableSet overloads) { + super(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..b29ca37a6 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/IntFunction.java @@ -0,0 +1,128 @@ +// 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.CelErrorCode; +import dev.cel.common.CelOptions; +import dev.cel.common.CelRuntimeException; +import dev.cel.common.internal.ProtoTimeUtils; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.RuntimeEquality; +import dev.cel.runtime.RuntimeHelpers; +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 CelRuntimeException( + new IllegalArgumentException("unsigned out of int range"), + CelErrorCode.NUMERIC_OVERFLOW); + } + return arg.longValue(); + }); + } else { + return CelFunctionBinding.from( + "uint64_to_int64", + Long.class, + (Long arg) -> { + if (celOptions.errorOnIntWrap() && arg < 0) { + throw new CelRuntimeException( + new IllegalArgumentException("unsigned out of int range"), + CelErrorCode.NUMERIC_OVERFLOW); + } + 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 CelRuntimeException( + new IllegalArgumentException("double is out of range for int"), + CelErrorCode.NUMERIC_OVERFLOW)); + } + 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 CelRuntimeException(e, CelErrorCode.BAD_FORMAT); + } + })), + TIMESTAMP_TO_INT64( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "timestamp_to_int64", Timestamp.class, ProtoTimeUtils::toSeconds)), + ; + + private final FunctionBindingCreator bindingCreator; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + IntOverload(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + private IntFunction(ImmutableSet overloads) { + super(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..3da852f8a --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/LessEqualsOperator.java @@ -0,0 +1,180 @@ +// 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.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.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) -> + CelFunctionBinding.from( + "less_equals_duration", + Duration.class, + Duration.class, + (Duration x, Duration y) -> ProtoTimeUtils.compare(x, y) <= 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) -> + CelFunctionBinding.from( + "less_equals_timestamp", + Timestamp.class, + Timestamp.class, + (Timestamp x, Timestamp y) -> ProtoTimeUtils.compare(x, y) <= 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 FunctionBindingCreator bindingCreator; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + LessEqualsOverload(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + private LessEqualsOperator(ImmutableSet overloads) { + super(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..8bae06a85 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/LessOperator.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.runtime.standard; + +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.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) -> + CelFunctionBinding.from( + "less_duration", + Duration.class, + Duration.class, + (Duration x, Duration y) -> ProtoTimeUtils.compare(x, y) < 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) -> + CelFunctionBinding.from( + "less_timestamp", + Timestamp.class, + Timestamp.class, + (Timestamp x, Timestamp y) -> ProtoTimeUtils.compare(x, y) < 0)), + ; + + private final FunctionBindingCreator bindingCreator; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + LessOverload(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + private LessOperator(ImmutableSet overloads) { + super(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..c11539a81 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/LogicalNotOperator.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 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 FunctionBindingCreator bindingCreator; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + LogicalNotOverload(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + private LogicalNotOperator(ImmutableSet overloads) { + super(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..1b0891b1a --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/MatchesFunction.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.CelErrorCode; +import dev.cel.common.CelOptions; +import dev.cel.common.CelRuntimeException; +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 CelRuntimeException(e, CelErrorCode.INVALID_ARGUMENT); + } + })), + // 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 CelRuntimeException(e, CelErrorCode.INVALID_ARGUMENT); + } + })), + ; + + private final FunctionBindingCreator bindingCreator; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + MatchesOverload(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + private MatchesFunction(ImmutableSet overloads) { + super(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..716db43b7 --- /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.runtime.standard.ArithmeticHelpers.getArithmeticErrorCode; + +import com.google.common.collect.ImmutableSet; +import com.google.common.primitives.UnsignedLong; +import dev.cel.common.CelOptions; +import dev.cel.common.CelRuntimeException; +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 CelRuntimeException(e, getArithmeticErrorCode(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 FunctionBindingCreator bindingCreator; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + ModuloOverload(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + private ModuloOperator(ImmutableSet overloads) { + super(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..46472fdd6 --- /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.runtime.standard.ArithmeticHelpers.getArithmeticErrorCode; + +import com.google.common.collect.ImmutableSet; +import com.google.common.primitives.UnsignedLong; +import dev.cel.common.CelOptions; +import dev.cel.common.CelRuntimeException; +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 CelRuntimeException(e, getArithmeticErrorCode(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 CelRuntimeException(e, getArithmeticErrorCode(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 CelRuntimeException(e, getArithmeticErrorCode(e)); + } + }); + } + }); + + private final FunctionBindingCreator bindingCreator; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + MultiplyOverload(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + private MultiplyOperator(ImmutableSet overloads) { + super(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..dd30c557e --- /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.runtime.standard.ArithmeticHelpers.getArithmeticErrorCode; + +import com.google.common.collect.ImmutableSet; +import dev.cel.common.CelOptions; +import dev.cel.common.CelRuntimeException; +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 CelRuntimeException(e, getArithmeticErrorCode(e)); + } + })), + NEGATE_DOUBLE( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from("negate_double", Double.class, (Double x) -> -x)); + + private final FunctionBindingCreator bindingCreator; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + NegateOverload(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + private NegateOperator(ImmutableSet overloads) { + super(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..886047e98 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/NotEqualsOperator.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 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 FunctionBindingCreator bindingCreator; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + NotEqualsOverload(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + private NotEqualsOperator(ImmutableSet overloads) { + super(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..206e4bdd4 --- /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 FunctionBindingCreator bindingCreator; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + SizeOverload(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + private SizeFunction(ImmutableSet overloads) { + super(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..3a73a9f35 --- /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 FunctionBindingCreator bindingCreator; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + StartsWithOverload(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + private StartsWithFunction(ImmutableSet overloads) { + super(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..3525ba5e0 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/StringFunction.java @@ -0,0 +1,126 @@ +// 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.CelErrorCode; +import dev.cel.common.CelOptions; +import dev.cel.common.CelRuntimeException; +import dev.cel.common.internal.ProtoTimeUtils; +import dev.cel.common.values.CelByteString; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.RuntimeEquality; +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 CelRuntimeException( + new IllegalArgumentException( + "invalid UTF-8 in bytes, cannot convert to string"), + CelErrorCode.BAD_FORMAT); + } + return byteStr.toStringUtf8(); + }); + } else { + return CelFunctionBinding.from( + "bytes_to_string", + ByteString.class, + (byteStr) -> { + if (!byteStr.isValidUtf8()) { + throw new CelRuntimeException( + new IllegalArgumentException( + "invalid UTF-8 in bytes, cannot convert to string"), + CelErrorCode.BAD_FORMAT); + } + return byteStr.toStringUtf8(); + }); + } + }), + TIMESTAMP_TO_STRING( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "timestamp_to_string", Timestamp.class, ProtoTimeUtils::toString)), + DURATION_TO_STRING( + (celOptions, runtimeEquality) -> + 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 FunctionBindingCreator bindingCreator; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + StringOverload(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + private StringFunction(ImmutableSet overloads) { + super(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..695650390 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/SubtractOperator.java @@ -0,0 +1,133 @@ +// 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.runtime.standard.ArithmeticHelpers.getArithmeticErrorCode; + +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.CelRuntimeException; +import dev.cel.common.internal.ProtoTimeUtils; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.RuntimeEquality; +import dev.cel.runtime.RuntimeHelpers; +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 CelRuntimeException(e, getArithmeticErrorCode(e)); + } + })), + SUBTRACT_TIMESTAMP_TIMESTAMP( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "subtract_timestamp_timestamp", + Timestamp.class, + Timestamp.class, + (Timestamp x, Timestamp y) -> ProtoTimeUtils.between(y, x))), + SUBTRACT_TIMESTAMP_DURATION( + (celOptions, runtimeEquality) -> + 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 CelRuntimeException(e, getArithmeticErrorCode(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 CelRuntimeException(e, getArithmeticErrorCode(e)); + } + }); + } + }), + SUBTRACT_DOUBLE( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "subtract_double", Double.class, Double.class, (Double x, Double y) -> x - y)), + SUBTRACT_DURATION_DURATION( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "subtract_duration_duration", + Duration.class, + Duration.class, + ProtoTimeUtils::subtract)), + ; + + private final FunctionBindingCreator bindingCreator; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + SubtractOverload(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + private SubtractOperator(ImmutableSet overloads) { + super(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..9b64b78cb --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/TimestampFunction.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.runtime.standard; + +import com.google.common.collect.ImmutableSet; +import com.google.protobuf.Timestamp; +import dev.cel.common.CelErrorCode; +import dev.cel.common.CelOptions; +import dev.cel.common.CelRuntimeException; +import dev.cel.common.internal.ProtoTimeUtils; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.RuntimeEquality; +import java.text.ParseException; +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) -> { + try { + return ProtoTimeUtils.parse(ts); + } catch (ParseException e) { + throw new CelRuntimeException(e, CelErrorCode.BAD_FORMAT); + } + })), + TIMESTAMP_TO_TIMESTAMP( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from("timestamp_to_timestamp", Timestamp.class, (Timestamp x) -> x)), + INT64_TO_TIMESTAMP( + (celOptions, runtimeEquality) -> + CelFunctionBinding.from( + "int64_to_timestamp", Long.class, ProtoTimeUtils::fromSecondsToTimestamp)), + ; + + private final FunctionBindingCreator bindingCreator; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + TimestampOverload(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + private TimestampFunction(ImmutableSet overloads) { + super(overloads); + } +} 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..a05258521 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/standard/UintFunction.java @@ -0,0 +1,163 @@ +// 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.CelErrorCode; +import dev.cel.common.CelOptions; +import dev.cel.common.CelRuntimeException; +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 CelRuntimeException( + new IllegalArgumentException("int out of uint range"), + CelErrorCode.NUMERIC_OVERFLOW); + } + return UnsignedLong.valueOf(arg); + }); + } else { + return CelFunctionBinding.from( + "int64_to_uint64", + Long.class, + (Long arg) -> { + if (celOptions.errorOnIntWrap() && arg < 0) { + throw new CelRuntimeException( + new IllegalArgumentException("int out of uint range"), + CelErrorCode.NUMERIC_OVERFLOW); + } + 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 CelRuntimeException( + new IllegalArgumentException("double out of uint range"), + CelErrorCode.NUMERIC_OVERFLOW)); + } + 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 CelRuntimeException( + new IllegalArgumentException("double out of uint range"), + CelErrorCode.NUMERIC_OVERFLOW)); + } + 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 CelRuntimeException(e, CelErrorCode.BAD_FORMAT); + } + }); + } else { + return CelFunctionBinding.from( + "string_to_uint64", + String.class, + (String arg) -> { + try { + return UnsignedLongs.parseUnsignedLong(arg); + } catch (NumberFormatException e) { + throw new CelRuntimeException(e, CelErrorCode.BAD_FORMAT); + } + }); + } + }), + ; + + private final FunctionBindingCreator bindingCreator; + + @Override + public CelFunctionBinding newFunctionBinding( + CelOptions celOptions, RuntimeEquality runtimeEquality) { + return bindingCreator.create(celOptions, runtimeEquality); + } + + UintOverload(FunctionBindingCreator bindingCreator) { + this.bindingCreator = bindingCreator; + } + } + + private UintFunction(ImmutableSet overloads) { + super(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..5e3f3f1fe 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; @@ -35,6 +35,13 @@ public final class ActivationTest { private static final CelOptions TEST_OPTIONS = CelOptions.current().enableTimestampEpoch(true).enableUnsignedLongs(true).build(); + private static final CelOptions TEST_OPTIONS_SKIP_UNSET_FIELDS = + CelOptions.current() + .enableTimestampEpoch(true) + .enableUnsignedLongs(true) + .fromProtoUnsetFieldOption(CelOptions.ProtoUnsetFieldOptions.SKIP) + .build(); + @Test public void copyOf_success_withNullEntries() { Map map = new HashMap<>(); @@ -49,27 +56,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 +98,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 +136,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 +154,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 +164,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..5d083cdc9 100644 --- a/runtime/src/test/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/test/java/dev/cel/runtime/BUILD.bazel @@ -1,22 +1,38 @@ +load("@rules_java//java:defs.bzl", "java_library") +load("//:cel_android_rules.bzl", "cel_android_local_test") load("//:testing.bzl", "junit4_test_suites") -package(default_applicable_licenses = ["//:license"]) +package( + default_applicable_licenses = ["//:license"], + default_testonly = True, +) + +ANDROID_TESTS = [ + "CelLiteRuntimeAndroidTest.java", +] java_library( name = "tests", testonly = 1, srcs = glob( ["*.java"], + # keep sorted exclude = [ + "CelLiteInterpreterTest.java", "CelValueInterpreterTest.java", "InterpreterTest.java", - ], + ] + ANDROID_TESTS, ), deps = [ "//:auto_value", "//:java_truth", "//bundle:cel", - "//common", + "//common:cel_ast", + "//common:cel_descriptors", + "//common:cel_exception", + "//common:cel_source", + "//common:compiler_common", + "//common:container", "//common:error_codes", "//common:options", "//common:proto_v1alpha1_ast", @@ -27,24 +43,54 @@ java_library( "//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:function_overload_impl", + "//runtime:interpretable", "//runtime:interpreter", - "//runtime:runtime_helper", + "//runtime:interpreter_util", + "//runtime:late_function_binding", + "//runtime:lite_runtime", + "//runtime:lite_runtime_factory", + "//runtime:proto_message_activation_factory", + "//runtime:proto_message_runtime_equality", + "//runtime:proto_message_runtime_helpers", + "//runtime:runtime_equality", + "//runtime:runtime_helpers", + "//runtime:standard_functions", + "//runtime:type_resolver", "//runtime:unknown_attributes", "//runtime:unknown_options", + "//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 +98,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,11 +110,9 @@ java_library( ], deps = [ # "//java/com/google/testing/testsize:annotations", - "//common:options", "//testing:base_interpreter_test", - "//testing:eval", - "//testing:sync", "@maven//:junit_junit", + "@maven//:com_google_testparameterinjector_test_parameter_injector", ], ) @@ -79,10 +124,62 @@ java_library( ], deps = [ # "//java/com/google/testing/testsize:annotations", + "//testing:base_interpreter_test", + "@maven//:junit_junit", + "@maven//:com_google_testparameterinjector_test_parameter_injector", + ], +) + +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/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_lite_interpreter_test", + testonly = 1, + srcs = [ + "CelLiteInterpreterTest.java", + ], + deps = [ + "//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,6 +193,7 @@ junit4_test_suites( ], src_dir = "src/test/java", deps = [ + ":cel_lite_interpreter_test", ":cel_value_interpreter_test", ":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..b3bc32e20 --- /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.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 CelRuntimeException(cause, CelErrorCode.BAD_FORMAT); + CelEvaluationExceptionBuilder builder = + CelEvaluationExceptionBuilder.newBuilder(celRuntimeException); + + CelEvaluationException e = builder.build(); + + assertThat(e).hasMessageThat().isEqualTo("evaluation error: cause error message"); + assertThat(e).hasCauseThat().isEqualTo(cause); + 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..03bed8ae6 --- /dev/null +++ b/runtime/src/test/java/dev/cel/runtime/CelLateFunctionBindingsTest.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.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.findOverload( + "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.findOverload( + "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.findOverload( + "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.findOverload( + "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.findOverload( + "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.findOverload("identity", ImmutableList.of("identity_int"), new Object[] {null}); + assertThat(overload).isEmpty(); + } + + @Test + public void findOverload_nullMessageArg_returnsOverload() throws Exception { + CelLateFunctionBindings bindings = + CelLateFunctionBindings.from( + CelFunctionBinding.from("identity_msg", TestAllTypes.class, (arg) -> arg)); + Optional overload = + bindings.findOverload("identity", ImmutableList.of("identity_msg"), new Object[] {null}); + assertThat(overload).isPresent(); + assertThat(overload.get().getOverloadId()).isEqualTo("identity_msg"); + assertThat(overload.get().getParameterTypes()).containsExactly(TestAllTypes.class); + assertThat(overload.get().getDefinition().apply(new Object[] {null})).isNull(); + } +} 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..088a2d7b0 --- /dev/null +++ b/runtime/src/test/java/dev/cel/runtime/CelLiteInterpreterTest.java @@ -0,0 +1,100 @@ +// 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.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 { + public CelLiteInterpreterTest() { + super( + CelRuntimeFactory.standardCelRuntimeBuilder() + .setValueProvider( + ProtoMessageLiteValueProvider.newInstance( + dev.cel.expr.conformance.proto2.TestAllTypesCelDescriptor.getDescriptor(), + TestAllTypesCelDescriptor.getDescriptor())) + .addLibraries(CelOptionalLibrary.INSTANCE) + .setOptions(newBaseCelOptions().toBuilder().enableCelValue(true).build()) + .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 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(); + } +} 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..2896e36f4 --- /dev/null +++ b/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeAndroidTest.java @@ -0,0 +1,740 @@ +// 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.CelLiteRuntime.Program; +import dev.cel.runtime.standard.EqualsOperator; +import dev.cel.runtime.standard.IntFunction; +import dev.cel.runtime.standard.IntFunction.IntOverload; +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(2); + assertThat(newRuntimeBuilder.customFunctionBindings).containsKey("string_isEmpty"); + assertThat(newRuntimeBuilder.customFunctionBindings).containsKey("list_sets_intersects_list"); + } + + @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, + ProtoTimeUtils.fromSecondsToDuration(15), + false, + ProtoTimeUtils.fromSecondsToDuration(16)), + ImmutableMap.of( + true, + ProtoTimeUtils.fromSecondsToTimestamp(17), + false, + ProtoTimeUtils.fromSecondsToTimestamp(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()), + STRING_CONCAT_DISABLED(newBaseTestOptions().enableStringConcatenation(false).build()), + STRING_CONVERSION_DISABLED(newBaseTestOptions().enableStringConversion(false).build()), + LIST_CONCATENATION_DISABLED(newBaseTestOptions().enableListConcatenation(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..4083ae494 --- /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.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.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.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.SingleFileCelDescriptor; +import dev.cel.testing.testdata.SingleFileProto.SingleFile; +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(ProtoTimeUtils.fromSecondsToDuration(600)); + } + + @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(); + + Timestamp result = (Timestamp) CEL_RUNTIME.createProgram(ast).eval(ImmutableMap.of("msg", msg)); + + assertThat(result).isEqualTo(ProtoTimeUtils.fromSecondsToTimestamp(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..8b0fd7193 --- /dev/null +++ b/runtime/src/test/java/dev/cel/runtime/CelResolvedOverloadTest.java @@ -0,0 +1,67 @@ +// 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 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( + "increment_int", + ImmutableList.of(Long.class), + (args) -> { + Long arg = (Long) args[0]; + return arg + 1; + }); + } + + @Test + public void canHandle_matchingTypes_returnsTrue() { + assertThat(getIncrementIntOverload().canHandle(new Object[] {1L})).isTrue(); + } + + @Test + public void canHandle_nullMessageType_returnsTrue() { + CelResolvedOverload overload = + CelResolvedOverload.of("identity", ImmutableList.of(TestAllTypes.class), (args) -> args[0]); + assertThat(overload.canHandle(new Object[] {null})).isTrue(); + } + + @Test + public void canHandle_nullPrimitive_returnsFalse() { + CelResolvedOverload overload = + CelResolvedOverload.of("identity", ImmutableList.of(Long.class), (args) -> args[0]); + 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(); + } +} diff --git a/runtime/src/test/java/dev/cel/runtime/CelRuntimeLegacyImplTest.java b/runtime/src/test/java/dev/cel/runtime/CelRuntimeLegacyImplTest.java index 02fd0ae7c..c4a041f6a 100644 --- a/runtime/src/test/java/dev/cel/runtime/CelRuntimeLegacyImplTest.java +++ b/runtime/src/test/java/dev/cel/runtime/CelRuntimeLegacyImplTest.java @@ -16,9 +16,15 @@ import static com.google.common.truth.Truth.assertThat; +import com.google.protobuf.Message; import dev.cel.common.CelException; +import dev.cel.common.values.CelValueProvider; import dev.cel.compiler.CelCompiler; import dev.cel.compiler.CelCompilerFactory; +import dev.cel.expr.conformance.proto3.TestAllTypes; +import dev.cel.runtime.CelStandardFunctions.StandardFunction; +import java.util.Optional; +import java.util.function.Function; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; @@ -35,4 +41,87 @@ public void evalException() throws CelException { CelEvaluationException e = Assert.assertThrows(CelEvaluationException.class, program::eval); assertThat(e).hasCauseThat().isInstanceOf(ArithmeticException.class); } + + @Test + public void toRuntimeBuilder_isNewInstance() { + CelRuntimeBuilder celRuntimeBuilder = CelRuntimeFactory.standardCelRuntimeBuilder(); + CelRuntimeLegacyImpl celRuntime = (CelRuntimeLegacyImpl) celRuntimeBuilder.build(); + + CelRuntimeLegacyImpl.Builder newRuntimeBuilder = + (CelRuntimeLegacyImpl.Builder) celRuntime.toRuntimeBuilder(); + + assertThat(newRuntimeBuilder).isNotEqualTo(celRuntimeBuilder); + } + + @Test + public void toRuntimeBuilder_isImmutable() { + CelRuntimeBuilder originalRuntimeBuilder = CelRuntimeFactory.standardCelRuntimeBuilder(); + CelRuntimeLegacyImpl celRuntime = (CelRuntimeLegacyImpl) originalRuntimeBuilder.build(); + originalRuntimeBuilder.addLibraries(runtimeBuilder -> {}); + + CelRuntimeLegacyImpl.Builder newRuntimeBuilder = + (CelRuntimeLegacyImpl.Builder) celRuntime.toRuntimeBuilder(); + + assertThat(newRuntimeBuilder.celRuntimeLibraries.build()).isEmpty(); + } + + @Test + public void toRuntimeBuilder_collectionProperties_copied() { + CelRuntimeBuilder celRuntimeBuilder = CelRuntimeFactory.standardCelRuntimeBuilder(); + celRuntimeBuilder.addMessageTypes(TestAllTypes.getDescriptor()); + 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.customFunctionBindings).hasSize(1); + assertThat(newRuntimeBuilder.celRuntimeLibraries.build()).hasSize(1); + assertThat(newRuntimeBuilder.fileTypes.build()).hasSize(originalFileTypesSize); + } + + @Test + public void toRuntimeBuilder_collectionProperties_areImmutable() { + CelRuntimeBuilder celRuntimeBuilder = CelRuntimeFactory.standardCelRuntimeBuilder(); + CelRuntimeLegacyImpl celRuntime = (CelRuntimeLegacyImpl) celRuntimeBuilder.build(); + CelRuntimeLegacyImpl.Builder newRuntimeBuilder = + (CelRuntimeLegacyImpl.Builder) celRuntime.toRuntimeBuilder(); + + // Mutate the original builder containing collections + celRuntimeBuilder.addMessageTypes(TestAllTypes.getDescriptor()); + celRuntimeBuilder.addFileTypes(TestAllTypes.getDescriptor().getFile()); + celRuntimeBuilder.addFunctionBindings(CelFunctionBinding.from("test", Integer.class, arg -> 1)); + celRuntimeBuilder.addLibraries(runtimeBuilder -> {}); + + 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..3a2bcc1f4 100644 --- a/runtime/src/test/java/dev/cel/runtime/CelRuntimeTest.java +++ b/runtime/src/test/java/dev/cel/runtime/CelRuntimeTest.java @@ -15,10 +15,12 @@ package dev.cel.runtime; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; 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,9 +28,13 @@ 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.CelOptions; import dev.cel.common.CelProtoV1Alpha1AbstractSyntaxTree; import dev.cel.common.CelSource; @@ -36,22 +42,23 @@ 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 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 { @Test @@ -112,6 +119,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 = @@ -275,7 +326,7 @@ public void trace_select() throws Exception { Cel cel = CelFactory.standardCelBuilder() .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 +336,16 @@ 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("TestAllTypes"); }; Cel cel = CelFactory.standardCelBuilder() .addMessageTypes(TestAllTypes.getDescriptor()) - .setContainer("dev.cel.testing.testdata.proto3") + .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) .build(); CelAbstractSyntaxTree ast = cel.compile("TestAllTypes{}").getAst(); @@ -305,12 +356,12 @@ 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(); @@ -323,12 +374,12 @@ 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(); @@ -398,4 +449,287 @@ 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 = + CelFactory.standardCelBuilder() + .setOptions(CelOptions.current().enableShortCircuiting(true).build()) + .build(); + Cel cel = + CelFactory.standardCelBuilder() + .setOptions(CelOptions.current().enableShortCircuiting(false).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 = + CelFactory.standardCelBuilder() + .addVar("x", SimpleType.BOOL) + .setOptions(CelOptions.current().enableShortCircuiting(false).build()) + .build(); + CelAbstractSyntaxTree ast = cel.compile(source).getAst(); + + boolean result = (boolean) cel.createProgram(ast).trace(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 = + CelFactory.standardCelBuilder() + .addVar("x", SimpleType.BOOL) + .setOptions(CelOptions.current().enableShortCircuiting(false).build()) + .build(); + CelAbstractSyntaxTree ast = cel.compile(source).getAst(); + + Object unknownResult = cel.createProgram(ast).trace(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 = + CelFactory.standardCelBuilder() + .setOptions(CelOptions.current().enableShortCircuiting(true).build()) + .build(); + Cel cel = + CelFactory.standardCelBuilder() + .setOptions(CelOptions.current().enableShortCircuiting(false).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 = + CelFactory.standardCelBuilder() + .addVar("x", SimpleType.BOOL) + .setOptions(CelOptions.current().enableShortCircuiting(false).build()) + .build(); + CelAbstractSyntaxTree ast = cel.compile(source).getAst(); + + Object unknownResult = cel.createProgram(ast).trace(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 = + CelFactory.standardCelBuilder() + .addVar("x", SimpleType.BOOL) + .setOptions(CelOptions.current().enableShortCircuiting(false).build()) + .build(); + CelAbstractSyntaxTree ast = cel.compile(source).getAst(); + + boolean result = (boolean) cel.createProgram(ast).trace(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 = + CelFactory.standardCelBuilder() + .setOptions(CelOptions.current().enableShortCircuiting(false).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 = + CelFactory.standardCelBuilder() + .addVar("x", SimpleType.BOOL) + .setOptions(CelOptions.current().enableShortCircuiting(false).build()) + .build(); + CelAbstractSyntaxTree ast = cel.compile(source).getAst(); + + Object unknownResult = cel.createProgram(ast).trace(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 = + CelFactory.standardCelBuilder() + .setOptions(CelOptions.current().enableShortCircuiting(true).build()) + .build(); + Cel cel = + CelFactory.standardCelBuilder() + .setOptions(CelOptions.current().enableShortCircuiting(false).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 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(); + + CelEvaluationException e = + assertThrows(CelEvaluationException.class, () -> celRuntime.createProgram(ast).eval()); + assertThat(e) + .hasMessageThat() + .contains("No matching overload for function 'size'. Overload candidates: size_string"); + } } 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..09da78615 --- /dev/null +++ b/runtime/src/test/java/dev/cel/runtime/CelStandardFunctionsTest.java @@ -0,0 +1,244 @@ +// 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.CelStandardFunctions.StandardFunction.Overload.Arithmetic; +import dev.cel.runtime.CelStandardFunctions.StandardOverload; +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(Arithmetic.ADD_INT64)) { + return true; + } + + if (func.equals(CelStandardFunctions.StandardFunction.SUBTRACT) + && over.equals(Arithmetic.SUBTRACT_INT64)) { + return true; + } + + return false; + }) + .build(); + + assertThat(celStandardFunction.getOverloads()) + .containsExactly(Arithmetic.ADD_INT64, Arithmetic.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().enableTimestampEpoch(true).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 index 4c657a0c8..f56bb3012 100644 --- a/runtime/src/test/java/dev/cel/runtime/CelValueInterpreterTest.java +++ b/runtime/src/test/java/dev/cel/runtime/CelValueInterpreterTest.java @@ -14,75 +14,17 @@ 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.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) +@RunWith(TestParameterInjector.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) - }, - })); + public CelValueInterpreterTest() { + super(newBaseCelOptions().toBuilder().enableCelValue(true).build()); } } diff --git a/runtime/src/test/java/dev/cel/runtime/CelVariableResolverTest.java b/runtime/src/test/java/dev/cel/runtime/CelVariableResolverTest.java index 235ef1516..3d2515ad3 100644 --- a/runtime/src/test/java/dev/cel/runtime/CelVariableResolverTest.java +++ b/runtime/src/test/java/dev/cel/runtime/CelVariableResolverTest.java @@ -14,7 +14,7 @@ package dev.cel.runtime; -import static com.google.common.truth.Truth8.assertThat; +import static com.google.common.truth.Truth.assertThat; import static dev.cel.runtime.CelVariableResolver.hierarchicalVariableResolver; import com.google.common.collect.ImmutableMap; 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..556d6945c --- /dev/null +++ b/runtime/src/test/java/dev/cel/runtime/DefaultDispatcherTest.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 aj +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS 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( + "overload_1", new Class[] {Long.class}, args -> (Long) args[0] + 1)); + overloads.put( + "overload_2", + CelResolvedOverload.of( + "overload_2", new Class[] {Long.class}, args -> (Long) args[0] + 2)); + } + + @Test + public void findOverload_multipleMatches_throwsException() { + CelEvaluationException e = + Assert.assertThrows( + CelEvaluationException.class, + () -> + DefaultDispatcher.findOverload( + "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..4f3501d41 --- /dev/null +++ b/runtime/src/test/java/dev/cel/runtime/DefaultInterpreterTest.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; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +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.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 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 dispatcher = DefaultDispatcher.create(); + dispatcher.add("error", long.class, (args) -> new IllegalArgumentException("Always throws")); + DefaultInterpreter defaultInterpreter = + new DefaultInterpreter(new TypeResolver(), emptyProvider, dispatcher, 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..8866d1fa6 100644 --- a/runtime/src/test/java/dev/cel/runtime/DescriptorMessageProviderTest.java +++ b/runtime/src/test/java/dev/cel/runtime/DescriptorMessageProviderTest.java @@ -38,11 +38,10 @@ // CEL-Internal-3 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( @@ -112,11 +120,12 @@ public void createMessage_missingDescriptorError() { @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 @@ -156,7 +165,10 @@ public void selectField_mapKeyNotFound() { @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).hasCauseThat().isInstanceOf(IllegalArgumentException.class); + assertThat(e.getErrorCode()).isEqualTo(CelErrorCode.ATTRIBUTE_NOT_FOUND); } @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..878576f94 --- /dev/null +++ b/runtime/src/test/java/dev/cel/runtime/DescriptorTypeResolverTest.java @@ -0,0 +1,171 @@ +// 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.CelOptions; +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() + .setOptions(CelOptions.current().enableTimestampEpoch(true).build()) + .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/ProtoMessageRuntimeEqualityTest.java b/runtime/src/test/java/dev/cel/runtime/ProtoMessageRuntimeEqualityTest.java new file mode 100644 index 000000000..16806c856 --- /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.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 52% rename from runtime/src/test/java/dev/cel/runtime/RuntimeHelpersTest.java rename to runtime/src/test/java/dev/cel/runtime/ProtoMessageRuntimeHelpersTest.java index b22c3f14e..f6d701741 100644 --- a/runtime/src/test/java/dev/cel/runtime/RuntimeHelpersTest.java +++ b/runtime/src/test/java/dev/cel/runtime/ProtoMessageRuntimeHelpersTest.java @@ -39,6 +39,7 @@ import dev.cel.common.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 +49,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 +65,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)); + () -> 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)); + ArithmeticException.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)); + () -> 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() @@ -191,105 +203,119 @@ public void uint64Multiply_signedLongs() throws Exception { .isEqualTo(0); assertThrows( ArithmeticException.class, - () -> RuntimeHelpers.uint64Multiply(Long.MIN_VALUE, 2, CelOptions.DEFAULT)); + () -> 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))); + () -> + 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)); + ArithmeticException.class, + () -> ProtoMessageRuntimeHelpers.uint64Subtract(0, 1, CelOptions.DEFAULT)); assertThrows( - ArithmeticException.class, () -> RuntimeHelpers.uint64Subtract(-3, -1, CelOptions.DEFAULT)); + ArithmeticException.class, + () -> ProtoMessageRuntimeHelpers.uint64Subtract(-3, -1, CelOptions.DEFAULT)); assertThrows( ArithmeticException.class, - () -> RuntimeHelpers.uint64Subtract(55, -40, CelOptions.DEFAULT)); + () -> 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))); + () -> ProtoMessageRuntimeHelpers.uint64Subtract(UnsignedLong.ONE, UnsignedLong.valueOf(2))); } @Test @@ -313,67 +339,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 +386,7 @@ public void adaptProtoToValue_jsonValues() throws Exception { .addValues( Value.newBuilder().setNullValue(NullValue.NULL_VALUE))) .build()) - .build(), - CelOptions.LEGACY)) + .build())) .isEqualTo(mp); } @@ -404,17 +405,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..d942295ba 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. @@ -14,673 +14,27 @@ 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)); - } - - @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(), - }, - }); - } @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..c5a11b680 --- /dev/null +++ b/runtime/src/test/java/dev/cel/runtime/TypeResolverTest.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; + +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 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(); + + @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/UnknownContextTest.java b/runtime/src/test/java/dev/cel/runtime/UnknownContextTest.java index a48fcc8d9..428fa6b26 100644 --- a/runtime/src/test/java/dev/cel/runtime/UnknownContextTest.java +++ b/runtime/src/test/java/dev/cel/runtime/UnknownContextTest.java @@ -15,7 +15,6 @@ package dev.cel.runtime; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth8.assertThat; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; 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..99e9fd59c 100644 --- a/runtime/src/test/java/dev/cel/runtime/async/BUILD.bazel +++ b/runtime/src/test/java/dev/cel/runtime/async/BUILD.bazel @@ -1,3 +1,4 @@ +load("@rules_java//java:defs.bzl", "java_library") load("//:testing.bzl", "junit4_test_suites") package(default_applicable_licenses = [ @@ -9,28 +10,31 @@ java_library( testonly = True, srcs = glob(["*Test.java"]), deps = [ - "//:java_truth", + # "//java/com/google/testing/testsize:annotations", "//bundle:cel", - "//common", + "//common:cel_ast", + "//common:container", "//common:options", - "//common/resources/testdata/proto3:test_all_types_java_proto", "//common/testing", "//common/types", "//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..ab2ba2004 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,11 @@ public void asyncProgram_basicUnknownResolution() throws Exception { } @Test - public void asyncProgram_basicAsyncResovler() throws Exception { + public void asyncProgram_basicAsyncResolver() 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() @@ -117,21 +118,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 +131,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 +158,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 +170,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 +183,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 +211,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 +221,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 +235,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 +242,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 +278,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 +288,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 +295,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 +323,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 +336,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 +347,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 +379,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 +392,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 +403,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 +434,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 +447,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 +455,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/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/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/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/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/contextPropagation.baseline b/runtime/src/test/resources/contextPropagation.baseline new file mode 100644 index 000000000..a70382604 --- /dev/null +++ b/runtime/src/test/resources/contextPropagation.baseline @@ -0,0 +1,7 @@ +Source: f() +declare f { + function f () -> string +} +=====> +bindings: {} +result: testpeer \ No newline at end of file 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/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 81% rename from runtime/src/test/resources/dynamicMessage.baseline rename to runtime/src/test/resources/dynamicMessage_adapted.baseline index 21751184b..789a39488 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,7 +684,7 @@ single_bool_wrapper { single_bytes_wrapper { value: "hi" } -single_list_value { +list_value { values { string_value: "d" } @@ -696,11 +696,11 @@ nanos: 20 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,7 +749,7 @@ single_bool_wrapper { single_bytes_wrapper { value: "hi" } -single_list_value { +list_value { values { string_value: "d" } @@ -761,11 +761,11 @@ nanos: 200 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 +814,7 @@ single_bool_wrapper { single_bytes_wrapper { value: "hi" } -single_list_value { +list_value { values { string_value: "d" } @@ -824,11 +824,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 +877,7 @@ single_bool_wrapper { single_bytes_wrapper { value: "hi" } -single_list_value { +list_value { values { string_value: "d" } @@ -885,13 +885,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 +940,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..36dac04c3 --- /dev/null +++ b/runtime/src/test/resources/dynamicMessage_dynamicDescriptor.baseline @@ -0,0 +1,222 @@ +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: seconds: 100 + + +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..ff406cf94 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 { @@ -55,7 +55,7 @@ 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 +77,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 +103,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 +123,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 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..1e79b815e 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 @@ -126,7 +126,7 @@ 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 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/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/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..68892ee2d 100644 --- a/runtime/src/test/resources/timeConversions.baseline +++ b/runtime/src/test/resources/timeConversions.baseline @@ -46,11 +46,19 @@ result: seconds: 3723 nanos: 400000000 -Source: duration('inf') +Source: duration(duration('15.0s')) declare t1 { value google.protobuf.Timestamp } =====> bindings: {} -error: evaluation error: invalid duration format -error_code: BAD_FORMAT +result: seconds: 15 + + +Source: timestamp(timestamp(123)) +declare t1 { + value google.protobuf.Timestamp +} +=====> +bindings: {} +result: seconds: 123 \ 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/typeComparisons.baseline b/runtime/src/test/resources/typeComparisons.baseline index 3af41c3da..ae3e8aa01 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 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..c5f3c755a 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]} diff --git a/runtime/src/test/resources/unknownResultSet.baseline b/runtime/src/test/resources/unknownResultSet.baseline index aa9d032ff..8c7183761 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: Failed to parse timestamp: invalid timestamp "bad timestamp string" 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: Failed to parse timestamp: invalid timestamp "bad timestamp string" 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..4a188e484 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 { @@ -68,8 +68,19 @@ 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 \ No newline at end of file diff --git a/runtime/standard/BUILD.bazel b/runtime/standard/BUILD.bazel new file mode 100644 index 000000000..5f84c105f --- /dev/null +++ b/runtime/standard/BUILD.bazel @@ -0,0 +1,407 @@ +load("@rules_java//java:defs.bzl", "java_library") +load("//:cel_android_rules.bzl", "cel_android_library") + +package( + default_applicable_licenses = ["//:license"], + default_visibility = ["//:internal"], +) + +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"], +) 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..c1b2a92b4 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( @@ -19,32 +21,22 @@ 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"], -) - -java_library( - name = "cel_value_sync", - exports = ["//testing/src/main/java/dev/cel/testing:cel_value_sync"], + name = "base_interpreter_test", + exports = ["//testing/src/main/java/dev/cel/testing:base_interpreter_test"], ) -java_library( - name = "eval", - exports = ["//testing/src/main/java/dev/cel/testing:eval"], +alias( + name = "policy_test_resources", + actual = "//testing/src/test/resources/policy:policy_yaml_files", ) java_library( - name = "base_interpreter_test", - exports = ["//testing/src/main/java/dev/cel/testing:base_interpreter_test"], + name = "expr_value_utils", + exports = ["//testing/src/main/java/dev/cel/testing/utils:expr_value_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..c51fbba85 --- /dev/null +++ b/testing/protos/BUILD.bazel @@ -0,0 +1,55 @@ +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 = "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..d9d6a6564 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,22 +68,37 @@ 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:type_providers", + "//common/values:cel_byte_string", + "//extensions:optional_library", + "//runtime", + "//runtime:function_binding", + "//runtime:late_function_binding", + "//runtime:unknown_attributes", + "@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_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", "@maven//:com_google_protobuf_protobuf_java_util", "@maven//:junit_junit", + "@maven_android//:com_google_protobuf_protobuf_javalite", ], ) diff --git a/testing/src/main/java/dev/cel/testing/BaseInterpreterTest.java b/testing/src/main/java/dev/cel/testing/BaseInterpreterTest.java index 88a4fd8ba..cd0da5b80 100644 --- a/testing/src/main/java/dev/cel/testing/BaseInterpreterTest.java +++ b/testing/src/main/java/dev/cel/testing/BaseInterpreterTest.java @@ -16,378 +16,495 @@ 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.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.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.CelOptionalLibrary; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.CelLateFunctionBindings; +import dev.cel.runtime.CelRuntime; +import dev.cel.runtime.CelRuntimeFactory; +import dev.cel.runtime.CelUnknownSet; +import dev.cel.runtime.CelVariableResolver; 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.util.Arrays; +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() + .enableTimestampEpoch(true) + .enableHeterogeneousNumericComparisons(true) + .enableOptionalSyntax(true) + .evaluateCanonicalTypesToNativeValues(true) + .comprehensionMaxIterations(1_000) + .build(); + private CelRuntime celRuntime; + + protected BaseInterpreterTest() { + this(newRuntime(BASE_CEL_OPTIONS)); + } - private final Eval eval; + protected BaseInterpreterTest(CelOptions celOptions) { + this(newRuntime(celOptions)); + } - public BaseInterpreterTest(boolean declareWithCelType, Eval eval) { - super(declareWithCelType); - this.eval = eval; + protected BaseInterpreterTest(CelRuntime celRuntime) { + this.celRuntime = celRuntime; } - /** 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()); + private static CelRuntime newRuntime(CelOptions celOptions) { + return CelRuntimeFactory.standardCelRuntimeBuilder() + .addLibraries(CelOptionalLibrary.INSTANCE) + .addFileTypes(TEST_FILE_DESCRIPTORS) + .setOptions(celOptions) + .build(); + } + + protected static CelOptions newBaseCelOptions() { + return BASE_CEL_OPTIONS; + } + + @Override + protected void prepareCompiler(CelTypeProvider typeProvider) { + super.prepareCompiler(typeProvider); + this.celCompiler = + celCompiler.toCompilerBuilder().addLibraries(CelOptionalLibrary.INSTANCE).build(); + } + + 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)); + } + + /** + * Helper to run a test for configured instance variables. Input must be of type map or {@link + * CelVariableResolver}. + */ + @SuppressWarnings("unchecked") + private Object runTestInternal( + Object input, Optional lateFunctionBindings) { + CelAbstractSyntaxTree ast = compileTestCase(); + if (ast == null) { + // Usually indicates test was not setup correctly + println("Source compilation failed"); + return null; + } + printBinding(input); Object result = null; try { - result = eval.eval(ast, activation); - if (result instanceof ByteString) { + CelRuntime.Program program = celRuntime.createProgram(ast); + 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 = "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); + public void arithmTimestamp() { + container = CelContainer.ofName(Type.getDescriptor().getFile().getPackage()); + declareVariable("ts1", SimpleType.TIMESTAMP); + declareVariable("ts2", SimpleType.TIMESTAMP); + declareVariable("d1", SimpleType.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)); + 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); + public void arithmDuration() { + container = CelContainer.ofName(Type.getDescriptor().getFile().getPackage()); + declareVariable("d1", SimpleType.DURATION); + declareVariable("d2", SimpleType.DURATION); + declareVariable("d3", SimpleType.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)); + + 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 +513,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); + container = 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); + container = 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() { + container = + 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 +590,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 +599,1100 @@ 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); + declareVariable("d1", SimpleType.DURATION); + declareVariable("d2", SimpleType.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(); + container = 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); + declareVariable("t1", SimpleType.TIMESTAMP); + declareVariable("t2", SimpleType.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(); + container = 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); + container = 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)); + 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())); + container = 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); + container = 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())); + container = 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(); } @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.from("ns_func_overload", String.class, s -> (long) s.length()), + 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"; + container = 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() { + container = 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"); + container = 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); + public void durationFunctions() { + declareVariable("d1", SimpleType.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(); + container = 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(); + public void timestampFunctions() { + declareVariable("ts1", SimpleType.TIMESTAMP); + container = CelContainer.ofName(Type.getDescriptor().getFile().getPackage()); Timestamp ts1 = Timestamp.newBuilder().setSeconds(1).setNanos(11000000).build(); - Timestamp ts2 = Timestamps.fromSeconds(-1); + Timestamp ts2 = ProtoTimeUtils.fromSecondsToTimestamp(-1); 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); + Timestamp tsSunday = ProtoTimeUtils.fromSecondsToTimestamp(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() { + container = 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)); + runTest(); - // PartialMessage cannot be a value of a map. - source = "{1: x.single_nested_message}"; - runTest(Activation.of("x", wm)); - - // 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() { + container = 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() { + container = 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", ProtoTimeUtils.fromSecondsToTimestamp(100))); source = "duration(\"1h2m3.4s\")"; - runTest(Activation.EMPTY); + runTest(); + + source = "duration(duration('15.0s'))"; // Identity + runTest(); + + source = "timestamp(timestamp(123))"; // Identity + runTest(); + } - // Not supported. + @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() { + container = 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)); } @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 +1700,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 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 bytesConversions() throws Exception { + 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() { + container = 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 +1786,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 +1794,7 @@ 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 list equality. TestAllTypes xList = @@ -1660,7 +1811,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 +1828,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 +1837,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 typeComparisons() throws Exception { - container = TestAllTypes.getDescriptor().getFile().getPackage(); + 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() { + container = CelContainer.ofName(TestAllTypes.getDescriptor().getFile().getPackage()); // Test numeric types. source = @@ -1720,49 +1884,49 @@ 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 @@ -1779,7 +1943,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 +1954,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,7 +1967,7 @@ 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()) @@ -1819,89 +1983,86 @@ 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)); } @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(), args -> 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())) @@ -1919,65 +2080,330 @@ public void dynamicMessage() throws Exception { .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(Duration.class); source = "msg.single_timestamp"; - assertThat(runTest(activation)).isInstanceOf(Timestamp.class); + assertThat(runTest(input)).isInstanceOf(Timestamp.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 { + container = CelContainer.ofName("dev.cel.testing.testdata.serialized.proto3"); - source = "msg.single_list_value"; - assertThat(runTest(activation)).isInstanceOf(List.class); + 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.from("f_msg_generated", TestAllTypes.class, x -> true), + CelFunctionBinding.from("f_msg_dynamic", DynamicMessage.class, x -> 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"); + } + + /** + * Checks that the CheckedExpr produced by CelCompiler is equal to the one reproduced by the + * native CelAbstractSyntaxTree + */ + private static void assertAstRoundTrip(CelAbstractSyntaxTree ast) { + CheckedExpr checkedExpr = CelProtoAbstractSyntaxTree.fromCelAst(ast).toCheckedExpr(); + CelProtoAbstractSyntaxTree protoAst = CelProtoAbstractSyntaxTree.fromCelAst(ast); + assertThat(checkedExpr).isEqualTo(protoAst.toCheckedExpr()); + } + + 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) { + if (input instanceof Map) { + Map inputMap = (Map) input; + if (inputMap.isEmpty()) { + println("bindings: {}"); + return; + } + + boolean first = true; + StringBuilder sb = new StringBuilder().append("{"); + for (Map.Entry entry : ((Map) input).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())); + } + } + sb.append("}"); + println("bindings: " + sb); + } 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) { + 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..0bee52af7 100644 --- a/testing/src/main/java/dev/cel/testing/CelBaselineTestCase.java +++ b/testing/src/main/java/dev/cel/testing/CelBaselineTestCase.java @@ -14,20 +14,21 @@ 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 +36,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,11 +46,11 @@ * 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; @@ -56,22 +58,13 @@ public abstract class CelBaselineTestCase extends BaselineTestCase { 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))); @@ -106,31 +99,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 +109,7 @@ protected void prepareCompiler(CelTypeProvider typeProvider) { CelCompilerFactory.standardCelCompilerBuilder() .setOptions(TEST_OPTIONS) .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addLibraries(CelExtensions.comprehensions()) .setContainer(container) .setTypeProvider(typeProvider); @@ -148,9 +117,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 +128,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,55 +146,42 @@ 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 * @@ -238,48 +190,34 @@ private void formatDeclImpl(Decl decl, StringBuilder declStr) { * is set, the protobuf overloads are internally converted into java native versions {@link * CelOverloadDecl}. */ - 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/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..bfbe68cab --- /dev/null +++ b/testing/src/main/java/dev/cel/testing/testrunner/BUILD.bazel @@ -0,0 +1,227 @@ +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_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 = ["@maven//:com_google_guava_guava"], +) + +java_library( + name = "cel_user_test_template", + srcs = ["CelUserTestTemplate.java"], + tags = [ + ], + deps = [ + ":cel_expression_source", + ":cel_test_context", + ":cel_test_suite", + ":test_runner_library", + "@maven//:junit_junit", + ], +) + +java_library( + name = "test_runner_library", + srcs = ["TestRunnerLibrary.java"], + tags = [ + ], + deps = [ + ":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:compiler_common", + "//common:options", + "//common:proto_ast", + "//common/internal:default_instance_message_factory", + "//policy", + "//policy:compiler_factory", + "//policy:parser", + "//policy:parser_factory", + "//policy:validation_exception", + "//runtime", + "//testing:expr_value_utils", + "//testing/testrunner:proto_descriptor_utils", + "@cel_spec//proto/cel/expr:expr_java_proto", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + ], +) + +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/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", + ":result_matcher", + "//:auto_value", + "//bundle:cel", + "//common:options", + "//policy:parser", + "//runtime", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "registry_utils", + srcs = ["RegistryUtils.java"], + tags = [ + ], + deps = [ + "//common/internal:default_instance_message_factory", + "//testing/testrunner:proto_descriptor_utils", + "@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", + "//runtime", + "//testing:expr_value_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", + "@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/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..aa0d4b34f --- /dev/null +++ b/testing/src/main/java/dev/cel/testing/testrunner/CelTestContext.java @@ -0,0 +1,138 @@ +// 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.common.collect.ImmutableMap; +import dev.cel.bundle.Cel; +import dev.cel.bundle.CelFactory; +import dev.cel.common.CelOptions; +import dev.cel.policy.CelPolicyParser; +import dev.cel.runtime.CelLateFunctionBindings; +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(); + + /** + * 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(); + + /** 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 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); + + 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..e6086f128 --- /dev/null +++ b/testing/src/main/java/dev/cel/testing/testrunner/CelTestSuite.java @@ -0,0 +1,244 @@ +// 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(); + } + + /** 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. + } + } + } +} 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..3819e38d2 --- /dev/null +++ b/testing/src/main/java/dev/cel/testing/testrunner/CelTestSuiteTextProtoParser.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.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.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 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}. + */ +final class CelTestSuiteTextProtoParser { + + /** Creates a new instance of {@link CelTestSuiteTextProtoParser}. */ + static CelTestSuiteTextProtoParser newInstance() { + return new CelTestSuiteTextProtoParser(); + } + + CelTestSuite parse(String textProto) throws IOException, CelTestSuiteException { + TestSuite testSuite = parseTestSuite(textProto); + return parseCelTestSuite(testSuite); + } + + private TestSuite parseTestSuite(String textProto) throws IOException { + String fileDescriptorSetPath = System.getProperty("file_descriptor_set_path"); + TypeRegistry typeRegistry = TypeRegistry.getEmptyTypeRegistry(); + ExtensionRegistry extensionRegistry = ExtensionRegistry.getEmptyRegistry(); + if (fileDescriptorSetPath != null) { + extensionRegistry = RegistryUtils.getExtensionRegistry(fileDescriptorSetPath); + typeRegistry = RegistryUtils.getTypeRegistry(fileDescriptorSetPath); + } + 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..d1a3d6615 --- /dev/null +++ b/testing/src/main/java/dev/cel/testing/testrunner/CelTestSuiteYamlParser.java @@ -0,0 +1,378 @@ +// 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.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}. + */ +final class CelTestSuiteYamlParser { + + /** Creates a new instance of {@link CelTestSuiteYamlParser}. */ + static CelTestSuiteYamlParser newInstance() { + return new CelTestSuiteYamlParser(); + } + + 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(); + 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 "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..15c68d6b4 --- /dev/null +++ b/testing/src/main/java/dev/cel/testing/testrunner/CelUserTestTemplate.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.testing.testrunner; + +import dev.cel.testing.testrunner.CelTestSuite.CelTestSection.CelTestCase; +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 public CelTestCase testCase; + private final CelTestContext celTestContext; + + public CelUserTestTemplate(CelTestContext celTestContext) { + this.celTestContext = celTestContext; + } + + @Test + public void test() throws Exception { + 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..2d33253af --- /dev/null +++ b/testing/src/main/java/dev/cel/testing/testrunner/DefaultResultMatcher.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.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.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 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()); + } + 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()); + } + 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) { + assertThat(exprValue) + .ignoringRepeatedFieldOrderOfFieldDescriptors( + MapValue.getDescriptor().findFieldByName("entries")) + .unpackingAnyUsing( + RegistryUtils.getTypeRegistry(fileDescriptorSetPath), + RegistryUtils.getExtensionRegistry(fileDescriptorSetPath)) + .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..df45f4c2c --- /dev/null +++ b/testing/src/main/java/dev/cel/testing/testrunner/JUnitXmlReporter.java @@ -0,0 +1,230 @@ +// 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.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; + } + + /** Called after all tests are run */ + void onFinish() { + generateReport(); + } + + /** 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() { + 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); + 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); + } + } + + /** 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"; + } +} 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..b2f195606 --- /dev/null +++ b/testing/src/main/java/dev/cel/testing/testrunner/RegistryUtils.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.testing.testrunner; + +import static dev.cel.testing.utils.ProtoDescriptorUtils.getAllDescriptorsFromJvm; + +import com.google.protobuf.Descriptors.FieldDescriptor; +import com.google.protobuf.ExtensionRegistry; +import com.google.protobuf.Message; +import com.google.protobuf.TypeRegistry; +import dev.cel.common.internal.DefaultInstanceMessageFactory; +import java.io.IOException; +import java.util.NoSuchElementException; + +/** Utility class for creating registries from a file descriptor set. */ +public final class RegistryUtils { + + /** Returns the {@link TypeRegistry} for the given file descriptor set. */ + public static TypeRegistry getTypeRegistry(String fileDescriptorSetPath) throws IOException { + return TypeRegistry.newBuilder() + .add(getAllDescriptorsFromJvm(fileDescriptorSetPath).messageTypeDescriptors()) + .build(); + } + + /** Returns the {@link ExtensionRegistry} for the given file descriptor set. */ + public static ExtensionRegistry getExtensionRegistry(String fileDescriptorSetPath) + throws IOException { + ExtensionRegistry extensionRegistry = ExtensionRegistry.newInstance(); + + getAllDescriptorsFromJvm(fileDescriptorSetPath) + .extensionDescriptors() + .forEach( + (descriptorName, descriptor) -> { + if (descriptor.getType().equals(FieldDescriptor.Type.MESSAGE)) { + Message output = + DefaultInstanceMessageFactory.getInstance() + .getPrototype(descriptor.getMessageType()) + .orElseThrow( + () -> + new NoSuchElementException( + "Could not find a default message for: " + + descriptor.getFullName())); + 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..f853b117b --- /dev/null +++ b/testing/src/main/java/dev/cel/testing/testrunner/TestExecutor.java @@ -0,0 +1,289 @@ +// 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.collect.ImmutableList; +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.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; + + for (CelTestSection testSection : testSuite.sections()) { + for (CelTestCase testCase : testSection.tests()) { + String testName = testSection.name() + "." + testCase.name(); + + Object[] parameter = new Object[] {testCase}; + TestWithParameters test = + new TestWithParameters( + testName, new TestClass(testClass), ImmutableList.copyOf(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); + } + } + } + + testContext.done(); + 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..13b660036 --- /dev/null +++ b/testing/src/main/java/dev/cel/testing/testrunner/TestRunnerLibrary.java @@ -0,0 +1,341 @@ +// 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.ExtensionRegistry; +import com.google.protobuf.Message; +import com.google.protobuf.TextFormat; +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.CelOptions; +import dev.cel.common.CelProtoAbstractSyntaxTree; +import dev.cel.common.CelValidationException; +import dev.cel.common.internal.DefaultInstanceMessageFactory; +import dev.cel.policy.CelPolicy; +import dev.cel.policy.CelPolicyCompilerFactory; +import dev.cel.policy.CelPolicyParser; +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 dev.cel.testing.utils.ProtoDescriptorUtils; +import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.logging.Logger; + +/** 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."); + } + } + + @VisibleForTesting + static void evaluateTestCase(CelTestCase testCase, CelTestContext celTestContext) + 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()); + } + evaluate(ast, testCase, celTestContext); + } + + 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. + if (celTestContext.fileDescriptorSetPath().isPresent()) { + extendedCel = + extendedCel + .toCelBuilder() + .addMessageTypes( + ProtoDescriptorUtils.getAllDescriptorsFromJvm( + celTestContext.fileDescriptorSetPath().get()) + .messageTypeDescriptors()) + .setExtensionRegistry( + RegistryUtils.getExtensionRegistry(celTestContext.fileDescriptorSetPath().get())) + .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) + 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); + 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) + 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 program.eval(unpackAny(testCase.input().contextMessage(), celTestContext)); + case CONTEXT_EXPR: + return program.eval(getEvaluatedContextExpr(testCase, celTestContext)); + case BINDINGS: + return program.eval(getBindings(testCase, celTestContext)); + 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 program.eval(newBindings.buildOrThrow()); + } + throw new IllegalArgumentException("Unexpected input type: " + testCase.input().kind()); + } + + private static Message unpackAny(Any any, CelTestContext celTestContext) throws IOException { + if (!celTestContext.fileDescriptorSetPath().isPresent()) { + throw new IllegalArgumentException( + "Proto descriptors are required for unpacking Any messages."); + } + Descriptor descriptor = + RegistryUtils.getTypeRegistry(celTestContext.fileDescriptorSetPath().get()) + .getDescriptorForTypeUrl(any.getTypeUrl()); + return getDefaultInstance(descriptor) + .getParserForType() + .parseFrom( + any.getValue(), + RegistryUtils.getExtensionRegistry(celTestContext.fileDescriptorSetPath().get())); + } + + private static Message getDefaultInstance(Descriptor descriptor) throws IOException { + return DefaultInstanceMessageFactory.getInstance() + .getPrototype(descriptor) + .orElseThrow( + () -> + new NoSuchElementException( + "Could not find a default message for: " + descriptor.getFullName())); + } + + 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.fileDescriptorSetPath().isPresent()) { + return fromValue((Value) value, celTestContext.fileDescriptorSetPath().get()); + } + return fromValue((Value) value); + } + 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..a7e4c628c --- /dev/null +++ b/testing/src/main/java/dev/cel/testing/utils/BUILD.bazel @@ -0,0 +1,58 @@ +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:default_instance_message_factory", + "//common/types", + "//common/types:type_providers", + "//common/values", + "//common/values:cel_byte_string", + "//runtime:unknown_attributes", + "//testing/testrunner:registry_utils", + "@cel_spec//proto/cel/expr:expr_java_proto", + "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto", + "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + "@maven_android//:com_google_protobuf_protobuf_javalite", + ], +) + +java_library( + name = "class_loader_utils", + srcs = ["ClassLoaderUtils.java"], + tags = [ + ], + deps = [ + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + "@maven//:io_github_classgraph_classgraph", + ], +) + +java_library( + name = "proto_descriptor_utils", + srcs = ["ProtoDescriptorUtils.java"], + tags = [ + ], + deps = [ + "//common:cel_descriptors", + "//testing/testrunner:class_loader_utils", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + ], +) diff --git a/testing/src/main/java/dev/cel/testing/utils/ClassLoaderUtils.java b/testing/src/main/java/dev/cel/testing/utils/ClassLoaderUtils.java new file mode 100644 index 000000000..652ec85c6 --- /dev/null +++ b/testing/src/main/java/dev/cel/testing/utils/ClassLoaderUtils.java @@ -0,0 +1,84 @@ +// 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 com.google.common.collect.ImmutableList; +import com.google.protobuf.Descriptors.Descriptor; +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ClassInfo; +import io.github.classgraph.ClassInfoList; +import io.github.classgraph.ScanResult; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.util.logging.Logger; + +/** Utility class for loading classes using {@link ClassGraph}. */ +public final class ClassLoaderUtils { + + private static final Logger logger = Logger.getLogger(ClassLoaderUtils.class.getName()); + + // Using `enableAllInfo()` to scan all class files upfront. This avoids repeated parsing + // of class files by individual methods, improving efficiency. + private static final Supplier CLASS_SCAN_RESULT = + Suppliers.memoize(() -> new ClassGraph().enableAllInfo().scan()); + + /** + * Loads all descriptor type classes from the JVM. + * + * @return A list of {@link Descriptor} objects representing the descriptors loaded from the JVM. + * @throws IOException If there is an error during the loading process. + */ + public static ImmutableList loadDescriptors() throws IOException { + ClassInfoList classInfoList = CLASS_SCAN_RESULT.get().getAllStandardClasses(); + ImmutableList.Builder compileTimeLoadedDescriptors = ImmutableList.builder(); + + for (ClassInfo classInfo : classInfoList) { + try { + Class classInfoClass = classInfo.loadClass(); + Descriptor descriptor = (Descriptor) classInfoClass.getMethod("getDescriptor").invoke(null); + compileTimeLoadedDescriptors.add(descriptor); + } catch (InvocationTargetException e) { + logger.severe( + "Failed to load descriptor: " + classInfo.getName() + " with error: " + e); + } catch (Exception e) { + // Ignore classes that do not have a getDescriptor method. + } + } + return compileTimeLoadedDescriptors.build(); + } + + /** + * Loads all subclasses of the given class from the JVM. + * + * @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..0b27fa452 --- /dev/null +++ b/testing/src/main/java/dev/cel/testing/utils/ExprValueUtils.java @@ -0,0 +1,312 @@ +// 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.ExtensionRegistry; +import com.google.protobuf.Message; +import com.google.protobuf.NullValue; +import com.google.protobuf.TypeRegistry; +import dev.cel.common.CelDescriptorUtil; +import dev.cel.common.CelDescriptors; +import dev.cel.common.internal.DefaultInstanceMessageFactory; +import dev.cel.common.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.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Optional; + +/** Utility class for ExprValue and Value type conversions during test execution. */ +@SuppressWarnings({"UnnecessarilyFullyQualified"}) +public final class ExprValueUtils { + + private ExprValueUtils() {} + + public static final TypeRegistry DEFAULT_TYPE_REGISTRY = newDefaultTypeRegistry(); + public static final ExtensionRegistry DEFAULT_EXTENSION_REGISTRY = newDefaultExtensionRegistry(); + + /** + * Converts a {@link Value} to a Java native object using the given file descriptor set to parse + * `Any` messages. + * + * @param value The {@link Value} to convert. + * @param fileDescriptorSetPath The path to the file descriptor set. + * @return The converted Java object. + * @throws IOException If there's an error during conversion. + */ + public static Object fromValue(Value value, String fileDescriptorSetPath) throws IOException { + if (value.getKindCase().equals(Value.KindCase.OBJECT_VALUE)) { + return parseAny(value.getObjectValue(), fileDescriptorSetPath); + } + return toNativeObject(value); + } + + /** + * Converts a {@link Value} to a Java native object. + * + * @param value The {@link Value} to convert. + * @return The converted Java object. + * @throws IOException If there's an error during conversion. + */ + public static Object fromValue(Value value) throws IOException { + if (value.getKindCase().equals(Value.KindCase.OBJECT_VALUE)) { + Descriptor descriptor = + DEFAULT_TYPE_REGISTRY.getDescriptorForTypeUrl(value.getObjectValue().getTypeUrl()); + Message prototype = getDefaultInstance(descriptor); + return prototype + .getParserForType() + .parseFrom(value.getObjectValue().getValue(), DEFAULT_EXTENSION_REGISTRY); + } + return toNativeObject(value); + } + + private static Object toNativeObject(Value value) 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()), fromValue(entry.getValue())); + } + 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)); + } + 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(), DEFAULT_EXTENSION_REGISTRY); + } + 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 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())); + } + + private static Message parseAny(Any value, String fileDescriptorSetPath) throws IOException { + TypeRegistry typeRegistry = RegistryUtils.getTypeRegistry(fileDescriptorSetPath); + ExtensionRegistry extensionRegistry = RegistryUtils.getExtensionRegistry(fileDescriptorSetPath); + Descriptor descriptor = typeRegistry.getDescriptorForTypeUrl(value.getTypeUrl()); + return unpackAny(value, descriptor, extensionRegistry); + } + + private static Message unpackAny( + Any value, Descriptor descriptor, ExtensionRegistry extensionRegistry) throws IOException { + Message defaultInstance = getDefaultInstance(descriptor); + return defaultInstance.getParserForType().parseFrom(value.getValue(), extensionRegistry); + } + + private static Message getDefaultInstance(Descriptor descriptor) { + return DefaultInstanceMessageFactory.getInstance() + .getPrototype(descriptor) + .orElseThrow( + () -> + new NoSuchElementException( + "Could not find a default message for: " + descriptor.getFullName())); + } + + private static ExtensionRegistry newDefaultExtensionRegistry() { + ExtensionRegistry extensionRegistry = ExtensionRegistry.newInstance(); + dev.cel.expr.conformance.proto2.TestAllTypesExtensions.registerAllExtensions(extensionRegistry); + + return extensionRegistry; + } + + private static TypeRegistry newDefaultTypeRegistry() { + CelDescriptors allDescriptors = + CelDescriptorUtil.getAllDescriptorsFromFileDescriptor( + ImmutableList.of( + dev.cel.expr.conformance.proto2.TestAllTypes.getDescriptor().getFile(), + dev.cel.expr.conformance.proto3.TestAllTypes.getDescriptor().getFile())); + + return TypeRegistry.newBuilder().add(allDescriptors.messageTypeDescriptors()).build(); + } +} diff --git a/testing/src/main/java/dev/cel/testing/utils/ProtoDescriptorUtils.java b/testing/src/main/java/dev/cel/testing/utils/ProtoDescriptorUtils.java new file mode 100644 index 000000000..b6fa2e64b --- /dev/null +++ b/testing/src/main/java/dev/cel/testing/utils/ProtoDescriptorUtils.java @@ -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. +package dev.cel.testing.utils; + +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.collect.ImmutableSet.toImmutableSet; +import static dev.cel.testing.utils.ClassLoaderUtils.loadDescriptors; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.io.Files; +import com.google.protobuf.DescriptorProtos.FileDescriptorSet; +import com.google.protobuf.Descriptors.Descriptor; +import com.google.protobuf.Descriptors.FileDescriptor; +import com.google.protobuf.ExtensionRegistry; +import dev.cel.common.CelDescriptorUtil; +import dev.cel.common.CelDescriptors; +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 JVM. + * + * @return The {@link CelDescriptors} object containing all the descriptors. + */ + public static CelDescriptors getAllDescriptorsFromJvm(String fileDescriptorSetPath) + throws IOException { + ImmutableList compileTimeLoadedDescriptors = loadDescriptors(); + FileDescriptorSet fileDescriptorSet = getFileDescriptorSet(fileDescriptorSetPath); + ImmutableSet runtimeFileDescriptorNames = + CelDescriptorUtil.getFileDescriptorsFromFileDescriptorSet(fileDescriptorSet).stream() + .map(FileDescriptor::getFullName) + .collect(toImmutableSet()); + + // Get all the file descriptors from the descriptors which are loaded from the JVM and use the + // ones which match the ones provided by the user in the file descriptor set. + ImmutableList userProvidedFileDescriptors = + CelDescriptorUtil.getFileDescriptorsForDescriptors(compileTimeLoadedDescriptors).stream() + .filter( + fileDescriptor -> runtimeFileDescriptorNames.contains(fileDescriptor.getFullName())) + .collect(toImmutableList()); + + // Get all the descriptors from the file descriptors above which include nested, extension and + // message type descriptors as well. + return CelDescriptorUtil.getAllDescriptorsFromFileDescriptor(userProvidedFileDescriptors); + } + + 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..db1202aa8 --- /dev/null +++ b/testing/src/test/java/dev/cel/testing/testrunner/BUILD.bazel @@ -0,0 +1,357 @@ +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, + default_visibility = [ + "//testing/testrunner:__pkg__", + ], +) + +# 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 = [ + "//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:junit_xml_reporter", + "@maven//:junit_junit", + "@maven//:org_mockito_mockito_core", + ], +) + +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 = "test_runner_sample_yaml", + cel_expr = "nested_rule/policy.yaml", + proto_deps = [ + "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_proto", + "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_proto", + ], + test_data_path = "//testing/src/test/resources/policy", + test_src = ":user_test", + test_suite = "nested_rule/testrunner_tests.yaml", + deps = [ + "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto", + "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", + ], +) + +cel_java_test( + name = "unknown_set_yaml", + cel_expr = "nested_rule/policy.yaml", + proto_deps = [ + "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_proto", + "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_proto", + ], + test_data_path = "//testing/src/test/resources/policy", + test_src = ":user_test", + test_suite = "nested_rule/testrunner_unknown_output_tests.yaml", +) + +cel_java_test( + name = "custom_variable_binding_test_runner_sample", + cel_expr = "custom_variable_bindings/policy.yaml", + config = "custom_variable_bindings/config.yaml", + proto_deps = [ + "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_proto", + "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_proto", + ], + test_data_path = "//testing/src/test/resources/policy", + test_src = ":custom_variable_binding_user_test", + test_suite = "custom_variable_bindings/tests.yaml", + 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 = "context_pb_user_test_runner_sample", + cel_expr = "context_pb/policy.yaml", + config = "context_pb/config.yaml", + proto_deps = [ + "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_proto", + "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_proto", + ], + test_data_path = "//testing/src/test/resources/policy", + test_src = ":context_pb_user_test", + test_suite = "context_pb/tests.yaml", + deps = [ + "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto", + "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", + ], +) + +cel_java_test( + name = "additional_config_test_runner_sample", + cel_expr = "nested_rule/policy.yaml", + config = "nested_rule/config.yaml", + proto_deps = [ + "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_proto", + "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_proto", + ], + test_data_path = "//testing/src/test/resources/policy", + test_src = ":env_config_user_test", + test_suite = "nested_rule/testrunner_tests.textproto", + deps = [ + "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto", + "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", + ], +) + +cel_java_test( + name = "test_runner_sample", + cel_expr = "nested_rule/policy.yaml", + proto_deps = [ + "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_proto", + "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_proto", + ], + test_data_path = "//testing/src/test/resources/policy", + test_src = ":user_test", + test_suite = "nested_rule/testrunner_tests.textproto", + deps = [ + "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto", + "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", + ], +) + +cel_java_test( + name = "test_runner_sample_with_expr_value_output", + cel_expr = "expr_value_output/policy.yaml", + 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 = "context_message_user_test_runner_textproto_sample", + cel_expr = "context_pb/policy.yaml", + config = "context_pb/config.yaml", + proto_deps = [ + "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_proto", + "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_proto", + ], + test_data_path = "//testing/src/test/resources/policy", + test_src = ":context_pb_user_test", + test_suite = "context_pb/context_msg_tests.textproto", + deps = [ + "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto", + "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", + ], +) + +cel_java_test( + name = "context_pb_user_test_runner_textproto_sample", + cel_expr = "context_pb/policy.yaml", + config = "context_pb/config.yaml", + proto_deps = [ + "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_proto", + "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_proto", + ], + test_data_path = "//testing/src/test/resources/policy", + test_src = ":context_pb_user_test", + test_suite = "context_pb/tests.textproto", + deps = [ + "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto", + "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", + ], +) + +cel_java_test( + name = "raw_expression_test", + cel_expr = "2 + 2 == 4", + 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", +) 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/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..707b5eef9 --- /dev/null +++ b/testing/src/test/java/dev/cel/testing/testrunner/CustomVariableBindingUserTest.java @@ -0,0 +1,43 @@ +// 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.Any; +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( + CelTestContext.newBuilder() + .setVariableBindings( + ImmutableMap.of( + "spec", + Any.pack( + 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..efdd9fedc --- /dev/null +++ b/testing/src/test/java/dev/cel/testing/testrunner/JUnitXmlReporterTest.java @@ -0,0 +1,126 @@ +// 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\""); + } +} 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..8c1129b0c --- /dev/null +++ b/testing/src/test/java/dev/cel/testing/testrunner/TestRunnerLibraryTest.java @@ -0,0 +1,266 @@ +// 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 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 are required for unpacking Any messages."); + } +} 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..8f6829838 --- /dev/null +++ b/testing/src/test/resources/environment/dump_env.yaml @@ -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. + +name: dump_env +description: dump_env description +container: test.container +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: 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 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..fbed2b9d5 --- /dev/null +++ b/testing/src/test/resources/environment/extended_env.yaml @@ -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. + +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" +functions: + - name: "isEmpty" + overloads: + - id: "wrapper_string_isEmpty" + target: + type_name: "google.protobuf.StringValue" + return: + type_name: "bool" + - id: "list_isEmpty" + target: + type_name: "list" + params: + - type_name: "T" + is_type_param: true + return: + type_name: "bool" +# 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 +# TODO: Add support for below +#features: +#- name: cel.feature.macro_call_tracking +# enabled: true \ No newline at end of file diff --git a/testing/src/test/resources/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/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/compile_errors/config.yaml b/testing/src/test/resources/policy/compile_errors/config.yaml new file mode 100644 index 000000000..b9c8f9750 --- /dev/null +++ b/testing/src/test/resources/policy/compile_errors/config.yaml @@ -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. + +name: "labels" +extensions: + - name: "sets" +variables: + - name: "destination.ip" + type: + type_name: "string" + - name: "origin.ip" + type: + type_name: "string" + - name: "spec.restricted_destinations" + type: + type_name: "list" + params: + - type_name: "string" + - name: "spec.origin" + type: + type_name: "string" + - name: "request" + type: + type_name: "map" + params: + - type_name: "string" + - type_name: "dyn" + - name: "resource" + type: + type_name: "map" + params: + - type_name: "string" + - type_name: "dyn" +functions: + - name: "locationCode" + overloads: + - id: "locationCode_string" + args: + - type_name: "string" + return: + type_name: "string" diff --git a/testing/src/test/resources/policy/compile_errors/expected_errors.baseline b/testing/src/test/resources/policy/compile_errors/expected_errors.baseline new file mode 100644 index 000000000..850ecce9d --- /dev/null +++ b/testing/src/test/resources/policy/compile_errors/expected_errors.baseline @@ -0,0 +1,30 @@ +ERROR: compile_errors/policy.yaml:19:5: Error configuring import: invalid qualified name: punc.Import!, wanted name of the form 'qualified.name' + | punc.Import! + | ....^ +ERROR: compile_errors/policy.yaml:20:10: Error configuring import: invalid qualified name: bad import, wanted name of the form 'qualified.name' + | - name: "bad import" + | .........^ +ERROR: compile_errors/policy.yaml:24:19: undeclared reference to 'spec' (in container '') + | expression: spec.labels + | ..................^ +ERROR: compile_errors/policy.yaml:26:50: mismatched input 'resource' expecting {'==', '!=', 'in', '<', '<=', '>=', '>', '&&', '||', '[', ')', '.', '-', '?', '+', '*', '/', '%%'} + | expression: variables.want.filter(l, !(lin resource.labels)) + | .................................................^ +ERROR: compile_errors/policy.yaml:26:66: extraneous input ')' expecting + | expression: variables.want.filter(l, !(lin resource.labels)) + | .................................................................^ +ERROR: compile_errors/policy.yaml:28:27: mismatched input '2' expecting {'}', ','} + | expression: "{1:305 2:569}" + | ..........................^ +ERROR: compile_errors/policy.yaml:36:75: extraneous input ']' expecting ')' + | "missing one or more required labels: %s".format(variables.missing]) + | ..........................................................................^ +ERROR: compile_errors/policy.yaml:39:67: undeclared reference to 'format' (in container '') + | "invalid values provided on one or more labels: %s".format([variables.invalid]) + | ..................................................................^ +ERROR: compile_errors/policy.yaml:40:19: condition must produce a boolean output. + | - condition: '1' + | ..................^ +ERROR: compile_errors/policy.yaml:43:24: found no matching overload for '_==_' applied to '(bool, string)' (candidates: (%A0, %A0)) + | - condition: false == "0" + | .......................^ \ No newline at end of file diff --git a/testing/src/test/resources/policy/compile_errors/policy.yaml b/testing/src/test/resources/policy/compile_errors/policy.yaml new file mode 100644 index 000000000..c17cd3056 --- /dev/null +++ b/testing/src/test/resources/policy/compile_errors/policy.yaml @@ -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. + +name: "errors" +imports: +- name: " untrimmed.Import1 " +- name: > + punc.Import! +- name: "bad import" +rule: + variables: + - name: want + expression: spec.labels + - name: missing + expression: variables.want.filter(l, !(lin resource.labels)) + - name: bad_data + expression: "{1:305 2:569}" + - name: invalid + expression: > + resource.labels.filter(l, + l in variables.want && variables.want[l] != resource.labels[l]) + match: + - condition: variables.missing.size() > 0 + output: | + "missing one or more required labels: %s".format(variables.missing]) + - condition: variables.invalid.size() > 0 + output: | + "invalid values provided on one or more labels: %s".format([variables.invalid]) + - condition: '1' + output: | + "condition wrong type" + - condition: false == "0" + output: | + "condition type-check failure" diff --git a/testing/src/test/resources/policy/compose_errors_conflicting_output/config.yaml b/testing/src/test/resources/policy/compose_errors_conflicting_output/config.yaml new file mode 100644 index 000000000..5d048a225 --- /dev/null +++ b/testing/src/test/resources/policy/compose_errors_conflicting_output/config.yaml @@ -0,0 +1,22 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "labels" +variables: +- name: "resource" + type: + type_name: "map" + params: + - type_name: "string" + - type_name: "dyn" diff --git a/testing/src/test/resources/policy/compose_errors_conflicting_output/expected_errors.baseline b/testing/src/test/resources/policy/compose_errors_conflicting_output/expected_errors.baseline new file mode 100644 index 000000000..3e2624b64 --- /dev/null +++ b/testing/src/test/resources/policy/compose_errors_conflicting_output/expected_errors.baseline @@ -0,0 +1,6 @@ +ERROR: compose_errors_conflicting_output/policy.yaml:22:14: conflicting output types found. + | output: "false" + | .............^ +ERROR: compose_errors_conflicting_output/policy.yaml:23:14: conflicting output types found. + | - output: "{'banned': true}" + | .............^ \ No newline at end of file diff --git a/testing/src/test/resources/policy/compose_errors_conflicting_output/policy.yaml b/testing/src/test/resources/policy/compose_errors_conflicting_output/policy.yaml new file mode 100644 index 000000000..a5ed5c09c --- /dev/null +++ b/testing/src/test/resources/policy/compose_errors_conflicting_output/policy.yaml @@ -0,0 +1,23 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: nested_rule +rule: + variables: + - name: "permitted_regions" + expression: "['us', 'uk', 'es']" + match: + - condition: resource.origin in variables.permitted_regions + output: "false" + - output: "{'banned': true}" diff --git a/testing/src/test/resources/policy/compose_errors_conflicting_subrule/config.yaml b/testing/src/test/resources/policy/compose_errors_conflicting_subrule/config.yaml new file mode 100644 index 000000000..5d048a225 --- /dev/null +++ b/testing/src/test/resources/policy/compose_errors_conflicting_subrule/config.yaml @@ -0,0 +1,22 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "labels" +variables: +- name: "resource" + type: + type_name: "map" + params: + - type_name: "string" + - type_name: "dyn" diff --git a/testing/src/test/resources/policy/compose_errors_conflicting_subrule/expected_errors.baseline b/testing/src/test/resources/policy/compose_errors_conflicting_subrule/expected_errors.baseline new file mode 100644 index 000000000..559d62e1d --- /dev/null +++ b/testing/src/test/resources/policy/compose_errors_conflicting_subrule/expected_errors.baseline @@ -0,0 +1,3 @@ +ERROR: compose_errors_conflicting_subrule/policy.yaml:36:14: failed composing the subrule 'banned regions' due to conflicting output types. + | output: "{'banned': false}" + | .............^ \ No newline at end of file diff --git a/testing/src/test/resources/policy/compose_errors_conflicting_subrule/policy.yaml b/testing/src/test/resources/policy/compose_errors_conflicting_subrule/policy.yaml new file mode 100644 index 000000000..9df1df8d0 --- /dev/null +++ b/testing/src/test/resources/policy/compose_errors_conflicting_subrule/policy.yaml @@ -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. + +name: nested_rule +rule: + variables: + - name: "permitted_regions" + expression: "['us', 'uk', 'es']" + match: + - rule: + id: "banned regions" + description: > + determine whether the resource origin is in the banned + list. If the region is also in the permitted list, the + ban has no effect. + variables: + - name: "banned_regions" + expression: "{'us': false, 'ru': false, 'ir': false}" + match: + - condition: | + resource.origin in variables.banned_regions && + !(resource.origin in variables.permitted_regions) + output: "true" + - condition: resource.origin in variables.permitted_regions + output: "{'banned': false}" + - output: "{'banned': true}" diff --git a/testing/src/test/resources/policy/context_pb/config.yaml b/testing/src/test/resources/policy/context_pb/config.yaml new file mode 100644 index 000000000..2ca7fac42 --- /dev/null +++ b/testing/src/test/resources/policy/context_pb/config.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: "context_pb" +container: "cel.expr.conformance.proto3" +extensions: + - name: "strings" + version: "latest" \ No newline at end of file diff --git a/testing/src/test/resources/policy/context_pb/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/context_pb/policy.yaml b/testing/src/test/resources/policy/context_pb/policy.yaml new file mode 100644 index 000000000..8111bb76c --- /dev/null +++ b/testing/src/test/resources/policy/context_pb/policy.yaml @@ -0,0 +1,25 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "context_pb" +rule: + match: + - condition: > + single_int32 > TestAllTypes{single_int64: 10}.single_int64 + output: | + ["invalid spec, got single_int32=" , single_int32 , ", wanted <= 10"].join() + - condition: > + standalone_enum == TestAllTypes.NestedEnum.BAR + output: | + "invalid spec, no nested enums may refer to BAR" \ No newline at end of file diff --git a/testing/src/test/resources/policy/context_pb/tests.textproto b/testing/src/test/resources/policy/context_pb/tests.textproto new file mode 100644 index 000000000..77f97fa7c --- /dev/null +++ b/testing/src/test/resources/policy/context_pb/tests.textproto @@ -0,0 +1,19 @@ +# proto-file: google3/third_party/cel/spec/proto/cel/expr/conformance/test/suite.proto +# proto-message: cel.expr.conformance.test.TestSuite + +name: "context_pb_tests" +description: "Protobuf input tests" +sections { + name: "valid" + description: "Valid protobuf input tests" + tests { + name: "good spec" + description: "Valid protobuf input tests" + input_context { + context_expr: "TestAllTypes{single_int32: 10}" + } + output { + result_expr: "optional.none()" + } + } +} diff --git a/testing/src/test/resources/policy/context_pb/tests.yaml b/testing/src/test/resources/policy/context_pb/tests.yaml new file mode 100644 index 000000000..d37e2bee3 --- /dev/null +++ b/testing/src/test/resources/policy/context_pb/tests.yaml @@ -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. + +name: "context_pb_cel_tests" +description: "Protobuf input tests" +sections: + - name: "valid" + description: "Valid context_expr" + tests: + - name: "good spec" + description: "good spec" + context_expr: + "TestAllTypes{single_int32: 10}" + output: + expr: "optional.none()" + - name: "invalid" + description: "Invalid context_expr" + tests: + - name: "bad spec" + description: "bad spec" + context_expr: + "TestAllTypes{single_int32: 11}" + output: + value: "invalid spec, got single_int32=11, wanted <= 10" \ No newline at end of file diff --git a/testing/src/test/resources/policy/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/errors_unreachable/config.yaml b/testing/src/test/resources/policy/errors_unreachable/config.yaml new file mode 100644 index 000000000..8f79bb763 --- /dev/null +++ b/testing/src/test/resources/policy/errors_unreachable/config.yaml @@ -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. + +name: "errors_unreachable" +extensions: +- name: "sets" +- name: "strings" + version: "latest" +variables: +- name: "destination.ip" + type: + type_name: "string" +- name: "origin.ip" + type: + type_name: "string" +- name: "spec.restricted_destinations" + type: + type_name: "list" + params: + - type_name: "string" +- name: "spec.origin" + type: + type_name: "string" +- name: "request" + type: + type_name: "map" + params: + - type_name: "string" + - type_name: "dyn" +- name: "resource" + type: + type_name: "map" + params: + - type_name: "string" + - type_name: "dyn" +functions: +- name: "locationCode" + overloads: + - id: "locationCode_string" + args: + - type_name: "string" + return: + type_name: "string" diff --git a/testing/src/test/resources/policy/errors_unreachable/expected_errors.baseline b/testing/src/test/resources/policy/errors_unreachable/expected_errors.baseline new file mode 100644 index 000000000..f5f24acbe --- /dev/null +++ b/testing/src/test/resources/policy/errors_unreachable/expected_errors.baseline @@ -0,0 +1,6 @@ +ERROR: errors_unreachable/policy.yaml:36:9: Match creates unreachable outputs + | - output: | + | ........^ +ERROR: errors_unreachable/policy.yaml:28:7: Rule creates unreachable outputs + | match: + | ......^ \ No newline at end of file diff --git a/testing/src/test/resources/policy/errors_unreachable/policy.yaml b/testing/src/test/resources/policy/errors_unreachable/policy.yaml new file mode 100644 index 000000000..f43fd62c7 --- /dev/null +++ b/testing/src/test/resources/policy/errors_unreachable/policy.yaml @@ -0,0 +1,39 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "errors_unreachable" +rule: + variables: + - name: want + expression: request.labels + - name: missing + expression: variables.want.filter(l, !(l in resource.labels)) + - name: invalid + expression: > + resource.labels.filter(l, + l in variables.want && variables.want[l] != resource.labels[l]) + match: + - rule: + match: + - output: "''" + - condition: variables.missing.size() > 0 + output: | + "missing one or more required labels: [\"" + variables.missing.join(',') + "\"]" + - condition: variables.invalid.size() > 0 + rule: + match: + - output: | + "invalid values provided on one or more labels: [\"" + variables.invalid.join(',') + "\"]" + - condition: "false" + output: "'unreachable'" diff --git a/testing/src/test/resources/policy/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/k8s/config.yaml b/testing/src/test/resources/policy/k8s/config.yaml new file mode 100644 index 000000000..4df8439ea --- /dev/null +++ b/testing/src/test/resources/policy/k8s/config.yaml @@ -0,0 +1,33 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: k8s +extensions: +- name: "strings" + version: 2 +variables: +- name: "resource.labels" + type: + type_name: "map" + params: + - type_name: "string" + - type_name: "string" +- name: "resource.containers" + type: + type_name: "list" + params: + - type_name: "string" +- name: "resource.namespace" + type: + type_name: "string" diff --git a/testing/src/test/resources/policy/k8s/policy.yaml b/testing/src/test/resources/policy/k8s/policy.yaml new file mode 100644 index 000000000..9cc9782fa --- /dev/null +++ b/testing/src/test/resources/policy/k8s/policy.yaml @@ -0,0 +1,36 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: k8s +kind: ValidatingAdmissionPolicy +metadata: + name: "policy.cel.dev" +spec: + failurePolicy: Fail + matchConstraints: + resourceRules: + - apiGroups: ["services"] + apiVersions: ["v3"] + operations: ["CREATE", "UPDATE"] + variables: + - name: env + expression: "resource.labels.?environment.orValue('prod')" + - name: break_glass + expression: "resource.labels.?break_glass.orValue('false') == 'true'" + validations: + - expression: > + variables.break_glass || + resource.containers.all(c, c.startsWith(variables.env + '.')) + messageExpression: > + 'only ' + variables.env + ' containers are allowed in namespace ' + resource.namespace diff --git a/testing/src/test/resources/policy/k8s/tests.yaml b/testing/src/test/resources/policy/k8s/tests.yaml new file mode 100644 index 000000000..8585c5efb --- /dev/null +++ b/testing/src/test/resources/policy/k8s/tests.yaml @@ -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. + +description: K8s admission control tests +section: +- name: "invalid" + tests: + - name: "restricted_container" + input: + resource.namespace: + value: "dev.cel" + resource.labels: + value: + environment: "staging" + resource.containers: + value: + - staging.dev.cel.container1 + - staging.dev.cel.container2 + - preprod.dev.cel.container3 + output: "'only staging containers are allowed in namespace dev.cel'" diff --git a/testing/src/test/resources/policy/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/limits/config.yaml b/testing/src/test/resources/policy/limits/config.yaml new file mode 100644 index 000000000..fa6fc737c --- /dev/null +++ b/testing/src/test/resources/policy/limits/config.yaml @@ -0,0 +1,22 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "limits" +extensions: +- name: "strings" + version: latest +variables: +- name: "now" + type: + type_name: "google.protobuf.Timestamp" \ No newline at end of file diff --git a/testing/src/test/resources/policy/limits/policy.yaml b/testing/src/test/resources/policy/limits/policy.yaml new file mode 100644 index 000000000..13c47c39b --- /dev/null +++ b/testing/src/test/resources/policy/limits/policy.yaml @@ -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. + +name: "limits" +rule: + variables: + - name: "greeting" + expression: "'hello'" + - name: "farewell" + expression: "'goodbye'" + - name: "person" + expression: "'me'" + - name: "message_fmt" + expression: "'%s, %s'" + match: + - condition: | + now.getHours() >= 20 + rule: + id: "farewells" + variables: + - name: "message" + expression: > + variables.farewell + ', ' + variables.person +# TODO: replace when string.format is available +# variables.message_fmt.format([variables.farewell, +# variables.person]) + match: + - condition: > + now.getHours() < 21 + output: variables.message + "!" + - condition: > + now.getHours() < 22 + output: variables.message + "!!" + - condition: > + now.getHours() < 24 + output: variables.message + "!!!" + - output: > + variables.greeting + ', ' + variables.person +# variables.message_fmt.format([variables.greeting, variables.person]) TODO: replace when string.format is available \ No newline at end of file diff --git a/testing/src/test/resources/policy/limits/tests.yaml b/testing/src/test/resources/policy/limits/tests.yaml new file mode 100644 index 000000000..fe6daa61d --- /dev/null +++ b/testing/src/test/resources/policy/limits/tests.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. + +description: Limits related tests +section: +- name: "now_after_hours" + tests: + - name: "7pm" + input: + now: + expr: "timestamp('2024-07-30T00:30:00Z')" + output: "'hello, me'" + - name: "8pm" + input: + now: + expr: "timestamp('2024-07-30T20:30:00Z')" + output: "'goodbye, me!'" + - name: "9pm" + input: + now: + expr: "timestamp('2024-07-30T21:30:00Z')" + output: "'goodbye, me!!'" + - name: "11pm" + input: + now: + expr: "timestamp('2024-07-30T23:30:00Z')" + output: "'goodbye, me!!!'" \ No newline at end of file diff --git a/testing/src/test/resources/policy/nested_rule/config.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/config.yaml b/testing/src/test/resources/policy/nested_rule/config.yaml new file mode 100644 index 000000000..bfd94b33c --- /dev/null +++ b/testing/src/test/resources/policy/nested_rule/config.yaml @@ -0,0 +1,22 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "nested_rule" +variables: + - name: "resource" + type: + type_name: "map" + params: + - type_name: "string" + - type_name: "dyn" diff --git a/testing/src/test/resources/policy/nested_rule/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/policy.yaml b/testing/src/test/resources/policy/nested_rule/policy.yaml new file mode 100644 index 000000000..2fc566b85 --- /dev/null +++ b/testing/src/test/resources/policy/nested_rule/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 +rule: + variables: + - name: "permitted_regions" + expression: "['us', 'uk', 'es']" + match: + - rule: + id: "banned regions" + description: > + determine whether the resource origin is in the banned + list. If the region is also in the permitted list, the + ban has no effect. + variables: + - name: "banned_regions" + expression: "{'us': false, 'ru': false, 'ir': false}" + match: + - condition: | + resource.origin in variables.banned_regions && + !(resource.origin in variables.permitted_regions) + output: "{'banned': true}" + - condition: resource.origin in variables.permitted_regions + output: "{'banned': false}" + - output: "{'banned': true}" + explanation: "'resource is in the banned region ' + resource.origin" \ No newline at end of file diff --git a/testing/src/test/resources/policy/nested_rule/testrunner_tests.textproto b/testing/src/test/resources/policy/nested_rule/testrunner_tests.textproto new file mode 100644 index 000000000..9a8dc691e --- /dev/null +++ b/testing/src/test/resources/policy/nested_rule/testrunner_tests.textproto @@ -0,0 +1,79 @@ +# proto-file: google3/third_party/cel/spec/proto/cel/expr/conformance/test/suite.proto +# proto-message: cel.expr.conformance.test.TestSuite + +name: "nested_rule" +description: "Nested rule conformance tests" +sections { + name: "valid" + description: "Valid nested rule" + tests { + name: "restricted_origin" + description: "Restricted origin" + input { + key: "resource" + value { + value { + object_value { + [type.googleapis.com/google.protobuf.Struct] { + fields { + key: "origin" + value { string_value: "ir" } + } + } + } + } + } + } + output { + result_expr: "{'banned': true}" + } + } + tests { + name: "by_default" + description: "By default" + input { + key: "resource" + value { + value { + object_value { + [type.googleapis.com/google.protobuf.Struct] { + fields { + key: "origin" + value { string_value: "'de'" } + } + } + } + } + } + } + output { + result_expr: "{'banned': true}" + } + } +} + +sections { + name: "permitted" + description: "Permitted nested rule" + tests { + name: "valid_origin" + input { + key: "resource" + value { + value { + object_value { + [type.googleapis.com/google.protobuf.Struct] { + fields { + key: "origin" + value { string_value: "uk" } + } + } + } + } + } + } + output { + result_expr: "{'banned': false}" + } + } +} \ No newline at end of file diff --git a/testing/src/test/resources/policy/nested_rule/testrunner_tests.yaml b/testing/src/test/resources/policy/nested_rule/testrunner_tests.yaml new file mode 100644 index 000000000..414784ad8 --- /dev/null +++ b/testing/src/test/resources/policy/nested_rule/testrunner_tests.yaml @@ -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. + +name: "nested_rule" +description: Nested rule conformance tests +sections: + - name: "banned" + description: "Tests for the banned section." + tests: + - name: "restricted_origin" + description: "Tests that the ir origin is restricted." + input: + resource: + value: + origin: "ir" + output: + expr: "{'banned': true}" + - name: "by_default" + description: "Tests that the de origin is restricted." + input: + resource: + value: + origin: "de" + output: + expr: "{'banned': true}" + - name: "permitted" + description: "Tests for the permitted section." + tests: + - name: "valid_origin" + description: "Tests that the valid origin is permitted." + input: + resource: + value: + origin: "uk" + output: + expr: "{'banned': false}" diff --git a/testing/src/test/resources/policy/nested_rule/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/nested_rule/tests.yaml b/testing/src/test/resources/policy/nested_rule/tests.yaml new file mode 100644 index 000000000..a9807c376 --- /dev/null +++ b/testing/src/test/resources/policy/nested_rule/tests.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. + +description: Nested rule conformance tests +section: + - name: "banned" + tests: + - name: "restricted_origin" + input: + resource: + value: + origin: "ir" + output: "{'banned': true}" + - name: "by_default" + input: + resource: + value: + origin: "de" + output: "{'banned': true}" + - name: "permitted" + tests: + - name: "valid_origin" + input: + resource: + value: + origin: "uk" + output: "{'banned': false}" diff --git a/testing/src/test/resources/policy/nested_rule2/config.yaml b/testing/src/test/resources/policy/nested_rule2/config.yaml new file mode 100644 index 000000000..9ee6f0e49 --- /dev/null +++ b/testing/src/test/resources/policy/nested_rule2/config.yaml @@ -0,0 +1,22 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "nested_rule2" +variables: +- name: "resource" + type: + type_name: "map" + params: + - type_name: "string" + - type_name: "dyn" \ No newline at end of file diff --git a/testing/src/test/resources/policy/nested_rule2/policy.yaml b/testing/src/test/resources/policy/nested_rule2/policy.yaml new file mode 100644 index 000000000..fef91869f --- /dev/null +++ b/testing/src/test/resources/policy/nested_rule2/policy.yaml @@ -0,0 +1,40 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: nested_rule2 +rule: + variables: + - name: "permitted_regions" + expression: "['us', 'uk', 'es']" + match: + - condition: resource.?user.orValue("").startsWith("bad") + rule: + id: "banned regions" + description: > + determine whether the resource origin is in the banned + list. If the region is also in the permitted list, the + ban has no effect. + variables: + - name: "banned_regions" + expression: "{'us': false, 'ru': false, 'ir': false}" + match: + - condition: | + resource.origin in variables.banned_regions && + !(resource.origin in variables.permitted_regions) + output: "{'banned': 'restricted_region'}" + explanation: "'resource is in the banned region ' + resource.origin" + - output: "{'banned': 'bad_actor'}" + - condition: "!(resource.origin in variables.permitted_regions)" + output: "{'banned': 'unconfigured_region'}" + - output: "{}" \ No newline at end of file diff --git a/testing/src/test/resources/policy/nested_rule2/tests.yaml b/testing/src/test/resources/policy/nested_rule2/tests.yaml new file mode 100644 index 000000000..b5fbba745 --- /dev/null +++ b/testing/src/test/resources/policy/nested_rule2/tests.yaml @@ -0,0 +1,48 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +description: Nested rule conformance tests +section: +- name: "banned" + tests: + - name: "restricted_origin" + input: + resource: + value: + user: "bad-user" + origin: "ir" + output: "{'banned': 'restricted_region'}" + - name: "by_default" + input: + resource: + value: + user: "bad-user" + origin: "de" + output: "{'banned': 'bad_actor'}" + - name: "unconfigured_region" + input: + resource: + value: + user: "good-user" + origin: "de" + output: "{'banned': 'unconfigured_region'}" +- name: "permitted" + tests: + - name: "valid_origin" + input: + resource: + value: + user: "good-user" + origin: "uk" + output: "{}" \ No newline at end of file diff --git a/testing/src/test/resources/policy/nested_rule3/config.yaml b/testing/src/test/resources/policy/nested_rule3/config.yaml new file mode 100644 index 000000000..d9360d5c9 --- /dev/null +++ b/testing/src/test/resources/policy/nested_rule3/config.yaml @@ -0,0 +1,22 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "nested_rule3" +variables: +- name: "resource" + type: + type_name: "map" + params: + - type_name: "string" + - type_name: "dyn" \ No newline at end of file diff --git a/testing/src/test/resources/policy/nested_rule3/policy.yaml b/testing/src/test/resources/policy/nested_rule3/policy.yaml new file mode 100644 index 000000000..4ad765c8d --- /dev/null +++ b/testing/src/test/resources/policy/nested_rule3/policy.yaml @@ -0,0 +1,39 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: nested_rule3 +rule: + variables: + - name: "permitted_regions" + expression: "['us', 'uk', 'es']" + match: + - condition: resource.?user.orValue("").startsWith("bad") + rule: + id: "banned regions" + description: > + determine whether the resource origin is in the banned + list. If the region is also in the permitted list, the + ban has no effect. + variables: + - name: "banned_regions" + expression: "{'us': false, 'ru': false, 'ir': false}" + match: + - condition: | + resource.origin in variables.banned_regions && + !(resource.origin in variables.permitted_regions) + output: "{'banned': 'restricted_region'}" + explanation: "'resource is in the banned region ' + resource.origin" + - output: "{'banned': 'bad_actor'}" + - condition: "!(resource.origin in variables.permitted_regions)" + output: "{'banned': 'unconfigured_region'}" \ No newline at end of file diff --git a/testing/src/test/resources/policy/nested_rule3/tests.yaml b/testing/src/test/resources/policy/nested_rule3/tests.yaml new file mode 100644 index 000000000..b10785d0c --- /dev/null +++ b/testing/src/test/resources/policy/nested_rule3/tests.yaml @@ -0,0 +1,48 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +description: Nested rule conformance tests +section: +- name: "banned" + tests: + - name: "restricted_origin" + input: + resource: + value: + user: "bad-user" + origin: "ir" + output: "{'banned': 'restricted_region'}" + - name: "by_default" + input: + resource: + value: + user: "bad-user" + origin: "de" + output: "{'banned': 'bad_actor'}" + - name: "unconfigured_region" + input: + resource: + value: + user: "good-user" + origin: "de" + output: "{'banned': 'unconfigured_region'}" +- name: "permitted" + tests: + - name: "valid_origin" + input: + resource: + value: + user: "good-user" + origin: "uk" + output: "optional.none()" \ No newline at end of file diff --git a/testing/src/test/resources/policy/pb/config.yaml b/testing/src/test/resources/policy/pb/config.yaml new file mode 100644 index 000000000..d13ce2ae1 --- /dev/null +++ b/testing/src/test/resources/policy/pb/config.yaml @@ -0,0 +1,23 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "pb" +container: "cel.expr.conformance.proto3" +extensions: +- name: "strings" + version: 2 +variables: +- name: "spec" + type: + type_name: "cel.expr.conformance.proto3.TestAllTypes" diff --git a/testing/src/test/resources/policy/pb/policy.yaml b/testing/src/test/resources/policy/pb/policy.yaml new file mode 100644 index 000000000..5d2b1d22a --- /dev/null +++ b/testing/src/test/resources/policy/pb/policy.yaml @@ -0,0 +1,36 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "pb" + +imports: +- name: cel.expr.conformance.proto3.TestAllTypes +- name: cel.expr.conformance.proto3.TestAllTypes.NestedEnum + # Note: Following enum is CEL-Java only. +- name: | + dev.cel.testing.testdata.proto3.StandaloneGlobalEnum + +rule: + match: + - condition: > + spec.single_int32 > TestAllTypes{single_int64: 10}.single_int64 + output: | + "invalid spec, got single_int32=" + string(spec.single_int32) + ", wanted <= 10" +# TODO: replace when string.format is available +# "invalid spec, got single_int32=%d, wanted <= 10".format([spec.single_int32]) + - condition: > + spec.standalone_enum == NestedEnum.BAR || + StandaloneGlobalEnum.SGAR == StandaloneGlobalEnum.SGOO + output: | + "invalid spec, neither nested nor imported enums may refer to BAR" \ No newline at end of file diff --git a/testing/src/test/resources/policy/pb/tests.yaml b/testing/src/test/resources/policy/pb/tests.yaml new file mode 100644 index 000000000..82dd6b11b --- /dev/null +++ b/testing/src/test/resources/policy/pb/tests.yaml @@ -0,0 +1,33 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +description: "Protobuf input tests" +section: +- name: "valid" + tests: + - name: "good spec" + input: + spec: + expr: > + TestAllTypes{single_int32: 10} + output: "optional.none()" +- name: "invalid" + tests: + - name: "bad spec" + input: + spec: + expr: > + TestAllTypes{single_int32: 11} + output: > + "invalid spec, got single_int32=11, wanted <= 10" diff --git a/testing/src/test/resources/policy/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/required_labels/config.yaml b/testing/src/test/resources/policy/required_labels/config.yaml new file mode 100644 index 000000000..14311d763 --- /dev/null +++ b/testing/src/test/resources/policy/required_labels/config.yaml @@ -0,0 +1,32 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "labels" +extensions: + - name: "bindings" + - name: "strings" + version: 2 +variables: + - name: "spec" + type: + type_name: "map" + params: + - type_name: "string" + - type_name: "dyn" + - name: "resource" + type: + type_name: "map" + params: + - type_name: "string" + - type_name: "dyn" diff --git a/testing/src/test/resources/policy/required_labels/policy.yaml b/testing/src/test/resources/policy/required_labels/policy.yaml new file mode 100644 index 000000000..aca75290f --- /dev/null +++ b/testing/src/test/resources/policy/required_labels/policy.yaml @@ -0,0 +1,32 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "required_labels" +rule: + variables: + - name: want + expression: spec.labels + - name: missing + expression: variables.want.filter(l, !(l in resource.labels)) + - name: invalid + expression: > + resource.labels.filter(l, + l in variables.want && variables.want[l] != resource.labels[l]) + match: + - condition: variables.missing.size() > 0 + output: | + "missing one or more required labels: [\"" + variables.missing.join(',') + "\"]" + - condition: variables.invalid.size() > 0 + output: | + "invalid values provided on one or more labels: [\"" + variables.invalid.join(',') + "\"]" diff --git a/testing/src/test/resources/policy/required_labels/tests.yaml b/testing/src/test/resources/policy/required_labels/tests.yaml new file mode 100644 index 000000000..67681ef46 --- /dev/null +++ b/testing/src/test/resources/policy/required_labels/tests.yaml @@ -0,0 +1,79 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +description: "Required labels conformance tests" +section: + - name: "valid" + tests: + - name: "matching" + input: + spec: + value: + labels: + env: prod + experiment: "group b" + resource: + value: + labels: + env: prod + experiment: "group b" + release: "v0.1.0" + output: "optional.none()" + - name: "missing" + tests: + - name: "env" + input: + spec: + value: + labels: + env: prod + experiment: "group b" + resource: + value: + labels: + experiment: "group b" + release: "v0.1.0" + output: > + "missing one or more required labels: [\"env\"]" + - name: "experiment" + input: + spec: + value: + labels: + env: prod + experiment: "group b" + resource: + value: + labels: + env: staging + release: "v0.1.0" + output: > + "missing one or more required labels: [\"experiment\"]" + - name: "invalid" + tests: + - name: "env" + input: + spec: + value: + labels: + env: prod + experiment: "group b" + resource: + value: + labels: + env: staging + experiment: "group b" + release: "v0.1.0" + output: > + "invalid values provided on one or more labels: [\"env\"]" diff --git a/testing/src/test/resources/policy/restricted_destinations/config.yaml b/testing/src/test/resources/policy/restricted_destinations/config.yaml new file mode 100644 index 000000000..b9c8f9750 --- /dev/null +++ b/testing/src/test/resources/policy/restricted_destinations/config.yaml @@ -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. + +name: "labels" +extensions: + - name: "sets" +variables: + - name: "destination.ip" + type: + type_name: "string" + - name: "origin.ip" + type: + type_name: "string" + - name: "spec.restricted_destinations" + type: + type_name: "list" + params: + - type_name: "string" + - name: "spec.origin" + type: + type_name: "string" + - name: "request" + type: + type_name: "map" + params: + - type_name: "string" + - type_name: "dyn" + - name: "resource" + type: + type_name: "map" + params: + - type_name: "string" + - type_name: "dyn" +functions: + - name: "locationCode" + overloads: + - id: "locationCode_string" + args: + - type_name: "string" + return: + type_name: "string" diff --git a/testing/src/test/resources/policy/restricted_destinations/policy.yaml b/testing/src/test/resources/policy/restricted_destinations/policy.yaml new file mode 100644 index 000000000..95fb454d7 --- /dev/null +++ b/testing/src/test/resources/policy/restricted_destinations/policy.yaml @@ -0,0 +1,42 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "restricted_destinations" +rule: + variables: + - name: matches_origin_ip + expression: > + locationCode(origin.ip) == spec.origin + - name: has_nationality + expression: > + has(request.auth.claims.nationality) + - name: matches_nationality + expression: > + variables.has_nationality && request.auth.claims.nationality == spec.origin + - name: matches_dest_ip + expression: > + locationCode(destination.ip) in spec.restricted_destinations + - name: matches_dest_label + expression: > + resource.labels.location in spec.restricted_destinations + - name: matches_dest + expression: > + variables.matches_dest_ip || variables.matches_dest_label + match: + - condition: variables.matches_nationality && variables.matches_dest + output: "true" + - condition: > + !variables.has_nationality && variables.matches_origin_ip && variables.matches_dest + output: "true" + - output: "false" diff --git a/testing/src/test/resources/policy/restricted_destinations/tests.yaml b/testing/src/test/resources/policy/restricted_destinations/tests.yaml new file mode 100644 index 000000000..c0feeb202 --- /dev/null +++ b/testing/src/test/resources/policy/restricted_destinations/tests.yaml @@ -0,0 +1,118 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +description: Restricted destinations conformance tests. +section: + - name: "valid" + tests: + - name: "ip_allowed" + input: + "spec.origin": + value: "us" + "spec.restricted_destinations": + value: + - "cu" + - "ir" + - "kp" + - "sd" + - "sy" + "destination.ip": + value: "10.0.0.1" + "origin.ip": + value: "10.0.0.1" + request: + value: + auth: + claims: {} + resource: + value: + name: "/company/acme/secrets/doomsday-device" + labels: + location: "us" + output: "false" # false means unrestricted + - name: "nationality_allowed" + input: + "spec.origin": + value: "us" + "spec.restricted_destinations": + value: + - "cu" + - "ir" + - "kp" + - "sd" + - "sy" + "destination.ip": + value: "10.0.0.1" + request: + value: + auth: + claims: + nationality: "us" + resource: + value: + name: "/company/acme/secrets/doomsday-device" + labels: + location: "us" + output: "false" + - name: "invalid" + tests: + - name: "destination_ip_prohibited" + input: + "spec.origin": + value: "us" + "spec.restricted_destinations": + value: + - "cu" + - "ir" + - "kp" + - "sd" + - "sy" + "destination.ip": + value: "123.123.123.123" + "origin.ip": + value: "10.0.0.1" + request: + value: + auth: + claims: {} + resource: + value: + name: "/company/acme/secrets/doomsday-device" + labels: + location: "us" + output: "true" # true means restricted + - name: "resource_nationality_prohibited" + input: + "spec.origin": + value: "us" + "spec.restricted_destinations": + value: + - "cu" + - "ir" + - "kp" + - "sd" + - "sy" + "destination.ip": + value: "10.0.0.1" + request: + value: + auth: + claims: + nationality: "us" + resource: + value: + name: "/company/acme/secrets/doomsday-device" + labels: + location: "cu" + output: "true" diff --git a/testing/src/test/resources/protos/BUILD.bazel b/testing/src/test/resources/protos/BUILD.bazel new file mode 100644 index 000000000..af361b174 --- /dev/null +++ b/testing/src/test/resources/protos/BUILD.bazel @@ -0,0 +1,107 @@ +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 = "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/testing/src/test/resources/protos/message_with_enum.proto b/testing/src/test/resources/protos/message_with_enum.proto new file mode 100644 index 000000000..c495eea16 --- /dev/null +++ b/testing/src/test/resources/protos/message_with_enum.proto @@ -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. + +syntax = "proto3"; + +package dev.cel.testing.testdata; + +option java_multiple_files = true; +option java_package = "dev.cel.testing.testdata"; +option java_outer_classname = "MessageWithEnumProto"; + +message MessageWithEnum { + SimpleEnum simple_enum = 1; +} + +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/common/src/test/resources/single_file.proto b/testing/src/test/resources/protos/single_file.proto similarity index 100% rename from common/src/test/resources/single_file.proto rename to testing/src/test/resources/protos/single_file.proto diff --git a/testing/testrunner/BUILD.bazel b/testing/testrunner/BUILD.bazel new file mode 100644 index 000000000..00cb4b832 --- /dev/null +++ b/testing/testrunner/BUILD.bazel @@ -0,0 +1,113 @@ +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"], +) + +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..b2a19f996 --- /dev/null +++ b/testing/testrunner/cel_java_test.bzl @@ -0,0 +1,173 @@ +# 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 cel_java_test( + name, + cel_expr, + test_src, + is_raw_expr = False, + test_suite = "", + filegroup = "", + config = "", + deps = [], + proto_deps = [], + test_data_path = "", + data = []): + """trigger the java impl of the CEL test runner. + + This rule will generate a java_binary and a run_test rule. This rule will be used to trigger + the java impl of the cel_test rule. + + Note: This rule is to be used only for OSS until cel/expr folder is made available in OSS. Internally, + the cel_test rule is supposed to be used. + + Args: + name: str name for the generated artifact + test_suite: str label of a file containing a test suite. The file should have a .yaml or a + .textproto extension. + cel_expr: cel expression to be evaluated. This could be a raw expression or a compiled + expression or cel policy. + is_raw_expr: bool whether the cel_expr is a raw expression or not. If true, the cel_expr + will be used as is and would not be treated as a file path. + filegroup: str label of a filegroup containing the test suite, the config and the checked + expression. + config: str label of a file containing a google.api.expr.conformance.Environment message. + The file should have the .textproto extension. + test_src: user's test class build target. + deps: list of dependencies for the java_binary rule. + data: list of data dependencies for the java_binary rule. + proto_deps: str label of the proto dependencies for the test. Note: This only supports proto_library rules. + test_data_path: absolute path of the directory containing the test files. This is needed only + if the test files are not located in the same directory as the BUILD file. This + would be of the form "//foo/bar". + """ + jvm_flags = [] + + data, test_data_path = _update_data_with_test_files(data, filegroup, test_data_path, config, test_suite, cel_expr, is_raw_expr) + + # Since the test_data_path is of the form "//foo/bar", we need to strip the leading "/" to get + # the absolute path. + test_data_path = test_data_path.lstrip("/") + + if test_suite != "": + test_suite = test_data_path + "/" + test_suite + jvm_flags.append("-Dtest_suite_path=%s" % test_suite) + + if config != "": + config = test_data_path + "/" + config + jvm_flags.append("-Dconfig_path=%s" % config) + + _, cel_expr_format = paths.split_extension(cel_expr) + + if is_valid_cel_file_format(file_extension = cel_expr_format) == True: + jvm_flags.append("-Dcel_expr=%s" % test_data_path + "/" + cel_expr) + elif is_raw_expr == True: + jvm_flags.append("-Dcel_expr='%s'" % cel_expr) + elif not is_valid_cel_file_format(file_extension = cel_expr_format) and not is_raw_expr: + jvm_flags.append("-Dcel_expr=$(location {})".format(cel_expr)) + + if proto_deps: + proto_descriptor_set( + name = name + "_proto_descriptor_set", + deps = proto_deps, + ) + descriptor_set_path = ":" + name + "_proto_descriptor_set" + data.append(descriptor_set_path) + jvm_flags.append("-Dfile_descriptor_set_path=$(location {})".format(descriptor_set_path)) + + java_proto_library( + name = name + "_proto_descriptor_set_java_proto", + deps = proto_deps, + ) + deps = deps + [":" + name + "_proto_descriptor_set_java_proto"] + + jvm_flags.append("-Dis_raw_expr=%s" % is_raw_expr) + + java_binary( + name = name + "_test_runner_binary", + srcs = ["//testing/testrunner:test_runner_binary"], + data = 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", + ] + deps, + ) + + sh_test( + name = name, + tags = ["nomsan"], + srcs = ["//testing/testrunner:run_testrunner_binary.sh"], + data = [ + ":%s_test_runner_binary" % name, + ], + args = [ + name, + ], + ) + +def _update_data_with_test_files(data, filegroup, test_data_path, config, test_suite, cel_expr, is_raw_expr): + """Updates the data with the test files.""" + + _, cel_expr_format = paths.split_extension(cel_expr) + if filegroup != "": + data = data + [filegroup] + elif test_data_path != "" and test_data_path != native.package_name(): + if config != "": + data = data + [test_data_path + ":" + config] + if test_suite != "": + data = data + [test_data_path + ":" + test_suite] + if is_valid_cel_file_format(file_extension = cel_expr_format): + data = data + [test_data_path + ":" + cel_expr] + else: + test_data_path = native.package_name() + if config != "": + data = data + [config] + if test_suite != "": + data = data + [test_suite] + if is_valid_cel_file_format(file_extension = cel_expr_format): + data = data + [cel_expr] + + if not is_valid_cel_file_format(file_extension = cel_expr_format) and not is_raw_expr: + data = data + [cel_expr] + return data, test_data_path + +def is_valid_cel_file_format(file_extension): + """Checks if the file extension is a valid CEL file format. + + Args: + file_extension: The file extension to check. + + Returns: + True if the file extension is a valid CEL file format, False otherwise. + """ + return file_extension in [ + ".cel", + ".celpolicy", + ".yaml", + ] diff --git a/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..c9dd20575 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,8 +68,9 @@ java_library( ], deps = [ "//bundle:cel", - "//common", + "//common:cel_source", "//common:compiler_common", + "//common:source_location", "//common/navigation", "@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..764864b4a 100644 --- a/validator/src/main/java/dev/cel/validator/CelAstValidator.java +++ b/validator/src/main/java/dev/cel/validator/CelAstValidator.java @@ -19,7 +19,9 @@ 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 java.util.Optional; /** Public interface for performing a single, custom validation on an AST. */ public interface CelAstValidator { @@ -53,12 +55,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/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..b7050466c 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 = [ @@ -57,7 +59,7 @@ java_library( ], deps = [ "//bundle:cel", - "//common", + "//common:cel_ast", "//common/ast", "//common/navigation", "//common/types:cel_types", @@ -68,19 +70,36 @@ java_library( ) java_library( - name = "literal_validator", + name = "ast_depth_limit_validator", srcs = [ - "LiteralValidator.java", + "AstDepthLimitValidator.java", ], tags = [ ], + deps = [ + "//bundle:cel", + "//common/navigation", + "//validator:ast_validator", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "literal_validator", + srcs = [ + "LiteralValidator.java", + ], 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", + "//runtime", "//validator:ast_validator", + "@maven//:com_google_errorprone_error_prone_annotations", ], ) 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..58947f0fb 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; @@ -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..2f23dab4c 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,17 @@ 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.runtime.CelEvaluationException; import dev.cel.validator.CelAstValidator; /** @@ -57,7 +61,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, expectedResultType); } catch (Exception e) { issuesFactory.addError( expr.id(), @@ -66,4 +70,21 @@ public void validate(CelNavigableAst navigableAst, Cel cel, IssuesFactory issues } }); } + + @CanIgnoreReturnValue + private static Object evaluateExpr(Cel cel, CelExpr expr, Class expectedResultType) + throws CelValidationException, CelEvaluationException { + CelAbstractSyntaxTree ast = + CelAbstractSyntaxTree.newParsedAst(expr, CelSource.newBuilder().build()); + ast = cel.check(ast).getAst(); + Object result = cel.createProgram(ast).eval(); + + if (!expectedResultType.isInstance(result)) { + throw new IllegalStateException( + String.format( + "Expected %s type but got %s instead", + expectedResultType.getName(), result.getClass().getName())); + } + return result; + } } diff --git a/validator/src/test/java/dev/cel/validator/BUILD.bazel b/validator/src/test/java/dev/cel/validator/BUILD.bazel index f6f94f625..38cf87363 100644 --- a/validator/src/test/java/dev/cel/validator/BUILD.bazel +++ b/validator/src/test/java/dev/cel/validator/BUILD.bazel @@ -1,3 +1,4 @@ +load("@rules_java//java:defs.bzl", "java_library") load("//:testing.bzl", "junit4_test_suites") package(default_applicable_licenses = ["//:license"]) @@ -11,7 +12,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..9e88e0d16 100644 --- a/validator/src/test/java/dev/cel/validator/validators/BUILD.bazel +++ b/validator/src/test/java/dev/cel/validator/validators/BUILD.bazel @@ -1,3 +1,4 @@ +load("@rules_java//java:defs.bzl", "java_library") load("//:testing.bzl", "junit4_test_suites") package(default_applicable_licenses = ["//:license"]) @@ -9,23 +10,28 @@ 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:optional_library", "//runtime", + "//runtime:function_binding", "//validator", "//validator:validator_builder", + "//validator/validators:ast_depth_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/DurationLiteralValidatorTest.java b/validator/src/test/java/dev/cel/validator/validators/DurationLiteralValidatorTest.java index 3fff1c2b1..e89e5ce35 100644 --- a/validator/src/test/java/dev/cel/validator/validators/DurationLiteralValidatorTest.java +++ b/validator/src/test/java/dev/cel/validator/validators/DurationLiteralValidatorTest.java @@ -21,7 +21,6 @@ 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,9 +28,10 @@ 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; @@ -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 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..35a9ffd4f 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; @@ -97,7 +97,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 +119,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 +146,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 +173,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..65b7d593a 100644 --- a/validator/src/test/java/dev/cel/validator/validators/TimestampLiteralValidatorTest.java +++ b/validator/src/test/java/dev/cel/validator/validators/TimestampLiteralValidatorTest.java @@ -21,7 +21,6 @@ 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,9 +29,10 @@ 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; @@ -100,7 +100,8 @@ 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: Failed to parse timestamp: invalid timestamp \"bad\""); } @Test @@ -117,7 +118,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 +137,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 diff --git a/validator/validators/BUILD.bazel b/validator/validators/BUILD.bazel index e4f9ea54b..b5498d682 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,8 @@ 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"], +)